/**
 * Centralized configuration module.
 *
 * Reads all environment variables once at import time, validates them with Zod,
 * and exports a typed, frozen config object. Invalid config causes a hard
 * failure with a clear error message so misconfigurations surface immediately.
 *
 * Usage:
 *   import { config } from "./config.js";
 *   console.log(config.port);
 *
 * In tests, call `_resetConfigForTesting()` after modifying process.env to
 * re-parse and re-validate the environment.
 */

import { existsSync, mkdirSync, readdirSync, copyFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { z } from "zod";
import { DEFAULT_PORT_DEV, DEFAULT_PORT_PROD } from "./constants.js";

/**
 * Typed error thrown when environment configuration fails Zod validation.
 * Callers can catch this specifically to distinguish config errors from other failures.
 */
export class ConfigValidationError extends Error {
  override readonly name = "ConfigValidationError";
  readonly issues: z.ZodIssue[];

  constructor(message: string, issues: z.ZodIssue[]) {
    super(message);
    this.issues = issues;
  }
}

// ── Legacy env var migration ────────────────────────────────────────────────

/** Map of legacy COMPANION_* env var names → new ZENCODE_* names. */
const LEGACY_ENV_MAP: ReadonlyArray<[legacy: string, current: string]> = [
  ["COMPANION_NO_AUTH", "ZENCODE_NO_AUTH"],
  ["COMPANION_AUTH_TOKEN", "ZENCODE_AUTH_TOKEN"],
  ["COMPANION_UNSAFE_NO_AUTH", "ZENCODE_UNSAFE_NO_AUTH"],
  ["COMPANION_LOG_LEVEL", "ZENCODE_LOG_LEVEL"],
  ["COMPANION_RATE_LIMIT", "ZENCODE_RATE_LIMIT"],
  ["COMPANION_RATE_WINDOW_MS", "ZENCODE_RATE_WINDOW_MS"],
  ["COMPANION_TRUST_PROXY", "ZENCODE_TRUST_PROXY"],
  ["COMPANION_RECORDINGS_DIR", "ZENCODE_RECORDINGS_DIR"],
  ["COMPANION_RECORDINGS_MAX_LINES", "ZENCODE_RECORDINGS_MAX_LINES"],
  ["COMPANION_RECORD", "ZENCODE_RECORD"],
  ["COMPANION_IDLE_TIMEOUT_SECONDS", "ZENCODE_IDLE_TIMEOUT_SECONDS"],
  ["COMPANION_RECONNECT_GRACE_MS", "ZENCODE_RECONNECT_GRACE_MS"],
  ["COMPANION_SESSION_DIR", "ZENCODE_SESSION_DIR"],
  ["COMPANION_CORS_ORIGINS", "ZENCODE_CORS_ORIGINS"],
  ["COMPANION_CONTAINER_SDK_HOST", "ZENCODE_CONTAINER_SDK_HOST"],
  ["COMPANION_FORCE_BYPASS_IN_CONTAINER", "ZENCODE_FORCE_BYPASS_IN_CONTAINER"],
  ["COMPANION_ALLOWED_PATHS", "ZENCODE_ALLOWED_PATHS"],
  ["COMPANION_EDITOR_PORT", "ZENCODE_EDITOR_PORT"],
  ["COMPANION_INIT_SCRIPT_TIMEOUT", "ZENCODE_INIT_SCRIPT_TIMEOUT"],
  ["__COMPANION_PACKAGE_ROOT", "__ZENCODE_PACKAGE_ROOT"],
  ["COMPANION_PORT", "ZENCODE_PORT"],
  ["COMPANION_HOST", "ZENCODE_HOST"],
  ["COMPANION_CLI_PATH", "ZENCODE_CLI_PATH"],
  ["COMPANION_CODEX_CLI_PATH", "ZENCODE_CODEX_CLI_PATH"],
  ["COMPANION_AIDER_CLI_PATH", "ZENCODE_AIDER_CLI_PATH"],
  ["COMPANION_NO_OPEN", "ZENCODE_NO_OPEN"],
  ["COMPANION_CODEX_HOME", "ZENCODE_CODEX_HOME"],
  ["COMPANION_LINEAR_API_KEY", "ZENCODE_LINEAR_API_KEY"],
  ["COMPANION_AUTO_NAME", "ZENCODE_AUTO_NAME"],
  ["COMPANION_RECORDING_DIR", "ZENCODE_RECORDING_DIR"],
  ["COMPANION_CONTAINER_IMAGE", "ZENCODE_CONTAINER_IMAGE"],
];

const _warnedLegacyVars = new Set<string>();

/**
 * Migrate legacy `COMPANION_*` env vars to `ZENCODE_*` equivalents.
 * If the new name is not already set, copies the legacy value and emits
 * a one-time deprecation warning.
 */
function migrateEnvVars(env: Record<string, string | undefined>): void {
  for (const [legacy, current] of LEGACY_ENV_MAP) {
    if (env[current] === undefined && env[legacy] !== undefined) {
      env[current] = env[legacy];
      if (!_warnedLegacyVars.has(legacy)) {
        _warnedLegacyVars.add(legacy);
        console.warn(`[zencode] ${legacy} is deprecated. Use ${current} instead.`);
      }
    }
  }
}

// ── Config directory migration ──────────────────────────────────────────────

/** Path to the new config directory. */
export const ZENCODE_DIR = join(homedir(), ".zencode");
/** Path to the legacy config directory (kept for migration). */
const LEGACY_COMPANION_DIR = join(homedir(), ".companion");

let _configDirMigrated = false;

/**
 * Migrate config files from `~/.companion/` to `~/.zencode/` if the old
 * directory exists and the new one does not. Only runs once per process.
 */
function migrateConfigDir(): void {
  if (_configDirMigrated) return;
  _configDirMigrated = true;

  if (existsSync(LEGACY_COMPANION_DIR) && !existsSync(ZENCODE_DIR)) {
    try {
      mkdirSync(ZENCODE_DIR, { recursive: true, mode: 0o700 });
      for (const entry of readdirSync(LEGACY_COMPANION_DIR)) {
        const oldPath = join(LEGACY_COMPANION_DIR, entry);
        const newPath = join(ZENCODE_DIR, entry);
        try {
          const stat = statSync(oldPath);
          if (stat.isFile()) {
            copyFileSync(oldPath, newPath);
          }
        } catch { /* expected: some entries may not be copyable (permissions, symlinks) */ }
      }
      console.warn(
        `[zencode] Migrated config from ~/.companion/ to ~/.zencode/\n` +
        `  You can safely delete ~/.companion/ after verifying.\n`,
      );
    } catch {
      console.warn(`[zencode] Could not migrate ~/.companion/ to ~/.zencode/ — please copy manually.`);
    }
  }
}

// ── Zod helpers ─────────────────────────────────────────────────────────────

/** Coerce an optional env string to a finite number, or `undefined` if absent. */
const optionalNumber = z
  .string()
  .optional()
  .transform((v) => {
    if (v === undefined || v === "") return undefined;
    const n = Number(v);
    return Number.isFinite(n) ? n : undefined;
  });

/** Coerce an env string to a boolean flag ("1" → true, "0"/"false" → false). */
const booleanFlag = (defaultValue: boolean) =>
  z
    .string()
    .optional()
    .transform((v) => {
      if (v === undefined || v === "") return defaultValue;
      return v !== "0" && v !== "false";
    });

/** Split a comma-separated env string into a trimmed, non-empty string[]. */
const commaList = z
  .string()
  .optional()
  .transform((v) =>
    v
      ? v
          .split(",")
          .map((s) => s.trim())
          .filter(Boolean)
      : [],
  );

// ── Schema ──────────────────────────────────────────────────────────────────

const envSchema = z.object({
  // Server
  PORT: optionalNumber,
  HOST: z.string().optional().default("localhost"),
  NODE_ENV: z
    .enum(["development", "production", "test"])
    .optional()
    .default("development"),

  // Auth
  ZENCODE_NO_AUTH: booleanFlag(false),
  ZENCODE_AUTH_TOKEN: z.string().optional(),
  ZENCODE_UNSAFE_NO_AUTH: booleanFlag(false),

  // Logging
  ZENCODE_LOG_LEVEL: z.string().optional().default("info"),

  // Rate limiting
  ZENCODE_RATE_LIMIT: optionalNumber,
  ZENCODE_RATE_WINDOW_MS: optionalNumber,
  ZENCODE_TRUST_PROXY: booleanFlag(false),

  // Recordings
  ZENCODE_RECORDINGS_DIR: z.string().optional(),
  ZENCODE_RECORDINGS_MAX_LINES: optionalNumber,
  ZENCODE_RECORD: booleanFlag(true),

  // Timeouts
  ZENCODE_IDLE_TIMEOUT_SECONDS: optionalNumber,
  ZENCODE_RECONNECT_GRACE_MS: optionalNumber,

  // Session storage
  ZENCODE_SESSION_DIR: z.string().optional(),

  // CORS
  ZENCODE_CORS_ORIGINS: commaList,

  // Container
  ZENCODE_CONTAINER_SDK_HOST: z.string().optional().default("host.docker.internal"),
  ZENCODE_FORCE_BYPASS_IN_CONTAINER: booleanFlag(false),

  // Filesystem access
  ZENCODE_ALLOWED_PATHS: commaList,

  // Editor
  ZENCODE_EDITOR_PORT: optionalNumber,

  // Init script
  ZENCODE_INIT_SCRIPT_TIMEOUT: optionalNumber,

  // Internal — set by the CLI entry point
  __ZENCODE_PACKAGE_ROOT: z.string().optional(),

  // Claude projects directory override
  CLAUDE_PROJECTS_DIR: z.string().optional(),
});

type RawEnvConfig = z.infer<typeof envSchema>;

// ── Typed config interface ──────────────────────────────────────────────────

export interface AppConfig {
  /** Server port. Defaults to 3457 (dev) or 3456 (production). */
  port: number;
  /** Server hostname. Default: "localhost" */
  host: string;
  /** Node environment. Default: "development" */
  nodeEnv: "development" | "production" | "test";
  /** Whether the environment is production. */
  isProduction: boolean;

  /** Disable authentication entirely. Default: false */
  noAuth: boolean;
  /** Explicitly allow --no-auth on non-localhost hosts. Default: false */
  unsafeNoAuth: boolean;
  /** Pre-configured auth token (env). May be absent if file-backed. */
  authToken: string | undefined;

  /** Pino log level. Default: "info" */
  logLevel: string;

  /** Max requests per window for rate limiting. Default: 100 */
  rateLimit: number;
  /** Rate limit window in milliseconds. Default: 15 000 */
  rateWindowMs: number;
  /** Trust X-Forwarded-For header for client IP resolution. Default: false */
  trustProxy: boolean;

  /** Directory for protocol recordings. Default: ~/.zencode/recordings */
  recordingsDir: string | undefined;
  /** Max lines before recording rotation. Default: 100 000 */
  recordingsMaxLines: number | undefined;
  /** Whether recording is enabled. Default: true */
  recordEnabled: boolean;

  /** WebSocket idle timeout in seconds. Default: 120 */
  idleTimeoutSeconds: number;
  /** Grace period for CLI reconnection in ms. Default: 30 000 */
  reconnectGraceMs: number;

  /** Custom session storage directory. */
  sessionDir: string | undefined;

  /** Extra CORS origins (parsed from comma-separated env). */
  corsOrigins: string[];

  /** Docker SDK host alias. Default: "host.docker.internal" */
  containerSdkHost: string;
  /** Force bypassPermissions inside containers. Default: false */
  forceBypassInContainer: boolean;

  /** Additional allowed filesystem paths (parsed from comma-separated env). */
  allowedPaths: string[];

  /** VS Code editor host port. Default: 13 338 */
  editorPort: number;

  /** Init script timeout in ms. Default: 120 000 */
  initScriptTimeout: number;

  /** Package root set by the CLI entry point. */
  packageRoot: string | undefined;

  /** Override for Claude projects directory. */
  claudeProjectsDir: string | undefined;
}

// ── Defaults ────────────────────────────────────────────────────────────────

const DEFAULT_RATE_LIMIT = 100;
const DEFAULT_RATE_WINDOW_MS = 15_000;
const DEFAULT_IDLE_TIMEOUT = 120;
const DEFAULT_RECONNECT_GRACE_MS = 30_000;
const DEFAULT_EDITOR_PORT = 13_338;
const DEFAULT_INIT_SCRIPT_TIMEOUT = 120_000;

// ── Parser ──────────────────────────────────────────────────────────────────

function parseConfig(env: Record<string, string | undefined>): AppConfig {
  // Migrate legacy COMPANION_* env vars before parsing
  migrateEnvVars(env);
  // Migrate config directory from ~/.companion/ to ~/.zencode/
  migrateConfigDir();

  const result = envSchema.safeParse(env);

  if (!result.success) {
    const formatted = result.error.issues
      .map((issue) => `  ${issue.path.join(".")}: ${issue.message}`)
      .join("\n");
    const message = `Invalid environment configuration:\n${formatted}`;
    throw new ConfigValidationError(message, result.error.issues);
  }

  const raw: RawEnvConfig = result.data;

  const nodeEnv = raw.NODE_ENV;
  const defaultPort =
    nodeEnv === "production" ? DEFAULT_PORT_PROD : DEFAULT_PORT_DEV;

  return Object.freeze({
    port: raw.PORT ?? defaultPort,
    host: raw.HOST,
    nodeEnv,
    isProduction: nodeEnv === "production",

    noAuth: raw.ZENCODE_NO_AUTH,
    unsafeNoAuth: raw.ZENCODE_UNSAFE_NO_AUTH,
    authToken: raw.ZENCODE_AUTH_TOKEN?.trim() || undefined,

    logLevel: raw.ZENCODE_LOG_LEVEL,

    rateLimit: raw.ZENCODE_RATE_LIMIT ?? DEFAULT_RATE_LIMIT,
    rateWindowMs: raw.ZENCODE_RATE_WINDOW_MS ?? DEFAULT_RATE_WINDOW_MS,
    trustProxy: raw.ZENCODE_TRUST_PROXY,

    recordingsDir: raw.ZENCODE_RECORDINGS_DIR || undefined,
    recordingsMaxLines: raw.ZENCODE_RECORDINGS_MAX_LINES,
    recordEnabled: raw.ZENCODE_RECORD,

    idleTimeoutSeconds:
      raw.ZENCODE_IDLE_TIMEOUT_SECONDS ?? DEFAULT_IDLE_TIMEOUT,
    reconnectGraceMs:
      raw.ZENCODE_RECONNECT_GRACE_MS ?? DEFAULT_RECONNECT_GRACE_MS,

    sessionDir: raw.ZENCODE_SESSION_DIR || undefined,

    corsOrigins: raw.ZENCODE_CORS_ORIGINS,

    containerSdkHost: raw.ZENCODE_CONTAINER_SDK_HOST,
    forceBypassInContainer: raw.ZENCODE_FORCE_BYPASS_IN_CONTAINER,

    allowedPaths: raw.ZENCODE_ALLOWED_PATHS,

    editorPort: raw.ZENCODE_EDITOR_PORT ?? DEFAULT_EDITOR_PORT,

    initScriptTimeout:
      raw.ZENCODE_INIT_SCRIPT_TIMEOUT ?? DEFAULT_INIT_SCRIPT_TIMEOUT,

    packageRoot: raw.__ZENCODE_PACKAGE_ROOT || undefined,

    claudeProjectsDir: raw.CLAUDE_PROJECTS_DIR || undefined,
  });
}

// ── Exported singleton ──────────────────────────────────────────────────────

/**
 * The validated, frozen application config.
 * Parsed once at import time from `process.env`.
 */
export let config: AppConfig = parseConfig(process.env);

/**
 * Re-parse config from the current `process.env`.
 * **Only** for use in tests — never call in production code.
 */
export function _resetConfigForTesting(): void {
  config = parseConfig(process.env);
}
