/**
 * T8 — pairing-code redemption + BIND + bind-time cert + content-secret mint (INDEX §4.5 3–5).
 * ATOMIC single-use: `casRedeem` compare-and-sets `redeemed_at` so exactly one concurrent caller
 * wins (no double-spend). A CODE-SCOPED lockout (Finding-4) counts failed redemptions per
 * `code_hash` and locks after `pairingMaxRedeemAttempts` — independent of P5's per-tenant limiter.
 * `accountId` is taken from the pairing ROW (INV3), never the agent's request. Returns the FROZEN
 * `EnrollResult` (imported from relay-contracts, never redefined).
 */
import type { EnrollResult } from 'relay-contracts'
import type { PairingStore } from '../store/ports.js'
import type { HostRegistry } from '../registry/hosts.js'
import type { SubdomainAssigner } from '../subdomain/assign.js'
import type { LeafSigner } from '../ca/sign.js'
import type { AuditWriter } from '../audit/log.js'
import { noopAuditWriter } from '../audit/log.js'
import { verifyCsrPoP } from '../ca/csr.js'
import { fingerprint } from '../ca/fingerprint.js'
import { sha256Hex } from '../util/hash.js'
import { timingSafeEqualBytes, bytesToBase64 } from '../util/bytes.js'
import { sealToX25519 } from '../util/crypto.js'
import { normalizePairingCode } from './code.js'
import { nowIso } from '../util/ids.js'
import { randomBytes } from 'node:crypto'

export type RedeemErrorCode =
  | 'unknown'
  | 'expired'
  | 'already_redeemed'
  | 'too_many_attempts'
  | 'bad_csr'

export class RedeemError extends Error {
  constructor(public readonly code: RedeemErrorCode) {
    super(`pairing redemption refused: ${code}`)
  }
}

export interface RedeemInput {
  readonly code: string
  readonly agentPubkey: Uint8Array
  readonly csr: Uint8Array
}

/**
 * CSR proof-of-possession verifier: returns whether the self-signature is valid and the embedded
 * public key. The Ed25519 relay arm passes `verifyCsrPoP` directly; the native EC-P256 arm adapts
 * `verifyCsrPoPEc` to this shape. Selecting the verifier is the ONLY key-algorithm difference in the
 * shared pairing gate below.
 */
export type CsrPopVerifier = (csr: Uint8Array) => Promise<{ ok: boolean; embeddedPub: Uint8Array }>

/** Just the pairing primitives + the code-scoped lockout budget the shared gate needs. */
export interface PairingGateDeps {
  readonly pairing: PairingStore
  readonly pairingMaxRedeemAttempts: number
}

/**
 * The SHARED, security-critical pairing gate reused by BOTH /enroll arms (relay + native): lookup →
 * code-scoped lockout (Finding-4) → expiry → single-use → CSR proof-of-possession + no-substitution →
 * atomic single-use compare-and-set (double-spend guard). Ordered EXACTLY as the original relay path;
 * `verifyCsr` selects the key algorithm. On success returns the `accountId` from the pairing ROW
 * (INV3 — never a request field). Throws `RedeemError` on any failure; a bad CSR counts toward the
 * code-scoped lockout. Extracted so the native arm cannot drift from the relay gate's exact ordering.
 */
export async function gateAndConsumePairingCode(
  deps: PairingGateDeps,
  input: RedeemInput,
  verifyCsr: CsrPopVerifier,
): Promise<{ accountId: string }> {
  const codeHash = sha256Hex(normalizePairingCode(input.code))
  const row = await deps.pairing.get(codeHash)
  if (row === null) throw new RedeemError('unknown') // blind guess; P5 limiter covers volume

  // Lockout FIRST — a locked code is refused even with a correct code (Finding-4).
  if (row.redeemAttempts >= deps.pairingMaxRedeemAttempts) throw new RedeemError('too_many_attempts')
  if (Date.parse(row.record.expiresAt) <= Date.now()) throw new RedeemError('expired')
  if (row.record.redeemedAt !== null) throw new RedeemError('already_redeemed')

  // CSR proof-of-possession + no-substitution. A failure counts toward the code-scoped lockout.
  const pop = await verifyCsr(input.csr)
  if (!pop.ok || !timingSafeEqualBytes(pop.embeddedPub, input.agentPubkey)) {
    await deps.pairing.registerFailure(codeHash)
    throw new RedeemError('bad_csr')
  }

  // Atomic single-use CAS (double-spend guard) — exactly one concurrent caller wins.
  const cas = await deps.pairing.casRedeem(codeHash, nowIso())
  if (cas !== 'ok') throw new RedeemError('already_redeemed')

  return { accountId: row.record.accountId } // from the pairing row (INV3)
}

/**
 * FIX 3 — mint + WRAP the host-scoped content secret at BIND. A per-host 32-byte CSPRNG secret
 * is sealed to the host's enrolled identity; the raw secret is NEVER stored or logged (INV5/INV9)
 * and each wrap is distinct (per-host revocable, INV12). P2 unwraps locally with its private key.
 */
export async function mintHostContentSecret(agentPubkey: Uint8Array): Promise<Uint8Array> {
  const secret = new Uint8Array(randomBytes(32))
  const wrapped = sealToX25519(agentPubkey, secret)
  secret.fill(0) // best-effort scrub of the raw secret
  return wrapped
}

export interface RedeemDeps {
  readonly pairing: PairingStore
  readonly hosts: HostRegistry
  readonly subdomains: SubdomainAssigner
  readonly leafSigner: LeafSigner
  readonly pairingMaxRedeemAttempts: number
  readonly audit?: AuditWriter
  /** Injectable content-secret minter (test seam); defaults to `mintHostContentSecret`. */
  readonly mintSecret?: (agentPubkey: Uint8Array) => Promise<Uint8Array>
}

export interface PairingRedeemer {
  redeemPairingCode(input: RedeemInput): Promise<EnrollResult>
}

function toPem(label: string, der: Uint8Array): string {
  const b64 = bytesToBase64(der)
  const lines = b64.match(/.{1,64}/g) ?? [b64]
  return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`
}

export function createPairingRedeemer(deps: RedeemDeps): PairingRedeemer {
  const audit = deps.audit ?? noopAuditWriter()
  const mint = deps.mintSecret ?? mintHostContentSecret
  return {
    async redeemPairingCode(input) {
      // Shared, security-critical gate (single-use CAS + lockout + expiry + Ed25519 PoP). Unchanged
      // ordering — the native EC arm reuses the SAME gate with an EC verifier (see native-redeem.ts).
      const { accountId } = await gateAndConsumePairingCode(
        { pairing: deps.pairing, pairingMaxRedeemAttempts: deps.pairingMaxRedeemAttempts },
        input,
        verifyCsrPoP,
      )
      const subdomain = await deps.subdomains.assignSubdomain(accountId)
      const host = await deps.hosts.bindHost({
        accountId,
        subdomain,
        agentPubkey: input.agentPubkey,
        enrollFpr: fingerprint(input.agentPubkey),
      })
      const leaf = await deps.leafSigner.signHostLeaf(host.hostId, input.agentPubkey, input.csr)
      const hostContentSecret = await mint(input.agentPubkey)

      await audit.writeAuditEvent({
        action: 'pairing.redeem',
        principalId: `host:${host.hostId}`,
        accountId,
        hostId: host.hostId,
        ts: nowIso(),
        meta: { subdomain },
      })

      return {
        hostId: host.hostId,
        subdomain,
        cert: toPem('CERTIFICATE', leaf.cert),
        caChain: leaf.caChain.map((der) => toPem('CERTIFICATE', der)).join(''),
        hostContentSecret,
      }
    },
  }
}
