/**
 * Session-gated proxy routes. A `preHandler` rejects any request without a valid session cookie
 * (401) — nothing downstream runs unauthenticated. For EACH call we mint a FRESH `manage` token and
 * hand it to the CP admin client:
 *   - GET    /api/hosts            → CP GET /accounts/{op}/hosts        → curated host list
 *   - POST   /api/pairing-codes    → CP POST /accounts/{op}/pairing-codes → { code, expiresAt, pairCommand, qrDataUrl }
 *   - DELETE /api/hosts/:hostId    → CP DELETE /hosts/:hostId           → 204
 *
 * Upstream/mint failures map to a generic 502 (never leak CP internals or token/key material).
 */
import { z } from 'zod'
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import type { PanelConfig } from '../config.js'
import type { CpClient } from '../cp-client.js'
import { CpClientError } from '../cp-client.js'
import type { ManageTokenMinter } from '../manage-token.js'
import { buildPairingArtifacts } from '../pairing.js'
import { requestIsAuthed } from '../security/request.js'

// hostId is a server-issued UUIDv4 — accept only a conservative id charset (defence-in-depth).
// A bare `.` or `..` passes the charset but would reshape the outbound CP admin URL
// (`${cpUrl}/hosts/${hostId}`) via dot-segment normalization, so reject those two exact values.
export const HostIdSchema = z
  .string()
  .min(1)
  .max(128)
  .regex(/^[A-Za-z0-9._-]+$/)
  .refine((v) => v !== '.' && v !== '..', { message: 'host id must not be a dot-segment' })

export interface ApiRoutesDeps {
  readonly config: PanelConfig
  readonly cpClient: CpClient
  readonly minter: ManageTokenMinter
  readonly now?: () => number
  /** Server-side logger for upstream failures (no secrets). Defaults to a no-op. */
  readonly logError?: (message: string) => void
}

function mapUpstreamError(reply: FastifyReply, err: unknown, log: (m: string) => void): FastifyReply {
  if (err instanceof CpClientError) {
    log(`upstream control-plane error: ${err.message}`)
    return reply.code(502).send({ error: 'upstream_error' })
  }
  log(`proxy error: ${err instanceof Error ? err.name : 'unknown'}`)
  return reply.code(502).send({ error: 'upstream_error' })
}

export function buildApiRoutes(deps: ApiRoutesDeps): FastifyPluginAsync {
  const now = deps.now ?? (() => Date.now())
  const log = deps.logError ?? (() => {})
  const accountId = deps.config.operatorAccountId

  return async (app) => {
    // Deny-by-default session gate for every route in this plugin.
    app.addHook('preHandler', async (req: FastifyRequest, reply: FastifyReply) => {
      if (!requestIsAuthed(req, deps.config.sessionSecret, now())) {
        await reply.code(401).send({ error: 'unauthenticated' })
      }
    })

    app.get('/api/hosts', async (_req, reply) => {
      try {
        const token = await deps.minter.mint()
        const hosts = await deps.cpClient.listHosts(accountId, token)
        return reply.code(200).send({ hosts })
      } catch (err) {
        return mapUpstreamError(reply, err, log)
      }
    })

    app.post('/api/pairing-codes', async (_req, reply) => {
      try {
        const token = await deps.minter.mint()
        const issued = await deps.cpClient.createPairingCode(accountId, token)
        const artifacts = await buildPairingArtifacts(issued, deps.config.tunnelZone)
        return reply.code(201).send(artifacts)
      } catch (err) {
        return mapUpstreamError(reply, err, log)
      }
    })

    app.delete('/api/hosts/:hostId', async (req, reply) => {
      const parsed = HostIdSchema.safeParse((req.params as { hostId?: unknown }).hostId)
      if (!parsed.success) return reply.code(400).send({ error: 'invalid host id' })
      try {
        const token = await deps.minter.mint()
        await deps.cpClient.deleteHost(parsed.data, token)
        return reply.code(204).send()
      } catch (err) {
        return mapUpstreamError(reply, err, log)
      }
    })
  }
}
