import { z } from 'zod';

// BooCoder's config is a superset of the server's Config type so it can be
// passed directly into the inference runner's InferenceContext. Fields the
// inference loop reads: LLAMA_SWAP_URL, PROJECT_ROOT_WHITELIST. The rest
// default to values that satisfy the server's Zod schema without BooCoder
// needing to supply them in its environment.
const ConfigSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().int().positive().default(3000),
  HOST: z.string().default('0.0.0.0'),
  DATABASE_URL: z.string().url(),
  LLAMA_SWAP_URL: z.string().url(),
  PROJECT_ROOT_WHITELIST: z.string().default('/opt'),
  BOOTSTRAP_ROOT: z.string().default('/opt/projects'),
  DEFAULT_MODEL: z.string().default('qwen3.6-35b-a3b-mxfp4'),
  LOG_LEVEL: z.string().default('info'),
  CONTAINER_GUIDANCE_FILE: z.string().optional(),
  // Fields needed to satisfy the server's Config type but unused by BooCoder:
  SEARXNG_URL: z.string().url().default('http://100.114.205.53:8888'),
  GITEA_BASE_URL: z.string().url().default('https://git.indifferentketchup.com'),
  GITEA_USER: z.string().default('indifferentketchup'),
  GITEA_TOKEN: z.string().optional(),
  GITEA_SSH_HOST: z.string().default('100.114.205.53:2222'),
  MCP_CONFIG_PATH: z.string().optional(),
  // v2.3: config-backed provider overrides/custom-ACP entries merged over the
  // hardcoded built-ins. Missing file = built-ins only (see provider-config.ts).
  CODER_PROVIDERS_PATH: z.string().default('/data/coder-providers.json'),
  // v2.3 phase 2: tier-2 (cold ACP probe) is skipped when available_agents was
  // probed more recently than this. 24h default — stale model lists self-heal
  // on the next snapshot; an explicit /refresh always re-probes.
  PROVIDER_PROBE_TTL_MS: z.coerce.number().int().positive().default(86_400_000),
  // v2.0.5: cheaper model for titles, summaries, labeling.
  FAST_MODEL: z.string().optional(),
  // SSH access to the host for external agent dispatch (Phase 5)
  BOOCODER_SSH_HOST: z.string().default('100.114.205.53'),
  BOOCODER_SSH_USER: z.string().default('samkintop'),
  // v2.6 Phase 3 (lifecycle hardening). Idle TTL: evict a non-busy warm backend
  // (opencode server / warm-ACP child) after this long with no turn — its worktree
  // + agent_sessions row persist, so the next turn re-spawns + reattaches. 30 min
  // default (design §6).
  AGENT_POOL_IDLE_TTL_MS: z.coerce.number().int().positive().default(1_800_000),
  // LRU cap: max live warm backends before the least-recently-used (non-busy) ones
  // are evicted. Bounds the long-lived-daemon's per-(chat,agent) Map growth.
  AGENT_POOL_MAX_LIVE: z.coerce.number().int().positive().default(10),
  // Periodic sweep cadence (idle/LRU pool eviction + orphan-worktree reap). 60s
  // mirrors the apps/server truncation/stale-streaming sweeper.
  LIFECYCLE_SWEEP_INTERVAL_MS: z.coerce.number().int().positive().default(60_000),
  // Orphan-worktree grace: an on-disk worktree dir with no live `worktrees` row is
  // only reaped after it's been untouched this long (avoids sweeping a dir mid
  // ensureSessionWorktree create). 1h default.
  ORPHAN_WORKTREE_GRACE_MS: z.coerce.number().int().positive().default(3_600_000),
  DEEPSEEK_API_KEY: z.string().optional(),
  DEEPSEEK_BASE_URL: z.string().url().default('https://api.deepseek.com'),
  // v2.9.x: flow step timeout (default 5 min). When a 'running' step exceeds
  // this duration, it is marked 'timed_out' and may be retried.
  FLOW_STEP_TIMEOUT_MS: z.coerce.number().int().positive().default(300_000),
  // vMultiProvider: path to the local providers config JSON file. Missing file
  // = legacy synthesis from LLAMA_SWAP_URL.
  LLAMA_PROVIDERS_PATH: z.string().optional(),
});

export type Config = z.infer<typeof ConfigSchema>;

let cached: Config | null = null;

export function loadConfig(): Config {
  if (cached) return cached;
  const parsed = ConfigSchema.safeParse(process.env);
  if (!parsed.success) {
    console.error('Invalid environment configuration:');
    console.error(parsed.error.flatten().fieldErrors);
    process.exit(1);
  }
  cached = parsed.data;
  return cached;
}
