/**
 * Agent configuration — PLAN_RELAY_AGENT T2. Zod-validated at the startup boundary (INV9):
 *   - relayUrl  MUST be wss://    (encrypted tunnel only)
 *   - enrollUrl MUST be https://  (enrollment over TLS only)
 *   - localTargetUrl MUST be ws:// to a LOOPBACK host (anti-SSRF: the agent forwards ONLY to
 *     the local web-terminal, never an arbitrary target).
 * Fail-fast on missing/invalid values — never silently default a security-relevant field.
 */
import { homedir } from 'node:os'
import { join } from 'node:path'
import { z } from 'zod'
import { isLoopbackHostLiteral } from '../net/loopbackLiteral.js'

export interface AgentConfig {
  readonly relayUrl: string
  readonly enrollUrl: string
  readonly stateDir: string
  readonly localTargetUrl: string
  readonly subdomain: string | null
  readonly hostId: string | null
  /**
   * Renewal endpoint used ONLY when the current leaf has already expired (the strict `/renew` vhost
   * rejects an expired client cert before it reaches the control-plane). Optional: when unset it is
   * derived from `enrollUrl` by swapping the `enroll.` label for `recover.` — see
   * `certs/rotation.ts` `recoveryRenewalUrlFor`.
   */
  readonly recoverUrl?: string | null | undefined
}

/**
 * True iff `url` is ws:// to a loopback host (a well-formed 127.0.0.0/8 IPv4 literal, localhost, or
 * ::1). Uses the shared strict check so a crafted suffixed hostname such as
 * `ws://127.0.0.1.attacker.example.com:3000` — which the outbound dial would DNS-resolve and connect
 * to wherever it points — is REJECTED, closing the anti-SSRF bypass (not merely `startsWith('127.')`).
 */
export function isLoopbackWsUrl(url: string): boolean {
  let parsed: URL
  try {
    parsed = new URL(url)
  } catch {
    return false
  }
  if (parsed.protocol !== 'ws:') return false
  return isLoopbackHostLiteral(parsed.hostname)
}

function hasScheme(url: string, scheme: string): boolean {
  try {
    return new URL(url).protocol === scheme
  } catch {
    return false
  }
}

export const AgentConfigSchema = z
  .object({
    relayUrl: z
      .string()
      .refine((u) => hasScheme(u, 'wss:'), 'relayUrl must be a wss:// URL'),
    enrollUrl: z
      .string()
      .refine((u) => hasScheme(u, 'https:'), 'enrollUrl must be an https:// URL'),
    stateDir: z.string().min(1),
    localTargetUrl: z
      .string()
      .refine(isLoopbackWsUrl, 'localTargetUrl must be a ws:// loopback URL (anti-SSRF)'),
    subdomain: z.string().min(1).nullable(),
    hostId: z.string().min(1).nullable(),
    recoverUrl: z
      .string()
      .refine((u) => hasScheme(u, 'https:'), 'recoverUrl must be an https:// URL')
      .nullable()
      .optional(),
  })
  .strict()
  .readonly()

const DEFAULT_LOCAL_TARGET = 'ws://127.0.0.1:3000'

/** Default state dir where key/cert/content-secret live (~/.web-terminal-agent). */
export function defaultStateDir(): string {
  return join(homedir(), '.web-terminal-agent')
}

/**
 * Build + validate an AgentConfig from env + explicit argv overrides (argv wins). Throws a
 * ZodError (fail-fast) if any required field is missing or fails a scheme/loopback refinement.
 */
export function loadAgentConfig(
  env: NodeJS.ProcessEnv,
  argv: Partial<AgentConfig> = {},
): AgentConfig {
  const merged = {
    relayUrl: argv.relayUrl ?? env.RELAY_URL,
    enrollUrl: argv.enrollUrl ?? env.ENROLL_URL,
    stateDir: argv.stateDir ?? env.STATE_DIR ?? defaultStateDir(),
    localTargetUrl: argv.localTargetUrl ?? env.LOCAL_TARGET_URL ?? DEFAULT_LOCAL_TARGET,
    subdomain: argv.subdomain ?? env.SUBDOMAIN ?? null,
    hostId: argv.hostId ?? env.HOST_ID ?? null,
    recoverUrl: argv.recoverUrl ?? env.RECOVER_URL ?? null,
  }
  return AgentConfigSchema.parse(merged)
}
