/**
 * §4.5 pairing-code redemption (agent side) — PLAN_RELAY_AGENT T4.
 *
 * REDEEM: POST /enroll { code, agentPubkey, csr }  (raw code NEVER persisted — INV5).
 * RETURN: the FROZEN EnrollResult (imported from relay-contracts, never redeclared), validated
 * with EnrollResultSchema at the boundary (untrusted external data). On success the FIX 3
 * `hostContentSecret` (wrapped to this agent's Ed25519 identity by P3) is UNWRAPPED in-process
 * and persisted 0600 via the keystore; the WRAPPED bytes are never stored, the unwrapped secret
 * is never logged/re-sent (INV5/INV9).
 *
 * Only the PUBLIC key + CSR leave the host (INV4).
 */
import { EnrollResultSchema, decodeBase64UrlBytes, encodeBase64UrlBytes } from 'relay-contracts'
import type { EnrollResult } from 'relay-contracts'
import type { AgentIdentity } from '../keys/identity.js'
import type { Keystore } from '../keys/keystore.js'
import { buildCsr } from './csr.js'
import { derBase64ToPem } from '../certs/pem.js'

/** v0.8 shared-token gate vs v0.9+ per-host Ed25519. Default is `'ed25519'` from v0.9. */
export type EnrollMode = 'token' | 'ed25519'
export const DEFAULT_ENROLL_MODE: EnrollMode = 'ed25519'

export class EnrollError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'EnrollError'
  }
}
export class PairingCodeSpentError extends EnrollError {
  constructor() {
    super('pairing code already redeemed (single-use)')
    this.name = 'PairingCodeSpentError'
  }
}
export class PairingCodeExpiredError extends EnrollError {
  constructor() {
    super('pairing code expired')
    this.name = 'PairingCodeExpiredError'
  }
}

/**
 * Unwrap the P3-wrapped `hostContentSecret` using the agent's enrollment identity (in-process).
 * P3's exact wrap scheme is not yet frozen (cross-plan integration point); this seam is injected
 * so the real unwrap drops in without touching the redeem flow. Default = passthrough.
 */
export type UnwrapContentSecret = (wrapped: Uint8Array, id: AgentIdentity) => Uint8Array
const passthroughUnwrap: UnwrapContentSecret = (wrapped) => wrapped

export interface RedeemOptions {
  readonly fetchImpl?: typeof fetch
  readonly mode?: EnrollMode
  readonly agentToken?: string
  readonly unwrapContentSecret?: UnwrapContentSecret
  readonly subject?: string
  /**
   * Native frp-client enroll has NO E2E content secret — the control-plane returns
   * `hostContentSecret: null`. When true, tolerate its absence (skip unwrap + storage); the plain
   * frpc byte tunnel needs no content key. Defaults false so the legacy relay path still requires it.
   */
  readonly allowMissingContentSecret?: boolean
}

interface EnrollResponseJson {
  hostId: string
  subdomain: string
  cert: string
  caChain: string
  hostContentSecret: string // base64url over the wire
}

function parseEnrollResult(json: unknown, allowMissingContentSecret = false): EnrollResult {
  const j = json as Partial<EnrollResponseJson>
  if (typeof j.hostContentSecret !== 'string') {
    if (!allowMissingContentSecret) {
      throw new EnrollError('enroll response missing hostContentSecret')
    }
    // Native frp-client enroll: cert = base64(DER) string, caChain = base64(DER) string[], no content
    // key. The keystore + frpc need PEM, so convert here. Empty secret sentinel is never stored.
    const caChain: unknown = j.caChain
    if (
      typeof j.hostId !== 'string' ||
      typeof j.subdomain !== 'string' ||
      typeof j.cert !== 'string' ||
      !Array.isArray(caChain) ||
      caChain.length === 0 ||
      !caChain.every((c) => typeof c === 'string')
    ) {
      throw new EnrollError('enroll response missing required fields')
    }
    return {
      hostId: j.hostId,
      subdomain: j.subdomain,
      cert: derBase64ToPem(j.cert),
      caChain: (caChain as string[]).map((c) => derBase64ToPem(c)).join(''),
      hostContentSecret: new Uint8Array(0),
    }
  }
  const candidate = {
    hostId: j.hostId,
    subdomain: j.subdomain,
    cert: j.cert,
    caChain: j.caChain,
    hostContentSecret: decodeBase64UrlBytes(j.hostContentSecret),
  }
  const result = EnrollResultSchema.safeParse(candidate)
  if (!result.success) {
    throw new EnrollError(`enroll response failed schema validation: ${result.error.message}`)
  }
  return result.data
}

/**
 * Redeem `code` at `enrollUrl`. Sends only pubkey + CSR (INV4); never persists the raw code
 * (INV5). Stores the returned cert + CA chain and the unwrapped hostContentSecret 0600.
 */
export async function redeemPairingCode(
  enrollUrl: string,
  code: string,
  id: AgentIdentity,
  ks: Keystore,
  opts: RedeemOptions = {},
): Promise<EnrollResult> {
  const doFetch = opts.fetchImpl ?? fetch
  const mode = opts.mode ?? DEFAULT_ENROLL_MODE
  const unwrap = opts.unwrapContentSecret ?? passthroughUnwrap

  const body =
    mode === 'token'
      ? { code, agentToken: opts.agentToken ?? '' }
      : {
          code,
          agentPubkey: encodeBase64UrlBytes(id.publicKey),
          csr: buildCsr(id, opts.subject ?? 'web-terminal-agent'),
        }

  let res: Response
  try {
    res = await doFetch(enrollUrl, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(body),
    })
  } catch (err) {
    throw new EnrollError(`enroll request failed: ${(err as Error).message}`)
  }

  if (res.status === 409) throw new PairingCodeSpentError()
  if (res.status === 410) throw new PairingCodeExpiredError()
  if (!res.ok) throw new EnrollError(`enroll returned HTTP ${res.status}`)

  let json: unknown
  try {
    json = await res.json()
  } catch (err) {
    throw new EnrollError(`enroll response was not JSON: ${(err as Error).message}`)
  }

  const enroll = parseEnrollResult(json, opts.allowMissingContentSecret ?? false)
  ks.saveCert(enroll.cert, enroll.caChain)
  // FIX 3: unwrap in-process, persist ONLY the unwrapped secret (wrapped bytes never stored).
  // Native frp-client enroll has no content secret (empty sentinel) → nothing to unwrap/store.
  if (enroll.hostContentSecret.length > 0) {
    const unwrapped = unwrap(enroll.hostContentSecret, id)
    ks.saveContentSecret(unwrapped)
  }
  return enroll
}
