/**
 * T1 — startup config/secret validation (INV9): secrets come from env/secret-manager,
 * are Zod-validated at startup, fail fast, and are NEVER logged (only key NAMES on failure).
 *
 * The CA intermediate signing key is referenced by a KMS/HSM ref only — never inlined,
 * never loaded raw into process memory (§3.1). The KMS key-policy check runs in
 * `boot/ca-wiring.ts` (T15 `buildCaSigner`) with an injected resolver, because it needs a
 * live KMS call; `loadEnv` validates only the presence/shape of the ref here.
 */
import { z } from 'zod'
import { dirname, join } from 'node:path'
import { base64ToBytes } from './util/bytes.js'

/** Pairing-code TTL default: 10 minutes (§5 T7). */
export const DEFAULT_PAIRING_TTL_SEC = 600
/** Per-code redemption lockout threshold default (§5 T7/T8). */
export const DEFAULT_PAIRING_MAX_REDEEM_ATTEMPTS = 5

export interface ControlPlaneEnv {
  readonly pgUrl: string
  readonly redisUrl: string
  /** P5 signing PUBLIC key, used to VERIFY admin capability tokens (INV3). */
  readonly capabilitySignPubkey: Uint8Array
  /** KMS/HSM key REF for the non-exportable intermediate leaf-signing key (§3.1). */
  readonly caIntermediateKmsKeyRef: string
  /** Intermediate cert (public) + chain up to the offline root. */
  readonly caIntermediateCertPath: string
  /**
   * Intermediate Ed25519 PRIVATE key (PKCS#8 PEM) used by the REAL leaf issuer. Optional: when the
   * file is present the control-plane issues real X.509 leaves; when absent it falls back to the
   * dev placeholder signer. Defaults to `caIntermediateCertPath` with `.cert.pem`/`.pem` → `.key.pem`.
   */
  readonly caIntermediateKeyPath: string
  /** Self-signed root cert (PEM) for the agent CA bundle. Defaults to a sibling `root.cert.pem`. */
  readonly caRootCertPath: string
  /** Bare trust domain for the SPIFFE SAN (`spiffe://relay.<domain>/...`). */
  readonly relayTrustDomain: string
  /** CA bundle used to VERIFY relay-node mTLS client certs (T9 node-auth). */
  readonly nodeMtlsTrustBundlePath: string
  /** 'term.<domain>' for subdomain assembly. */
  readonly baseDomain: string
  readonly heartbeatTtlSec: number
  readonly pairingTtlSec: number
  readonly pairingMaxRedeemAttempts: number
  /**
   * B1 operator-login seam (single-tenant MVP). ALL THREE are set together or NONE — a half-configured
   * login is a boot error (fail-fast). Unset ⇒ the `/auth/login` route is fail-closed (503). When set:
   *   - `operatorPassword` — the operator login secret (mirrors WEBTERM_TOKEN / relay OPERATOR_PASSWORD;
   *     16–512 URL/cookie-safe chars). Constant-time compared, NEVER logged (INV9).
   *   - `operatorAccountId` — the account the minted `device:enroll` bearer is scoped to (`sub`).
   *   - `capabilitySignPrivkey` — Ed25519 PKCS#8 DER private key whose PUBLIC half is
   *     `capabilitySignPubkey`; used ONLY to sign the enroll bearer so it verifies on the same §4.3 path.
   */
  readonly operatorPassword?: string
  readonly operatorAccountId?: string
  readonly capabilitySignPrivkey?: Uint8Array
  /**
   * A2-prep — the native-tunnel PKI's on-disk P-256 CA material (the VPS `gen-device-ca.sh` output:
   * `frp-client-CA` for host frp-client leaves + `device-CA` for device leaves). Set together or NOT
   * at all — a partial set is a boot error (fail-closed). Undefined ⇒ the dev/test injection path
   * (self-signed in-process CAs) is used. The private KEY paths mirror `CA_INTERMEDIATE_KEY_PATH`
   * (optional; derived from the cert path when unset). The raw keys are custody-on-disk (single-owner
   * file-custody reality; a non-exportable KMS is the future upgrade).
   */
  readonly nativeCa?: NativeCaEnvConfig
}

/** Resolved on-disk material for the two native-tunnel P-256 CAs + the DNS zone their leaves live under. */
export interface NativeCaEnvConfig {
  /** frp-client-CA (public) cert PEM path — the host-leaf trust anchor. */
  readonly frpClientCaCertPath: string
  /** frp-client-CA PKCS#8 PEM private key path (derived from the cert path when unset). */
  readonly frpClientCaKeyPath: string
  /** device-CA (public) cert PEM path — the device-leaf trust anchor. */
  readonly deviceCaCertPath: string
  /** device-CA PKCS#8 PEM private key path (derived from the cert path when unset). */
  readonly deviceCaKeyPath: string
  /** DNS zone the leaves' `dNSName <sub>.<zone>` SAN is stamped under (the nginx :8470 tenant key). */
  readonly dnsZone: string
}

/** A positive integer parsed from an env string, or a default when unset/empty. */
const intWithDefault = (fallback: number) =>
  z
    .string()
    .trim()
    .optional()
    .transform((v) => (v === undefined || v === '' ? String(fallback) : v))
    .pipe(z.coerce.number().int().positive())

const requiredString = (name: string) =>
  z.string({ required_error: `${name} is required` }).trim().min(1, `${name} must not be empty`)

/** Default intermediate KEY path from its CERT path: `*.cert.pem`/`*.pem` → `*.key.pem`. */
function deriveKeyPath(certPath: string): string {
  if (certPath.endsWith('.cert.pem')) return certPath.slice(0, -'.cert.pem'.length) + '.key.pem'
  if (certPath.endsWith('.pem')) return certPath.slice(0, -'.pem'.length) + '.key.pem'
  return certPath + '.key.pem'
}

const EnvSchema = z.object({
  PG_URL: requiredString('PG_URL').url('PG_URL must be a valid connection URL'),
  REDIS_URL: requiredString('REDIS_URL').url('REDIS_URL must be a valid connection URL'),
  CAPABILITY_SIGN_PUBKEY_B64: requiredString('CAPABILITY_SIGN_PUBKEY_B64'),
  CA_INTERMEDIATE_KMS_KEY_REF: requiredString('CA_INTERMEDIATE_KMS_KEY_REF'),
  CA_INTERMEDIATE_CERT_PATH: requiredString('CA_INTERMEDIATE_CERT_PATH'),
  CA_INTERMEDIATE_KEY_PATH: z.string().trim().optional(),
  CA_ROOT_CERT_PATH: z.string().trim().optional(),
  RELAY_TRUST_DOMAIN: z.string().trim().optional(),
  NODE_MTLS_TRUST_BUNDLE_PATH: requiredString('NODE_MTLS_TRUST_BUNDLE_PATH'),
  BASE_DOMAIN: requiredString('BASE_DOMAIN'),
  HEARTBEAT_TTL_SEC: intWithDefault(15),
  PAIRING_TTL_SEC: intWithDefault(DEFAULT_PAIRING_TTL_SEC),
  PAIRING_MAX_REDEEM_ATTEMPTS: intWithDefault(DEFAULT_PAIRING_MAX_REDEEM_ATTEMPTS),
  // B1 operator-login seam — all optional; cross-validated below (set together or not at all).
  OPERATOR_PASSWORD: z
    .string()
    .trim()
    .regex(/^[A-Za-z0-9._~+/=-]{16,512}$/, 'OPERATOR_PASSWORD must be 16–512 URL/cookie-safe chars')
    .optional(),
  OPERATOR_ACCOUNT_ID: z.string().trim().uuid('OPERATOR_ACCOUNT_ID must be a UUID').optional(),
  CAPABILITY_SIGN_PRIVKEY_B64: z.string().trim().min(1).optional(),
  // A2-prep native-tunnel CA material — all optional; cross-validated below (set together or not at all).
  NATIVE_FRP_CLIENT_CA_CERT_PATH: z.string().trim().optional(),
  NATIVE_FRP_CLIENT_CA_KEY_PATH: z.string().trim().optional(),
  NATIVE_DEVICE_CA_CERT_PATH: z.string().trim().optional(),
  NATIVE_DEVICE_CA_KEY_PATH: z.string().trim().optional(),
  NATIVE_DNS_ZONE: z.string().trim().optional(),
})

/**
 * Resolve the optional operator-login triplet. Deny-by-default + fail-fast: if the operator password
 * is set, the account id and the Ed25519 signing key MUST also be set (a half-configured login is a
 * boot error, never a silent half-open). Returns `{}` when the feature is unconfigured. NEVER echoes
 * secret VALUES — only key names on failure (INV9).
 */
function resolveOperatorLogin(e: {
  OPERATOR_PASSWORD: string | undefined
  OPERATOR_ACCOUNT_ID: string | undefined
  CAPABILITY_SIGN_PRIVKEY_B64: string | undefined
}): { operatorPassword?: string; operatorAccountId?: string; capabilitySignPrivkey?: Uint8Array } {
  const has = (v: string | undefined): v is string => v !== undefined && v.length > 0
  const pw = e.OPERATOR_PASSWORD
  const acct = e.OPERATOR_ACCOUNT_ID
  const priv = e.CAPABILITY_SIGN_PRIVKEY_B64
  if (!has(pw) && !has(acct) && !has(priv)) {
    return {} // feature off — /auth/login is fail-closed at runtime
  }
  if (!has(pw) || !has(acct) || !has(priv)) {
    const missing = [
      has(pw) ? null : 'OPERATOR_PASSWORD',
      has(acct) ? null : 'OPERATOR_ACCOUNT_ID',
      has(priv) ? null : 'CAPABILITY_SIGN_PRIVKEY_B64',
    ].filter((x): x is string => x !== null)
    throw new Error(`Invalid control-plane env: operator login requires all of ${missing.join(', ')} to be set`)
  }
  let capabilitySignPrivkey: Uint8Array
  try {
    capabilitySignPrivkey = base64ToBytes(priv)
  } catch {
    throw new Error('Invalid control-plane env: CAPABILITY_SIGN_PRIVKEY_B64 is not valid base64')
  }
  return { operatorPassword: pw, operatorAccountId: acct, capabilitySignPrivkey }
}

/**
 * Resolve the optional native-tunnel CA material. Deny-by-default + fail-fast: the two (public) CA
 * cert paths and the DNS zone are the mandatory triad; if ANY native field is supplied (including a
 * lone KEY_PATH), all three MUST be present or boot fails (a partial set is never a silent half-open,
 * mirroring `resolveOperatorLogin`). The private KEY paths are optional and derived from their cert
 * path when unset (mirrors `CA_INTERMEDIATE_KEY_PATH`). Returns `{}` when the feature is unconfigured.
 * NEVER echoes path VALUES — only field NAMES on failure (INV9).
 */
function resolveNativeCa(e: {
  NATIVE_FRP_CLIENT_CA_CERT_PATH: string | undefined
  NATIVE_FRP_CLIENT_CA_KEY_PATH: string | undefined
  NATIVE_DEVICE_CA_CERT_PATH: string | undefined
  NATIVE_DEVICE_CA_KEY_PATH: string | undefined
  NATIVE_DNS_ZONE: string | undefined
}): { nativeCa?: NativeCaEnvConfig } {
  const has = (v: string | undefined): v is string => v !== undefined && v.length > 0
  const frpCert = e.NATIVE_FRP_CLIENT_CA_CERT_PATH
  const frpKey = e.NATIVE_FRP_CLIENT_CA_KEY_PATH
  const devCert = e.NATIVE_DEVICE_CA_CERT_PATH
  const devKey = e.NATIVE_DEVICE_CA_KEY_PATH
  const zone = e.NATIVE_DNS_ZONE
  const anyPresent = [frpCert, frpKey, devCert, devKey, zone].some(has)
  if (!anyPresent) return {} // feature off — dev/test injection path (self-signed in-process CAs)
  if (!has(frpCert) || !has(devCert) || !has(zone)) {
    const missing = [
      has(frpCert) ? null : 'NATIVE_FRP_CLIENT_CA_CERT_PATH',
      has(devCert) ? null : 'NATIVE_DEVICE_CA_CERT_PATH',
      has(zone) ? null : 'NATIVE_DNS_ZONE',
    ].filter((x): x is string => x !== null)
    throw new Error(
      `Invalid control-plane env: native-tunnel CA material requires all of ${missing.join(', ')} to be set`,
    )
  }
  return {
    nativeCa: {
      frpClientCaCertPath: frpCert,
      frpClientCaKeyPath: has(frpKey) ? frpKey : deriveKeyPath(frpCert),
      deviceCaCertPath: devCert,
      deviceCaKeyPath: has(devKey) ? devKey : deriveKeyPath(devCert),
      dnsZone: zone,
    },
  }
}

/**
 * Parse + validate control-plane config. THROWS (fail-fast) listing every missing/invalid
 * key by NAME. Never echoes secret VALUES (INV9).
 */
export function loadEnv(source: NodeJS.ProcessEnv): ControlPlaneEnv {
  const parsed = EnvSchema.safeParse(source)
  if (!parsed.success) {
    const problems = parsed.error.issues
      .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
      .join('; ')
    // Only key names + messages — never values (INV9).
    throw new Error(`Invalid control-plane env: ${problems}`)
  }
  const e = parsed.data
  let capabilitySignPubkey: Uint8Array
  try {
    capabilitySignPubkey = base64ToBytes(e.CAPABILITY_SIGN_PUBKEY_B64)
  } catch {
    throw new Error('Invalid control-plane env: CAPABILITY_SIGN_PUBKEY_B64 is not valid base64')
  }
  if (capabilitySignPubkey.length !== 32) {
    throw new Error('Invalid control-plane env: CAPABILITY_SIGN_PUBKEY_B64 must decode to 32 bytes (Ed25519)')
  }
  const caIntermediateKeyPath =
    e.CA_INTERMEDIATE_KEY_PATH !== undefined && e.CA_INTERMEDIATE_KEY_PATH.length > 0
      ? e.CA_INTERMEDIATE_KEY_PATH
      : deriveKeyPath(e.CA_INTERMEDIATE_CERT_PATH)
  const caRootCertPath =
    e.CA_ROOT_CERT_PATH !== undefined && e.CA_ROOT_CERT_PATH.length > 0
      ? e.CA_ROOT_CERT_PATH
      : join(dirname(e.CA_INTERMEDIATE_CERT_PATH), 'root.cert.pem')
  return {
    pgUrl: e.PG_URL,
    redisUrl: e.REDIS_URL,
    capabilitySignPubkey,
    caIntermediateKmsKeyRef: e.CA_INTERMEDIATE_KMS_KEY_REF,
    caIntermediateCertPath: e.CA_INTERMEDIATE_CERT_PATH,
    caIntermediateKeyPath,
    caRootCertPath,
    relayTrustDomain:
      e.RELAY_TRUST_DOMAIN !== undefined && e.RELAY_TRUST_DOMAIN.length > 0 ? e.RELAY_TRUST_DOMAIN : 'example.com',
    nodeMtlsTrustBundlePath: e.NODE_MTLS_TRUST_BUNDLE_PATH,
    baseDomain: e.BASE_DOMAIN,
    heartbeatTtlSec: e.HEARTBEAT_TTL_SEC,
    pairingTtlSec: e.PAIRING_TTL_SEC,
    pairingMaxRedeemAttempts: e.PAIRING_MAX_REDEEM_ATTEMPTS,
    ...resolveOperatorLogin({
      OPERATOR_PASSWORD: e.OPERATOR_PASSWORD,
      OPERATOR_ACCOUNT_ID: e.OPERATOR_ACCOUNT_ID,
      CAPABILITY_SIGN_PRIVKEY_B64: e.CAPABILITY_SIGN_PRIVKEY_B64,
    }),
    ...resolveNativeCa({
      NATIVE_FRP_CLIENT_CA_CERT_PATH: e.NATIVE_FRP_CLIENT_CA_CERT_PATH,
      NATIVE_FRP_CLIENT_CA_KEY_PATH: e.NATIVE_FRP_CLIENT_CA_KEY_PATH,
      NATIVE_DEVICE_CA_CERT_PATH: e.NATIVE_DEVICE_CA_CERT_PATH,
      NATIVE_DEVICE_CA_KEY_PATH: e.NATIVE_DEVICE_CA_KEY_PATH,
      NATIVE_DNS_ZONE: e.NATIVE_DNS_ZONE,
    }),
  }
}
