/**
 * A2 — P3 control-plane server entrypoint. Wires validated env → Postgres pool/stores → migrations →
 * ioredis revocation bus → real capability verifier → Fastify app, then binds the ADMIN provisioning
 * API to a LOOPBACK host by default (CP_BIND_HOST). This process owns no terminal/PTY state, so
 * PTY!=WS restart-safety (INV7) is unaffected: it only serves provisioning/pairing/revocation.
 *
 * Config is env-only (no hardcoded hosts/ports/secrets): control-plane secrets via `loadEnv`
 * (fail-fast, never echoes values — INV9); bind address via CP_BIND_HOST / CP_BIND_PORT.
 * accountId is only ever derived from the authenticated capability token inside buildControlPlane
 * (INV3) — this entrypoint never fabricates identity.
 */
import { loadEnv } from './env.js'
import { createPgPool, createQuery } from './db/pool.js'
import { createPgStores } from './store/pg.js'
import { runMigrations } from './db/migrate.js'
import { createRedisRevocationBus } from './routing/bus.js'
import { createCapabilityVerifier, configureCapabilityVerifyKey } from './boot/verifier.js'
import { createRedisClient, createRedisPublisher } from './boot/redis.js'
import { buildControlPlane, type ControlPlaneOverrides } from './main.js'
import { buildFileBackedNativeCas } from './boot/native-ca.js'

/** Admin API binds to loopback by default — never expose the provisioning API on a public interface. */
const DEFAULT_CP_BIND_HOST = '127.0.0.1'
const DEFAULT_CP_BIND_PORT = 8080
const MAX_TCP_PORT = 65535

function resolveBindHost(source: NodeJS.ProcessEnv): string {
  const raw = source.CP_BIND_HOST?.trim()
  return raw === undefined || raw === '' ? DEFAULT_CP_BIND_HOST : raw
}

function resolveBindPort(source: NodeJS.ProcessEnv): number {
  const raw = source.CP_BIND_PORT?.trim()
  if (raw === undefined || raw === '') return DEFAULT_CP_BIND_PORT
  const port = Number(raw)
  if (!Number.isInteger(port) || port < 1 || port > MAX_TCP_PORT) {
    throw new Error('Invalid CP_BIND_PORT: must be an integer 1-65535')
  }
  return port
}

async function main(): Promise<void> {
  const env = loadEnv(process.env)

  // Postgres: parameterized-only query wrapper + PG-backed repository ports, migrations applied
  // idempotently at boot.
  const pool = createPgPool(env.pgUrl)
  const query = createQuery(pool)
  const stores = createPgStores(query)
  await runMigrations(query)

  // Redis: single client drives the revocation-bus publish side (relay:revocations).
  const redis = createRedisClient(env.redisUrl)
  const bus = createRedisRevocationBus(createRedisPublisher(redis))

  // Capability verifier: load the §4.3 verifying key into relay-auth's registry BEFORE serving,
  // then delegate to relay-auth's async verifyCapabilityToken (INV3).
  await configureCapabilityVerifyKey(env.capabilitySignPubkey)
  const verifier = createCapabilityVerifier()

  // A2-prep: when NATIVE_* on-disk CA material is configured, load the two REAL P-256 CAs (frp-client
  // + device) from disk and thread them into the app so /enroll + /device/enroll issue leaves from the
  // production CAs. `buildFileBackedNativeCas` fails fast (INV9) on unreadable/non-P-256 material. When
  // unset, `buildControlPlane` keeps its existing behaviour (dev self-signed CAs, or fail-closed if
  // NODE_ENV=production) — the dev/test injection path is untouched.
  const nativeOverrides: Pick<ControlPlaneOverrides, 'production' | 'nativeCas' | 'nativeDnsZone'> =
    env.nativeCa !== undefined
      ? {
          production: true,
          nativeCas: await buildFileBackedNativeCas(env, env.nativeCa),
          nativeDnsZone: env.nativeCa.dnsZone,
        }
      : {}

  const { app } = await buildControlPlane(env, { stores, bus, verifier, ...nativeOverrides })

  const host = resolveBindHost(process.env)
  const port = resolveBindPort(process.env)

  let shuttingDown = false
  const shutdown = async (signal: string): Promise<void> => {
    if (shuttingDown) return
    shuttingDown = true
    process.stdout.write(`control-plane received ${signal}, shutting down\n`)
    try {
      await app.close()
    } finally {
      // Best-effort Redis close; force-disconnect if a graceful QUIT cannot complete.
      await redis.quit().catch(() => redis.disconnect())
      await pool.end()
    }
  }

  for (const signal of ['SIGINT', 'SIGTERM'] as const) {
    process.once(signal, () => {
      void shutdown(signal).then(
        () => process.exit(0),
        (err: unknown) => {
          process.stderr.write(`shutdown failed: ${err instanceof Error ? err.message : String(err)}\n`)
          process.exit(1)
        },
      )
    })
  }

  await app.listen({ host, port })
  // Startup breadcrumb — bind address only, never secrets (INV9).
  process.stdout.write(`control-plane listening on ${host}:${port}\n`)
}

main().catch((err: unknown) => {
  process.stderr.write(`control-plane failed to start: ${err instanceof Error ? err.message : String(err)}\n`)
  process.exit(1)
})
