import { spawn, type ChildProcess } from "child_process";
import path from "path";
import fs from "fs";
import { resolveMitmDataDir } from "./dataDir.ts";
import { addDNSEntry, addDNSEntries, removeDNSEntry } from "./dns/dnsConfig.ts";
import { generateCert } from "./cert/generate.ts";
import { installCert } from "./cert/install.ts";
import { ALL_TARGETS } from "./targets/index.ts";
import { detectAgent } from "./detection/index.ts";
import type { AgentId, DetectionResult, MitmTarget } from "./types.ts";
import { getAllAgentBridgeStates } from "@/lib/db/agentBridgeState.ts";
import { listCustomHosts } from "@/lib/db/inspectorCustomHosts.ts";
import { getUserBypassPatterns } from "@/lib/db/agentBridgeBypass.ts";
import { configureUpstreamCa } from "./upstreamTrust.ts";
import { createLogger } from "@/shared/utils/logger.ts";

const log = createLogger("mitm-manager");

// Store server process
let serverProcess: ChildProcess | null = null;
let serverPid: number | null = null;

// Module-scoped password cache (not exposed on globalThis).
// Cleared automatically when the MITM proxy is stopped.
let _cachedPassword: string | null = null;
export function getCachedPassword(): string | null {
  return _cachedPassword;
}
export function setCachedPassword(pwd: string | null | undefined): void {
  _cachedPassword = pwd || null;
}
export function clearCachedPassword(): void {
  _cachedPassword = null;
}

const PID_FILE = path.join(resolveMitmDataDir(), "mitm", ".mitm.pid");
const TARGETS_JSON_FILE = path.join(resolveMitmDataDir(), "mitm", "targets.json");
const BYPASS_JSON_FILE = path.join(resolveMitmDataDir(), "mitm", "bypass.json");
const CA_PATH_FILE = path.join(resolveMitmDataDir(), "mitm", "upstream-ca.path");

/** Read the persisted upstream CA path written by the POST upstream-ca route handler. */
function readStoredUpstreamCaPath(): string | null {
  try {
    if (!fs.existsSync(CA_PATH_FILE)) return null;
    const raw = fs.readFileSync(CA_PATH_FILE, "utf8").trim();
    return raw || null;
  } catch {
    return null;
  }
}

/**
 * Write the canonical `targets.json` consumed by `server.cjs` at startup.
 *
 * The file mirrors the static `ALL_TARGETS` registry; server.cjs treats it as
 * an extension of its baseline antigravity hosts. Hard Rule #13: only the
 * declarative target hosts are persisted — no runtime paths, no shell escapes.
 */
export function writeTargetsJson(targets: MitmTarget[] = ALL_TARGETS): void {
  const dir = path.join(resolveMitmDataDir(), "mitm");
  try {
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  } catch {
    // mkdir failures are non-fatal; the write below will report the real error.
  }
  const payload = {
    version: 1,
    generatedAt: new Date().toISOString(),
    targets: targets.map((t) => ({
      id: t.id,
      name: t.name,
      hosts: t.hosts,
      endpointPatterns: t.endpointPatterns,
      viability: t.viability ?? "supported",
    })),
  };
  fs.writeFileSync(TARGETS_JSON_FILE, JSON.stringify(payload, null, 2));
}

/**
 * Write the canonical `bypass.json` file consumed by `server.cjs` at startup.
 *
 * Only USER-configured patterns are persisted here — the default bypass
 * regexes (banks/gov/okta/auth0) live hard-coded in `server.cjs` and in
 * `src/mitm/passthrough.ts` so they apply even when the file is missing.
 *
 * Plan reference: 11-agent-bridge.plan.md §4.6 + master-plan-group-A.md §3.7.
 * Hard Rule #13: no shell interpolation, file only.
 */
export function writeBypassJson(userPatterns?: string[]): void {
  const dir = path.join(resolveMitmDataDir(), "mitm");
  try {
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  } catch {
    // mkdir failures are non-fatal; the write below will report the real error.
  }
  const patterns =
    Array.isArray(userPatterns) && userPatterns.length >= 0
      ? userPatterns
      : getUserBypassPatterns();
  const payload = {
    version: 1,
    generatedAt: new Date().toISOString(),
    patterns,
  };
  fs.writeFileSync(BYPASS_JSON_FILE, JSON.stringify(payload, null, 2));
}

export interface AgentStatus {
  id: AgentId;
  name: string;
  hosts: string[];
  viability: "supported" | "investigating" | "deprecated";
  detection: DetectionResult;
}

/**
 * Aggregate every registered MITM target with its current installation
 * detection result. Read-only — used by the AgentBridge dashboard.
 */
export function getAllAgentsStatus(): AgentStatus[] {
  return ALL_TARGETS.map((t) => ({
    id: t.id,
    name: t.name,
    hosts: t.hosts,
    viability: t.viability ?? "supported",
    detection: detectAgent(t.id),
  }));
}
const MITM_SERVER_URL = new URL("./server.cjs", import.meta.url);
const urlPath =
  process.platform === "win32" && MITM_SERVER_URL.pathname.startsWith("/")
    ? decodeURIComponent(MITM_SERVER_URL.pathname.slice(1))
    : decodeURIComponent(MITM_SERVER_URL.pathname);

const cwdPath = path.join(process.cwd(), "src", "mitm", "server.cjs");
const MITM_SERVER_PATH = fs.existsSync(cwdPath) ? cwdPath : urlPath;

// Check if a PID is alive
function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

/**
 * Get MITM status
 */
export async function getMitmStatus(): Promise<{
  running: boolean;
  pid: number | null;
  dnsConfigured: boolean;
  certExists: boolean;
}> {
  // Check in-memory process first, then fallback to PID file
  let running = serverProcess !== null && !serverProcess.killed;
  let pid = serverPid;

  if (!running) {
    try {
      if (fs.existsSync(PID_FILE)) {
        const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
        if (savedPid && isProcessAlive(savedPid)) {
          running = true;
          pid = savedPid;
        } else {
          // Stale PID file, clean up
          fs.unlinkSync(PID_FILE);
        }
      }
    } catch {
      // Ignore
    }
  }

  // Check DNS configuration
  let dnsConfigured = false;
  try {
    const hostsContent = fs.readFileSync("/etc/hosts", "utf-8");
    dnsConfigured = /\bdaily-cloudcode-pa\.googleapis\.com\b/.test(hostsContent);
  } catch {
    // Ignore
  }

  // Check cert
  const certDir = path.join(resolveMitmDataDir(), "mitm");
  const certExists = fs.existsSync(path.join(certDir, "server.crt"));

  return { running, pid, dnsConfigured, certExists };
}

/**
 * Start MITM proxy
 * @param {string} apiKey - OmniRoute API key
 * @param {string} sudoPassword - Sudo password for DNS/cert operations
 */
export async function startMitm(
  apiKey: string,
  sudoPassword: string,
  options: { port?: number } = {}
): Promise<{ running: true; pid: number | null }> {
  // Check if already running
  if (serverProcess && !serverProcess.killed) {
    throw new Error("MITM proxy is already running");
  }

  // 0. Persist the canonical targets.json so server.cjs can pick up the full
  //    AgentBridge target registry alongside its hard-coded antigravity baseline.
  try {
    writeTargetsJson();
  } catch (err) {
    log.error({ err }, "Failed to write targets.json (continuing)");
  }

  // 0b. Persist the user bypass patterns to bypass.json so server.cjs can
  //     route CONNECT tunnels for those hostnames without TLS decryption.
  //     Defaults (banks/gov/okta/auth0) are hard-coded in server.cjs.
  try {
    writeBypassJson();
  } catch (err) {
    log.error({ err }, "Failed to write bypass.json (continuing)");
  }

  // 0c. Apply upstream CA certificate (env var wins over stored path).
  //     Spec: plan 11 §4.7 + acceptance criterion §12 #18.
  try {
    const storedCaPath = readStoredUpstreamCaPath();
    const activeCaPath = process.env.AGENTBRIDGE_UPSTREAM_CA_CERT || storedCaPath;
    if (activeCaPath) {
      configureUpstreamCa(activeCaPath);
      log.info({ caPath: activeCaPath }, "Upstream CA certificate configured");
    }
  } catch (err) {
    log.error(
      { err },
      `AGENTBRIDGE_UPSTREAM_CA_CERT path invalid: ${(err as Error).message ?? err} (continuing without custom CA)`
    );
  }

  // 1. Generate SSL certificate if not exists
  const certPath = path.join(resolveMitmDataDir(), "mitm", "server.crt");
  if (!fs.existsSync(certPath)) {
    log.info("Generating SSL certificate...");
    await generateCert();
  }

  // 2. Install certificate to system keychain
  await installCert(sudoPassword, certPath);

  // 3. Add DNS entries: Antigravity defaults + all agents with dns_enabled=true +
  //    all custom hosts with enabled=true.
  log.info("Adding DNS entries...");
  await addDNSEntry(sudoPassword);

  // Collect hosts from agents that have dns_enabled=true in the DB.
  try {
    const agentStates = getAllAgentBridgeStates();
    const agentHostsToAdd: string[] = [];
    for (const state of agentStates) {
      if (!state.dns_enabled) continue;
      const target = ALL_TARGETS.find((t) => t.id === state.agent_id);
      if (target) {
        agentHostsToAdd.push(...target.hosts);
      }
    }
    if (agentHostsToAdd.length > 0) {
      log.info({ count: agentHostsToAdd.length }, "Adding DNS for agent host(s)...");
      await addDNSEntries(agentHostsToAdd, sudoPassword);
    }
  } catch (err) {
    log.error({ err }, "Failed to add agent DNS entries (continuing)");
  }

  // Collect enabled custom hosts.
  try {
    const customHosts = listCustomHosts({ enabledOnly: true });
    const customHostNames = customHosts.map((h) => h.host);
    if (customHostNames.length > 0) {
      log.info({ count: customHostNames.length }, "Adding DNS for custom host(s)...");
      await addDNSEntries(customHostNames, sudoPassword);
    }
  } catch (err) {
    log.error({ err }, "Failed to add custom host DNS entries (continuing)");
  }

  // 4. Start MITM server
  log.info("Starting MITM server...");
  const port =
    typeof options.port === "number" &&
    Number.isInteger(options.port) &&
    options.port > 0 &&
    options.port <= 65535
      ? options.port
      : 443;
  serverProcess = spawn(process.execPath, [MITM_SERVER_PATH], {
    env: {
      ...process.env,
      ROUTER_API_KEY: apiKey,
      MITM_LOCAL_PORT: String(port),
      NODE_ENV: "production",
    },
    detached: false,
    stdio: ["ignore", "pipe", "pipe"],
  });

  const proc = serverProcess;
  serverPid = proc.pid ?? null;

  // Save PID to file
  if (serverPid !== null) {
    fs.writeFileSync(PID_FILE, String(serverPid));
  }

  // Log server output
  proc.stdout?.on("data", (data) => {
    log.info({ source: "mitm-server" }, data.toString().trim());
  });

  proc.stderr?.on("data", (data) => {
    log.error({ source: "mitm-server" }, data.toString().trim());
  });

  proc.on("exit", (code) => {
    log.info({ exitCode: code }, "MITM server exited");
    serverProcess = null;
    serverPid = null;

    // Remove PID file
    try {
      fs.unlinkSync(PID_FILE);
    } catch (error) {
      // Ignore
    }
  });

  // Wait and verify server actually started
  const started = await new Promise<boolean>((resolve) => {
    let resolved = false;
    const timeout = setTimeout(() => {
      if (!resolved) {
        resolved = true;
        resolve(true);
      }
    }, 2000);

    proc.on("exit", () => {
      clearTimeout(timeout);
      if (!resolved) {
        resolved = true;
        resolve(false);
      }
    });

    // Check stderr for error messages
    proc.stderr?.on("data", (data) => {
      const msg = data.toString().trim();
      if (msg.includes("Port") && msg.includes("already in use")) {
        clearTimeout(timeout);
        if (!resolved) {
          resolved = true;
          resolve(false);
        }
      }
    });
  });

  if (!started) {
    throw new Error("MITM server failed to start (port 443 may be in use)");
  }

  return {
    running: true,
    pid: serverPid,
  };
}

/**
 * Stop MITM proxy
 * @param {string} sudoPassword - Sudo password for DNS cleanup
 */
export async function stopMitm(sudoPassword: string): Promise<{ running: false; pid: null }> {
  // 1. Kill server process (in-memory or from PID file)
  const proc = serverProcess;
  if (proc && !proc.killed) {
    log.info("Stopping MITM server...");
    proc.kill("SIGTERM");
    await new Promise((resolve) => setTimeout(resolve, 1000));
    if (!proc.killed) {
      proc.kill("SIGKILL");
    }
    serverProcess = null;
    serverPid = null;
  } else {
    // Fallback: kill by PID file
    try {
      if (fs.existsSync(PID_FILE)) {
        const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
        if (savedPid && isProcessAlive(savedPid)) {
          log.info({ pid: savedPid }, "Killing MITM server by PID...");
          process.kill(savedPid, "SIGTERM");
          await new Promise((resolve) => setTimeout(resolve, 1000));
          if (isProcessAlive(savedPid)) {
            process.kill(savedPid, "SIGKILL");
          }
        }
      }
    } catch {
      // Ignore
    }
    serverProcess = null;
    serverPid = null;
  }

  // 2. Remove DNS entry
  log.info("Removing DNS entry...");
  await removeDNSEntry(sudoPassword);

  // 3. Clean up
  clearCachedPassword(); // Clear password from memory when proxy stops
  try {
    fs.unlinkSync(PID_FILE);
  } catch (error) {
    // Ignore
  }

  return {
    running: false,
    pid: null,
  };
}
