/**
 * Boot configuration — zod-validated at the process boundary, FAIL-CLOSED.
 *
 * Every field is read from the environment ONCE at startup and returned as an immutable
 * `PanelConfig`. Structural problems (missing SESSION_SECRET, a non-numeric port, an empty
 * OPERATOR_ACCOUNT_ID) throw here so the server never boots half-configured.
 *
 * `PANEL_PASSWORD` is intentionally OPTIONAL at this layer: an unset password is a valid (if
 * useless) deployment state that the /login route turns into a fail-closed 503 — the panel must
 * still start so an operator can see the error, rather than crash-looping. Every other secret is
 * required. Secrets are NEVER logged; this module only ever returns them inside the config object.
 */
import { z } from 'zod'

/** Default control-plane admin API base (loopback — the CP admin surface must never be public). */
export const DEFAULT_CP_URL = 'http://127.0.0.1:8080'
/** Default PKCS#8 Ed25519 capability signing key (matches relay-run/mint-manage-token.ts). */
export const DEFAULT_CAPABILITY_SIGN_KEY_PATH = '/etc/relay/capability/capability-sign.key.pem'
/** Default tunnel zone used to render the `pair` command shown to the operator. */
export const DEFAULT_TUNNEL_ZONE = 'terminal.yaojia.wang'
/** Default loopback bind port for the panel HTTP server. */
export const DEFAULT_PANEL_BIND_PORT = 8090
/** SESSION_SECRET must have enough entropy to make cookie forgery infeasible. */
export const MIN_SESSION_SECRET_LEN = 16

/**
 * True iff `hostname` is a loopback literal: `localhost`, `::1`, or anything in 127.0.0.0/8.
 * The panel's CP admin client must ONLY ever reach the local control-plane — enforcing this at the
 * config boundary is anti-SSRF (a misconfigured CP_URL pointing at a remote/internal host is refused).
 */
export function isLoopbackHost(hostname: string): boolean {
  const h = hostname.replace(/^\[/, '').replace(/\]$/, '').toLowerCase() // strip IPv6 brackets
  if (h === 'localhost' || h === '::1') return true
  const m = /^(\d{1,3})\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.exec(h)
  return m !== null && Number(m[1]) === 127
}

/** True iff `url` parses AND its host is a loopback literal. Malformed URLs fail closed (false). */
function cpUrlHostIsLoopback(url: string): boolean {
  try {
    return isLoopbackHost(new URL(url).hostname)
  } catch {
    return false
  }
}

const EnvSchema = z.object({
  // Optional: unset ⇒ /login returns 503 (fail-closed at the route, not at boot).
  PANEL_PASSWORD: z.string().min(1).optional(),
  // Required: HMAC key that signs the session cookie. No default — a predictable secret is a bug.
  SESSION_SECRET: z.string().min(MIN_SESSION_SECRET_LEN, `SESSION_SECRET must be >= ${MIN_SESSION_SECRET_LEN} chars`),
  // Must be loopback (127.0.0.0/8, ::1, localhost): the CP admin surface is local-only. Fail closed otherwise.
  CP_URL: z
    .string()
    .url()
    .refine(cpUrlHostIsLoopback, 'CP_URL host must be loopback (127.0.0.0/8, ::1, or localhost)')
    .default(DEFAULT_CP_URL),
  // `aud` of every minted manage token — MUST equal the control-plane's expectedAud (its BASE_DOMAIN).
  BASE_DOMAIN: z.string().min(1),
  // `sub`/accountId scoped into every manage token and admin path.
  OPERATOR_ACCOUNT_ID: z.string().min(1),
  CAPABILITY_SIGN_KEY_PATH: z.string().min(1).default(DEFAULT_CAPABILITY_SIGN_KEY_PATH),
  TUNNEL_ZONE: z.string().min(1).default(DEFAULT_TUNNEL_ZONE),
  PANEL_BIND_PORT: z.coerce.number().int().min(1).max(65535).default(DEFAULT_PANEL_BIND_PORT),
})

export interface PanelConfig {
  readonly panelPassword: string | undefined
  readonly sessionSecret: string
  readonly cpUrl: string
  readonly baseDomain: string
  readonly operatorAccountId: string
  readonly capabilitySignKeyPath: string
  readonly tunnelZone: string
  readonly panelBindPort: number
}

/** Loopback-only bind host — the admin panel must never listen on a public interface. */
export const PANEL_BIND_HOST = '127.0.0.1'

/**
 * Parse + validate the environment into an immutable config. Throws a `ZodError` on any structural
 * problem (fail-closed). Never logs the values it reads.
 */
export function loadConfig(env: NodeJS.ProcessEnv): PanelConfig {
  const parsed = EnvSchema.parse(env)
  return {
    panelPassword: parsed.PANEL_PASSWORD,
    sessionSecret: parsed.SESSION_SECRET,
    cpUrl: parsed.CP_URL.replace(/\/+$/, ''), // normalise: strip trailing slashes for clean joins
    baseDomain: parsed.BASE_DOMAIN,
    operatorAccountId: parsed.OPERATOR_ACCOUNT_ID,
    capabilitySignKeyPath: parsed.CAPABILITY_SIGN_KEY_PATH,
    tunnelZone: parsed.TUNNEL_ZONE,
    panelBindPort: parsed.PANEL_BIND_PORT,
  }
}
