/**
 * 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 { 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
  /** 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
}

/** 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`)

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'),
  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),
})

/**
 * 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)')
  }
  return {
    pgUrl: e.PG_URL,
    redisUrl: e.REDIS_URL,
    capabilitySignPubkey,
    caIntermediateKmsKeyRef: e.CA_INTERMEDIATE_KMS_KEY_REF,
    caIntermediateCertPath: e.CA_INTERMEDIATE_CERT_PATH,
    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,
  }
}
