/**
 * Phase 1 PRODUCTION entrypoint — `npm run start:phase1`. Composes the SHARED-STORE data plane
 * (B1–B4) behind a publicly-bound TLS listener, serves the relay-web bundle same-origin as the
 * browser WSS (D1), and hosts a STAGING operator token-mint (`POST /auth/mint`, B5/auth-mint.ts).
 *
 * Unlike Phase-0 `main.ts` (in-RAM fakes + self-signed dev CA), everything here is real and
 * env-configured — one world of truth over the SAME Postgres + Redis as the control-plane (INV7,
 * restart-safe): the host registry that gates mTLS (INV14) and route resolution, the Redis
 * revocation bus that tears live tunnels down (INV12), and the shared P5 verify key (INV9).
 *
 *   browser ──WSS(:BIND_PORT)──▶ relay-node ──opaque splice(INV2)──▶ agent tunnel ◀──mTLS(:AGENT_BIND_PORT)── agent
 *                    │ P5 onUpgrade: Origin/CSWSH + capability verify + DPoP        │ registry-gated verifyAgentCert
 *                    └ same-origin: static bundle (D1) + POST /auth/mint (B5)        └ Redis relay:revocations → teardown
 *
 * All configuration is from ENV (no hardcoded hosts/ports/secrets). Phase-0 `main.ts` is UNTOUCHED.
 */
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

import { loadVerifyKeyFromEnv, KeyConfigError } from 'relay-auth/src/config/keys.js'
import { createPgPool, createQuery } from 'control-plane/src/db/pool.js'
import { createPgStores } from 'control-plane/src/store/pg.js'
import { createRedisClient } from 'control-plane/src/boot/redis.js'
import type {
  MtlsVerifier,
  TlsServerFactory,
} from 'term-relay/data-plane/agent-listener.js'

import { createRelayEnforceDeps } from './wiring/stores-pg.js'
import { createMtlsVerifier, type AsyncMtlsVerifier } from './wiring/mtls-verifier.js'
import { createStoreRouteResolver } from './wiring/route-resolver.js'
import { startRevocationSubscriber, type RevocableNode } from './wiring/revocation-subscriber.js'
import { createAuthorizer } from './wiring/authorizer.js'
import { buildDataPlane, makeDataPlaneConfig } from './wiring/data-plane.js'
import { makeAgentTlsServerFactory } from './servers/agent-tls.js'
import { startBrowserServer } from './servers/browser-server.js'
import { createAuthMintRoute, loadSigningKeyFromEnv } from './servers/auth-mint.js'

const DEFAULT_BIND_HOST = '0.0.0.0'
const DEFAULT_BIND_PORT = 443
const HERE = dirname(fileURLToPath(import.meta.url)) // <repo>/relay-run/src
const DEFAULT_WEB_ROOT = join(HERE, '..', '..', 'relay-web', 'public')

// ── env helpers (fail-fast on misconfiguration) ─────────────────────────────────────────────────

function requireEnv(name: string): string {
  const v = process.env[name]
  if (v === undefined || v.length === 0) {
    throw new KeyConfigError(`required env ${name} is not set`)
  }
  return v
}

function requirePort(name: string): number {
  const raw = requireEnv(name)
  const n = Number(raw)
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
    throw new KeyConfigError(`env ${name} must be an integer port 1–65535 (got ${JSON.stringify(raw)})`)
  }
  return n
}

function intEnv(name: string, fallback: number): number {
  const raw = process.env[name]
  if (raw === undefined || raw.length === 0) return fallback
  const n = Number(raw)
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
    throw new KeyConfigError(`env ${name} must be an integer port 1–65535 (got ${JSON.stringify(raw)})`)
  }
  return n
}

/**
 * F5/INV9 · render an error for logs WITHOUT leaking the raw object. A raw PG/Redis error can carry
 * the connection DSN (with password) in its properties, so we log only `.message` (+ `.code` when
 * present, e.g. 'ECONNREFUSED') and never the object itself.
 */
function errText(e: unknown): string {
  if (e instanceof Error) {
    const code = (e as { code?: unknown }).code
    return code === undefined ? e.message : `${e.message} (code=${String(code)})`
  }
  return String(e)
}

// ── async mTLS → sync-slot bridge ───────────────────────────────────────────────────────────────

interface MtlsBridge {
  /** Sync `MtlsVerifier` for the data plane; reads the pre-computed verdict for this connection. */
  readonly sync: MtlsVerifier
  /** Wrap the real TLS factory so each peer is registry-verified (async) BEFORE `attach` runs. */
  wrap(base: TlsServerFactory): TlsServerFactory
}

/**
 * term-relay's `MtlsVerifier.verifyPeer` is SYNC, but a registry-backed verifier (B2) is inherently
 * async (Postgres lookup) — the ASYNC IMPEDANCE flagged in mtls-verifier.ts. We bridge it WITHOUT
 * editing term-relay (outside our lane) by doing the async verify in the TLS `onPeer` hook and
 * caching the verdict keyed by the peer's DER, which the sync `verifyPeer` (called synchronously by
 * `attach`, immediately after `onPeer` fires) then reads. The set→onPeer→get sequence runs
 * synchronously inside one `.then` callback, so a single-slot cache per DER is race-free. Fail-closed
 * throughout: a rejected/failed verify caches `null`, so `attach` closes the peer with 4401 (INV14).
 */
function bridgeAsyncMtls(
  asyncMtls: AsyncMtlsVerifier,
  onError: (e: unknown) => void,
): MtlsBridge {
  const pending = new Map<string, { hostId: string; accountId: string } | null>()
  const keyOf = (der: Uint8Array): string => Buffer.from(der).toString('base64')

  const sync: MtlsVerifier = {
    verifyPeer(peerCert) {
      const k = keyOf(peerCert)
      const verdict = pending.get(k) ?? null
      pending.delete(k) // one-shot: consumed by the attach() that triggered this onPeer
      return verdict
    },
  }

  const wrap = (base: TlsServerFactory): TlsServerFactory => (opts, onPeer) =>
    base(opts, (ws, der) => {
      asyncMtls
        .verifyPeer(der)
        .then((verdict) => {
          pending.set(keyOf(der), verdict)
          onPeer(ws, der) // sync attach() → sync.verifyPeer(der) reads + consumes the verdict
        })
        .catch((e: unknown) => {
          onError(e)
          pending.set(keyOf(der), null) // fail-closed → attach() closes 4401
          onPeer(ws, der)
        })
    })

  return { sync, wrap }
}

// ── boot ────────────────────────────────────────────────────────────────────────────────────────

async function main(): Promise<void> {
  const now = (): number => Math.floor(Date.now() / 1000)

  // Config (fail-fast; secrets are read but never logged — INV9).
  const bindHost = process.env.BIND_HOST || DEFAULT_BIND_HOST
  const bindPort = intEnv('BIND_PORT', DEFAULT_BIND_PORT)
  const agentBindPort = requirePort('AGENT_BIND_PORT')
  const tlsCertPath = requireEnv('TLS_CERT_PATH')
  const tlsKeyPath = requireEnv('TLS_KEY_PATH')
  const agentServerCertPath = requireEnv('AGENT_SERVER_CERT_PATH')
  const agentServerKeyPath = requireEnv('AGENT_SERVER_KEY_PATH')
  const agentCaCertPath = requireEnv('AGENT_CA_CERT_PATH')
  const agentCaChainPath = requireEnv('AGENT_CA_CHAIN_PATH')
  const baseDomain = requireEnv('BASE_DOMAIN')
  const relayNodeId = requireEnv('RELAY_NODE_ID')
  const trustDomain = requireEnv('RELAY_TRUST_DOMAIN')
  const allowedOrigins = requireEnv('ALLOWED_ORIGINS')
    .split(',')
    .map((o) => o.trim())
    .filter((o) => o.length > 0)
  if (allowedOrigins.length === 0) {
    throw new KeyConfigError('ALLOWED_ORIGINS must contain at least one origin (CSWSH exact-match)')
  }
  const pgUrl = requireEnv('PG_URL')
  const redisUrl = requireEnv('REDIS_URL')
  const webRoot = process.env.WEB_ROOT || DEFAULT_WEB_ROOT

  // Shared P5 verify key (RELAY_AUTH_VERIFY_PUBKEY) — configured process-wide, never logged (INV9).
  await loadVerifyKeyFromEnv()

  // Shared stores: SAME Postgres + Redis as the control-plane (INV7).
  const pool = createPgPool(pgUrl)
  const query = createQuery(pool)
  const stores = createPgStores(query)
  const redis = createRedisClient(redisUrl)
  const redisSubscriber = createRedisClient(redisUrl) // dedicated subscriber-mode connection

  const deps = createRelayEnforceDeps({ query, redis })
  const resolver = createStoreRouteResolver({ hosts: stores.hosts })

  const asyncMtls = createMtlsVerifier({
    caChainPem: readFileSync(agentCaChainPath, 'utf8'),
    hosts: deps.hosts,
    now,
    onError: (e) => console.error('[mtls-verify]', errText(e)),
  })
  const mtlsBridge = bridgeAsyncMtls(asyncMtls, (e) => console.error('[mtls-bridge]', errText(e)))

  const authorizer = createAuthorizer({ deps, allowedOrigins, now })

  const config = makeDataPlaneConfig({ baseDomain, bindHost, bindPort, agentBindPort, relayNodeId })

  const agentTlsFactory = makeAgentTlsServerFactory({
    serverCertPath: agentServerCertPath,
    serverKeyPath: agentServerKeyPath,
    bindHost,
    bindPort: agentBindPort,
    onListening: () => console.log(`[agent-mtls] listening wss://${bindHost}:${agentBindPort}`),
    onError: (e) => console.error('[agent-mtls]', errText(e)),
  })

  const dp = buildDataPlane({
    config,
    authorizer,
    resolver,
    mtls: mtlsBridge.sync,
    now,
    // Agent-TLS trust store for validating the agent's CLIENT leaf (requestCert+rejectUnauthorized).
    // MUST be the FULL chain (intermediate + self-signed root): Node/OpenSSL rejects a leaf whose only
    // anchor is a non-self-signed intermediate (no PARTIAL_CHAIN flag). Intermediate-only ⇒ every agent
    // is reset at the TLS layer before attach() runs.
    caBundle: [readFileSync(agentCaChainPath)],
    onError: (e) => console.error('[data-plane]', errText(e)),
    tlsServerFactory: mtlsBridge.wrap(agentTlsFactory),
  })

  // STAGING operator token-mint (B5). Enabled only when BOTH the password gate and the signing key
  // are configured; otherwise the endpoint stays off (fail-closed) and static-only mode serves.
  const operatorPassword = process.env.OPERATOR_PASSWORD ?? ''
  const signPrivRaw = process.env.CAPABILITY_SIGN_PRIVKEY ?? ''
  let onRequest: ReturnType<typeof createAuthMintRoute> | undefined
  let mintEnabled = false
  if (operatorPassword.length > 0 && signPrivRaw.length > 0) {
    const signingKey = await loadSigningKeyFromEnv(signPrivRaw)
    // F1: the public :443 mint MUST be rate-limited (brute-force of OPERATOR_PASSWORD → full shell).
    // Reuse the shared Redis token bucket keyed on a salted hash of the client IP (raw IP never stored).
    const mintRateSalt = process.env.MINT_RATE_SALT || 'relay-run-mint-rate-salt'
    onRequest = createAuthMintRoute({
      signingKey,
      hosts: stores.hosts,
      operatorPassword,
      now,
      rateLimit: { buckets: deps.buckets, salt: mintRateSalt },
      onError: (e) => console.error('[auth-mint]', errText(e)),
    })
    mintEnabled = true
  } else {
    console.warn(
      '[auth-mint] STAGING mint disabled — set OPERATOR_PASSWORD and CAPABILITY_SIGN_PRIVKEY to enable POST /auth/mint',
    )
  }

  const browserServer = startBrowserServer({
    certPath: tlsCertPath,
    keyPath: tlsKeyPath,
    bindHost,
    bindPort,
    node: dp.node,
    landingHtml: '<!doctype html><title>relay</title>', // unused when staticRoot is set
    staticRoot: webRoot,
    ...(onRequest ? { onRequest } : {}),
    onListening: () => console.log(`[browser-wss] listening https://${bindHost}:${bindPort}`),
    onError: (e) => console.error('[browser-wss]', errText(e)),
  })

  // INV12: a Redis relay:revocations kill-signal tears matching live tunnel(s) down on this node.
  const revocableNode: RevocableNode = {
    activeTunnels: () =>
      [...dp.listener.tunnels().values()].map((t) => ({ hostId: t.hostId, accountId: t.accountId })),
    closeStream: (hostId) => dp.node.closeTunnel(hostId),
  }
  const revsub = startRevocationSubscriber({
    redisSubscriber,
    node: revocableNode,
    // INV10: log counts + scope KIND only — never signal.reason / terminal payload.
    onApplied: (signal, hostsAffected) =>
      console.log(`[revocation] applied scope=${signal.scope.kind} hostsAffected=${hostsAffected}`),
    onDropped: () => console.warn('[revocation] dropped malformed kill-signal'),
    onError: (e) => console.error('[revocation]', errText(e)),
  })

  console.log('\n=== relay-run Phase 1 READY ===')
  console.log(`Base domain     : ${baseDomain}   trustDomain: ${trustDomain}   node: ${relayNodeId}`)
  console.log(`Browser WSS     : https://${bindHost}:${bindPort}  (static root: ${webRoot})`)
  console.log(`Agent mTLS      : wss://${bindHost}:${agentBindPort}`)
  console.log(`Allowed origins : ${allowedOrigins.join(', ')}`)
  console.log(`Operator mint   : ${mintEnabled ? 'ENABLED (STAGING /auth/mint)' : 'disabled'}`)
  console.log('Ctrl-C to stop.\n')

  let shuttingDown = false
  const shutdown = async (): Promise<void> => {
    if (shuttingDown) return
    shuttingDown = true
    console.log('\nshutting down…')
    try {
      revsub.close()
      browserServer.close()
      dp.listener.close()
      await Promise.allSettled([redis.quit(), redisSubscriber.quit(), pool.end()])
    } catch (e) {
      console.error('[shutdown]', errText(e))
    } finally {
      process.exit(0)
    }
  }
  process.on('SIGINT', () => void shutdown())
  process.on('SIGTERM', () => void shutdown())
}

main().catch((e) => {
  console.error('fatal:', errText(e))
  process.exit(1)
})
