/**
 * T4 — host registry: the ownership source of truth (INV1). A host is resolvable ONLY via
 * `hostId`/`subdomain` bound to an account — there is NO raw address/port/hostname resolution
 * path in this module. `ownsHost` is the deny-by-default ownership predicate (INV1/INV6). DB
 * stores only the PUBLIC Ed25519 key (INV4). Status changes are versioned snapshots (INV8).
 */
import type { HostRecord, HostStatus } from '../model/records.js'
import type { HostStore } from '../store/ports.js'
import type { AuditWriter } from '../audit/log.js'
import { noopAuditWriter } from '../audit/log.js'
import { newUuid, nowIso } from '../util/ids.js'
import { fingerprint } from '../ca/fingerprint.js'

export interface BindHostInput {
  readonly accountId: string
  readonly subdomain: string
  readonly agentPubkey: Uint8Array
  readonly enrollFpr: string
}

export interface HostRegistry {
  bindHost(input: BindHostInput): Promise<HostRecord>
  getHost(hostId: string): Promise<HostRecord | null>
  getHostBySubdomain(subdomain: string): Promise<HostRecord | null>
  listHosts(accountId: string): Promise<readonly HostRecord[]>
  setHostStatus(hostId: string, status: HostStatus): Promise<HostRecord>
  ownsHost(accountId: string, hostId: string): Promise<boolean>
  touchLastSeen(hostId: string): Promise<void>
}

export interface HostRegistryDeps {
  readonly hosts: HostStore
  readonly audit?: AuditWriter
  readonly actor?: string
}

export function createHostRegistry(deps: HostRegistryDeps): HostRegistry {
  const audit = deps.audit ?? noopAuditWriter()
  const actor = deps.actor ?? 'system'
  return {
    async bindHost(input) {
      // enroll_fpr MUST be the fingerprint of the presented pubkey (defence-in-depth: recompute).
      const expectedFpr = fingerprint(input.agentPubkey)
      if (input.enrollFpr !== expectedFpr) {
        throw new Error('enrollFpr does not match agentPubkey fingerprint')
      }
      const rec: HostRecord = {
        hostId: newUuid(), // unguessable UUIDv4, never recycled (INV1)
        accountId: input.accountId,
        subdomain: input.subdomain,
        // Defensive copy → fresh ArrayBuffer-backed array (immutability + TS6 variance). PUBLIC key only (INV4).
        agentPubkey: new Uint8Array(input.agentPubkey),
        enrollFpr: input.enrollFpr,
        status: 'offline',
        lastSeen: nowIso(),
        createdAt: nowIso(),
        revokedAt: null,
      }
      await deps.hosts.insert(rec)
      await audit.writeAuditEvent({
        action: 'host.bind',
        principalId: actor,
        accountId: input.accountId,
        hostId: rec.hostId,
        ts: rec.createdAt,
        meta: { subdomain: input.subdomain, enrollFpr: input.enrollFpr },
      })
      return rec
    },
    async getHost(hostId) {
      return deps.hosts.get(hostId)
    },
    async getHostBySubdomain(subdomain) {
      return deps.hosts.getBySubdomain(subdomain)
    },
    async listHosts(accountId) {
      return deps.hosts.listByAccount(accountId) // ownership-scoped
    },
    async setHostStatus(hostId, status) {
      const revokedAt = status === 'revoked' ? nowIso() : null
      const next = await deps.hosts.swapStatus(hostId, status, revokedAt, actor)
      if (status === 'revoked') {
        await audit.writeAuditEvent({
          action: 'host.revoke',
          principalId: actor,
          accountId: next.accountId,
          hostId,
          ts: nowIso(),
          meta: { status },
        })
      }
      return next
    },
    async ownsHost(accountId, hostId) {
      const host = await deps.hosts.get(hostId)
      // Deny-by-default: unknown host or account mismatch ⇒ false (INV1/INV6).
      return host !== null && host.accountId === accountId
    },
    async touchLastSeen(hostId) {
      await deps.hosts.touchLastSeen(hostId, nowIso()) // advisory, not versioned (INV7)
    },
  }
}
