/**
 * T11 — provisioning / deprovisioning HTTP routes. ALL admin routes are deny-by-default and scope
 * every operation to `principal.accountId` (INV3/INV6). Host-targeting routes re-check
 * `ownsHost` (INV1). No route resolves a host by a user-supplied address. `/enroll` is the one
 * route NOT gated by a capability token (it IS the auth bootstrap) but is guarded by the single-use
 * pairing code (T8). Every body is Zod-validated at the boundary; malformed → 400.
 */
import { z } from 'zod'
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import type { PlanTier } from 'relay-contracts'
import type { Authorizer, AdminPrincipal } from './authz.js'
import { AuthzError } from './authz.js'
import type { AccountRegistry } from '../registry/accounts.js'
import type { HostRegistry } from '../registry/hosts.js'
import type { PairingIssuer } from '../pairing/issue.js'
import type { PairingRedeemer } from '../pairing/redeem.js'
import { RedeemError } from '../pairing/redeem.js'
import type { NativeHostEnroller } from '../pairing/native-redeem.js'
import { decodeCsrWire } from '../ca/csr.js'
import { isEcP256Csr } from '../ca/csr-ec.js'
import type { Deprovisioner } from '../deprovision/deprovision.js'

export interface ProvisionDeps {
  readonly authorizer: Authorizer
  readonly accounts: AccountRegistry
  readonly hosts: HostRegistry
  readonly pairingIssuer: PairingIssuer
  readonly redeemer: PairingRedeemer
  readonly deprovisioner: Deprovisioner
  /**
   * Native (EC-P256) frp-client enrollment arm. When present, an EC-P256 CSR on `/enroll` is routed
   * here (P-256 frp-client leaf); Ed25519 CSRs always take the relay `redeemer`. Optional/additive:
   * absent → EC CSRs fall through to the relay arm, which rejects a non-Ed25519 key (deny-by-default).
   */
  readonly nativeEnroller?: NativeHostEnroller
}

const PlanSchema = z.enum(['free', 'personal', 'pro', 'team'])
const StatusSchema = z.object({ status: z.enum(['active', 'suspended']) })
const EnrollSchema = z.object({
  code: z.string().min(1),
  agentPubkey: z.string().min(1), // base64 (agent sends base64url; Buffer decodes both)
  csr: z.string().min(1), // PKCS#10: PEM block (agent) or base64(DER) — see decodeCsrWire
  machineId: z.string().min(1).max(256).optional(), // native arm only; accepted + audited, no dedup yet (M-cp-idempotent)
})
// NOT `.strict()`: any client-supplied `subdomain` (or other extra field) is INERT — the native arm's
// SAN comes ONLY from the server-assigned host binding (A4 anti-smuggling), never from the body.

function sendError(reply: FastifyReply, err: unknown): void {
  if (err instanceof AuthzError) {
    void reply.code(err.status).send({ error: err.message })
    return
  }
  if (err instanceof RedeemError) {
    const status = err.code === 'too_many_attempts' ? 429 : 400
    void reply.code(status).send({ error: err.code })
    return
  }
  void reply.code(400).send({ error: err instanceof Error ? err.message : 'bad request' })
}

/** Enforce that the path :id equals the authenticated principal's account (INV1). */
function assertOwnAccount(principal: AdminPrincipal, pathAccountId: string): void {
  if (principal.accountId !== pathAccountId) {
    throw new AuthzError(403, 'account scope mismatch') // never operate on another tenant
  }
}

export function buildRouter(deps: ProvisionDeps): FastifyPluginAsync {
  return async (app) => {
    const principal = (req: FastifyRequest): Promise<AdminPrincipal> =>
      deps.authorizer.principalFromRequest({ headers: req.headers })

    app.post('/accounts', async (req, reply) => {
      try {
        const p = await principal(req)
        deps.authorizer.requireRight(p, 'manage')
        const plan: PlanTier = PlanSchema.parse((req.body as { plan?: unknown })?.plan ?? 'free')
        const account = await deps.accounts.createAccount(plan)
        await reply.code(201).send(account)
      } catch (err) {
        sendError(reply, err)
      }
    })

    app.post('/accounts/:id/pairing-codes', async (req, reply) => {
      try {
        const p = await principal(req)
        deps.authorizer.requireRight(p, 'manage')
        assertOwnAccount(p, (req.params as { id: string }).id)
        const issued = await deps.pairingIssuer.issuePairingCode(p.accountId)
        await reply.code(201).send(issued)
      } catch (err) {
        sendError(reply, err)
      }
    })

    app.post('/accounts/:id/status', async (req, reply) => {
      try {
        const p = await principal(req)
        deps.authorizer.requireRight(p, 'manage')
        assertOwnAccount(p, (req.params as { id: string }).id)
        const { status } = StatusSchema.parse(req.body)
        const account = await deps.accounts.setAccountStatus(p.accountId, status)
        await reply.send(account)
      } catch (err) {
        sendError(reply, err)
      }
    })

    app.get('/accounts/:id/hosts', async (req, reply) => {
      try {
        const p = await principal(req)
        assertOwnAccount(p, (req.params as { id: string }).id)
        const hosts = await deps.hosts.listHosts(p.accountId) // ownership-scoped
        await reply.send(hosts.map((h) => ({ ...h, agentPubkey: Buffer.from(h.agentPubkey).toString('base64') })))
      } catch (err) {
        sendError(reply, err)
      }
    })

    app.delete('/hosts/:hostId', async (req, reply) => {
      try {
        const p = await principal(req)
        deps.authorizer.requireRight(p, 'manage')
        // account_id in the body is IGNORED — authz uses ONLY the token principal (INV3).
        await deps.deprovisioner.deprovisionHost(p, (req.params as { hostId: string }).hostId)
        await reply.code(204).send()
      } catch (err) {
        sendError(reply, err)
      }
    })

    app.post('/enroll', async (req, reply) => {
      // NOT capability-token gated — guarded by the single-use pairing code (T8).
      try {
        const body = EnrollSchema.parse(req.body)
        const agentPubkey = new Uint8Array(Buffer.from(body.agentPubkey, 'base64'))
        const csr = decodeCsrWire(body.csr)

        // Fork by CSR key algorithm: an EC-P256 CSR is a NATIVE frp-client host → P-256 leaf; an
        // Ed25519 CSR is the legacy relay host (unchanged). Routing is a pure SPKI parse; each arm
        // verifies the CSR self-signature inside its own gate.
        if (deps.nativeEnroller !== undefined && isEcP256Csr(csr)) {
          const native = await deps.nativeEnroller.enrollNativeHost({
            code: body.code,
            agentPubkey,
            csr,
            ...(body.machineId !== undefined ? { machineId: body.machineId } : {}),
          })
          // Native tunnel has no E2E-relay content secret (L-host-hcs); cert/caChain are base64(DER).
          await reply.code(201).send({
            hostId: native.hostId,
            subdomain: native.subdomain,
            cert: Buffer.from(native.cert).toString('base64'),
            caChain: native.caChain.map((der) => Buffer.from(der).toString('base64')),
            hostContentSecret: null,
          })
          return
        }

        const result = await deps.redeemer.redeemPairingCode({ code: body.code, agentPubkey, csr })
        // base64URL (not base64) — the agent decodes this via decodeBase64UrlBytes (enroll/pair.ts).
        await reply.code(201).send({ ...result, hostContentSecret: Buffer.from(result.hostContentSecret).toString('base64url') })
      } catch (err) {
        sendError(reply, err)
      }
    })
  }
}
