import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import type {
  Installer,
  DetectResult,
  InstallContext,
  InstallPlan,
  InstallReceipt,
  HealthReport,
  StepReceipt,
  ConfigShape,
  ConfigMutation,
} from "../Installer";

const CODEX_CONFIG_DIR = join(homedir(), ".codex");
const CODEX_CONFIG_FILE = join(CODEX_CONFIG_DIR, "config.toml");
const MIN_VERSION = "1.0.0";

function getCodexVersion(): string | null {
  try {
    const r = Bun.spawnSync(["codex", "--version"], { stdout: "pipe", stderr: "pipe" });
    if (r.exitCode !== 0) return null;
    const out = new TextDecoder().decode(r.stdout).trim();
    const m = out.match(/\d+\.\d+\.\d+/);
    return m ? m[0] : (out || null);
  } catch {
    return null;
  }
}

function versionGte(a: string, b: string): boolean {
  const pa = a.split(".").map(Number);
  const pb = b.split(".").map(Number);
  for (let i = 0; i < 3; i++) {
    if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
    if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
  }
  return true;
}

/**
 * CodexInstaller — fewtok installer for OpenAI Codex CLI.
 *
 * Sets `base_url` in ~/.codex/config.toml so Codex routes requests
 * through the fewtok compression proxy, and writes compression hints
 * into AGENTS.md via the hint surface.
 */
export class CodexInstaller implements Installer {
  readonly id = "codex";
  readonly displayName = "OpenAI Codex CLI";

  async detect(): Promise<DetectResult> {
    const version = getCodexVersion();
    if (version === null) {
      return {
        present: false,
        hint: "codex not found on PATH. Install: npm install -g @openai/codex",
      };
    }
    return {
      present: true,
      version,
      minSatisfied: versionGte(version, MIN_VERSION),
      minRequired: MIN_VERSION,
    };
  }

  async plan(ctx: InstallContext): Promise<InstallPlan> {
    return {
      installerId: this.id,
      fewtokHome: ctx.fewtokHome,
      steps: [
        {
          kind: "config-file",
          path: CODEX_CONFIG_FILE,
          shape: "toml",
          mutations: [
            { op: "set", path: ["model", "provider"], value: "openai" },
            { op: "set", path: ["model", "base_url"], value: `${ctx.proxyUrl}/v1` },
          ],
        },
        {
          kind: "hint-surface",
          surfaceId: "agents-md",
        },
      ],
    };
  }

  async apply(plan: InstallPlan): Promise<InstallReceipt> {
    const receipts: StepReceipt[] = [];

    for (const step of plan.steps) {
      if (step.kind === "config-file" && step.shape === "toml") {
        mkdirSync(CODEX_CONFIG_DIR, { recursive: true });
        const prior = existsSync(step.path)
          ? readFileSync(step.path, "utf8")
          : null;

        let toml = prior ?? "";
        for (const mut of step.mutations) {
          if (mut.op === "set" && mut.path.length === 2) {
            toml = applyTomlSet(toml, mut.path[0]!, mut.path[1]!, String(mut.value));
          }
        }
        writeFileSync(step.path, toml, "utf8");

        // Checksum = first 16 base64url chars of the new content hash
        const enc = new TextEncoder();
        const hashBuf = await crypto.subtle.digest("SHA-1", enc.encode(toml));
        const postChecksum = Buffer.from(hashBuf).toString("hex").slice(0, 16);
        const backupPath = `${step.path}.bak`;
        if (prior !== null) writeFileSync(backupPath, prior, "utf8");

        receipts.push({
          kind: "config-file",
          path: step.path,
          backupPath,
          priorContent: prior,
          postChecksum,
        });
      } else if (step.kind === "hint-surface") {
        receipts.push({ kind: "hint-surface", surfaceId: step.surfaceId });
      }
    }

    return {
      installerId: this.id,
      version: 1,
      appliedAt: new Date().toISOString(),
      steps: receipts,
    };
  }

  async revert(receipt: InstallReceipt, _opts?: { force?: boolean }): Promise<void> {
    for (const step of receipt.steps) {
      if (step.kind === "config-file") {
        if (step.priorContent !== null) {
          writeFileSync(step.path, step.priorContent, "utf8");
        } else if (existsSync(step.path)) {
          // Remove only the base_url line we added
          const current = readFileSync(step.path, "utf8");
          const cleaned = current
            .split("\n")
            .filter((l) => !/^(base_url|provider)\s*=/.test(l.trim()))
            .join("\n");
          writeFileSync(step.path, cleaned, "utf8");
        }
      }
    }
  }

  async check(): Promise<HealthReport> {
    const findings: HealthReport["findings"] = [];

    if (!getCodexVersion()) {
      return {
        installerId: this.id,
        ok: false,
        findings: [{ level: "error", message: "codex not found on PATH" }],
      };
    }

    if (!existsSync(CODEX_CONFIG_FILE)) {
      findings.push({
        level: "warn",
        message: `${CODEX_CONFIG_FILE} not found`,
        remediation: "Run: fewtok install codex",
      });
      return { installerId: this.id, ok: false, findings };
    }

    const cfg = readFileSync(CODEX_CONFIG_FILE, "utf8");
    if (!cfg.includes("base_url")) {
      findings.push({
        level: "warn",
        message: "base_url not configured in codex config.toml",
        remediation: "Run: fewtok install codex",
      });
    }

    return { installerId: this.id, ok: findings.length === 0, findings };
  }
}

/** Minimal TOML key setter: adds/updates `key = "value"` under `[section]`. */
function applyTomlSet(toml: string, section: string, key: string, value: string): string {
  const header = `[${section}]`;
  const newLine = `${key} = "${value}"`;
  const keyRe = new RegExp(`^${key}\\s*=.*$`, "m");

  if (toml.includes(header)) {
    return keyRe.test(toml)
      ? toml.replace(keyRe, newLine)
      : toml.replace(header, `${header}\n${newLine}`);
  }

  const sep = toml.length > 0 && !toml.endsWith("\n") ? "\n" : "";
  return `${toml}${sep}\n[${section}]\n${newLine}\n`;
}
