/**
 * T13 — revocation (global + per-host), INV12. The within-seconds tunnel-kill PUBLISHES a
 * KillSignal on the FROZEN `relay:revocations` bus (INDEX §4.2 FIX 4 — SAME channel T10 drain
 * uses); every P1 node subscribes and injects the §4.1 CLOSE+RST within REVOCATION_PUSH_BUDGET_MS.
 * KillSignal / RevocationScope / the channel + budget constants are IMPORTED from relay-contracts,
 * never redefined. `reason` is metadata only (INV10, zero payload). Revocation writes an immutable
 * host snapshot (INV8) and is idempotent.
 */
import { REVOCATION_PUSH_BUDGET_MS, type RevocationBus } from 'relay-contracts'
import type { HostRegistry } from '../registry/hosts.js'
import type { RoutingTable } from '../routing/table.js'
import type { AuditWriter } from '../audit/log.js'
import { noopAuditWriter } from '../audit/log.js'

export { REVOCATION_PUSH_BUDGET_MS }

/** Short-TTL token revocation store (Redis `revoked:{jti}` in prod). Shared key space with P5. */
export interface RevokedTokenStore {
  revoke(jti: string, ttlSec: number): Promise<void>
  isRevoked(jti: string): Promise<boolean>
}

/** In-memory implementation with TTL expiry (default; Redis is the production swap). */
export function createInMemoryRevokedTokenStore(): RevokedTokenStore {
  const until = new Map<string, number>()
  return {
    async revoke(jti, ttlSec) {
      until.set(jti, Date.now() + Math.max(0, ttlSec) * 1000)
    },
    async isRevoked(jti) {
      const exp = until.get(jti)
      if (exp === undefined) return false
      if (exp <= Date.now()) {
        until.delete(jti)
        return false
      }
      return true
    },
  }
}

export interface Revoker {
  revokeHost(hostId: string): Promise<void>
  revokeAccount(accountId: string): Promise<void>
  revokeToken(jti: string, expUnix: number): Promise<void>
  isTokenRevoked(jti: string): Promise<boolean>
}

export interface RevokerDeps {
  readonly hosts: HostRegistry
  readonly routing: RoutingTable
  readonly bus: RevocationBus
  readonly tokens: RevokedTokenStore
  readonly audit?: AuditWriter
  readonly actor?: string
}

export function createRevoker(deps: RevokerDeps): Revoker {
  const audit = deps.audit ?? noopAuditWriter()
  const actor = deps.actor ?? 'system'
  const at = () => Math.floor(Date.now() / 1000)
  return {
    async revokeHost(hostId) {
      const host = await deps.hosts.getHost(hostId)
      if (host === null) return // idempotent: nothing to revoke
      if (host.status !== 'revoked') {
        await deps.hosts.setHostStatus(hostId, 'revoked') // immutable snapshot (INV8) + audit host.revoke
      }
      await deps.routing.systemDropRoute(hostId) // resolveRoute → null after this
      await deps.bus.publish({ scope: { kind: 'host', hostId }, at: at(), reason: 'revoked' })
      await audit.writeAuditEvent({
        action: 'revoke',
        principalId: actor,
        accountId: host.accountId,
        hostId,
        ts: new Date().toISOString(),
        meta: { scope: 'host' },
      })
    },
    async revokeAccount(accountId) {
      const hosts = await deps.hosts.listHosts(accountId)
      for (const h of hosts) {
        if (h.status !== 'revoked') {
          // eslint-disable-next-line no-await-in-loop
          await deps.hosts.setHostStatus(h.hostId, 'revoked')
        }
        // eslint-disable-next-line no-await-in-loop
        await deps.routing.systemDropRoute(h.hostId)
      }
      // One account-scoped KillSignal; P1 tears down all of the account's live streams.
      await deps.bus.publish({ scope: { kind: 'account', accountId }, at: at(), reason: 'revoked' })
      await audit.writeAuditEvent({
        action: 'revoke',
        principalId: actor,
        accountId,
        hostId: null,
        ts: new Date().toISOString(),
        meta: { scope: 'account', hosts: String(hosts.length) },
      })
    },
    async revokeToken(jti, expUnix) {
      const ttlSec = Math.max(0, expUnix - Math.floor(Date.now() / 1000))
      await deps.tokens.revoke(jti, ttlSec)
    },
    async isTokenRevoked(jti) {
      return deps.tokens.isRevoked(jti)
    },
  }
}
