// src/install/installers/ClaudeCodeInstaller.ts
import { join } from "node:path";
import { existsSync, readFileSync, mkdirSync } from "node:fs";
import type {
  Installer,
  InstallContext,
  InstallPlan,
  InstallReceipt,
  DetectResult,
  HealthReport,
  StepReceipt,
} from "../Installer.ts";
import { ConfigFileWriter } from "../blocks/ConfigFileWriter.ts";
import { WrapperScriptInstaller } from "../blocks/WrapperScriptInstaller.ts";
import { receiptPaths, saveReceipt } from "../receipt.ts";
import { writeSnapshot, type InstallerSnapshot, type SnapshotEntry } from "../snapshot.ts";
import { CTX_MATCHER } from "../../adapters/contextMode.ts";

export class ClaudeCodeInstaller implements Installer {
  readonly id = "claude";
  readonly displayName = "Claude Code";

  constructor(private readonly userHome: string) {}

  private get settingsPath(): string {
    return join(this.userHome, ".claude", "settings.json");
  }

  async detect(): Promise<DetectResult> {
    const proc = Bun.spawnSync(["which", "claude"], { stderr: "ignore" });
    if (proc.exitCode !== 0) {
      return { present: false, hint: "claude CLI not found; install from https://claude.ai/download" };
    }
    const versionProc = Bun.spawnSync(["claude", "--version"], { stderr: "ignore" });
    const versionStr = new TextDecoder().decode(versionProc.stdout).trim();
    const match = versionStr.match(/(\d+\.\d+\.\d+)/);
    const version = match?.[1] ?? "0.0.0";
    return { present: true, version, minSatisfied: true, minRequired: "0.0.0" };
  }

  // DO NOT add `set` or `hook-array-merge` ops here — see CLAUDE.md
  // "no-global-settings-injection". 3 reverts as of 2026-05-22.
  // Only cleanup mutations (hook-array-delete + env unset) are permitted, so
  // re-running this installer scrubs prior pollution without re-injecting it.
  // The `cld` wrapper (bin/cld) is the sole activation path.
  async plan(_ctx: InstallContext): Promise<InstallPlan> {
    const steps: InstallPlan["steps"] = [];

    const allMutations: import("../Installer.ts").ConfigMutation[] = [
      // ── Cleanup ops only ──
      // Strip stale ft entries if they exist from a prior (now-forbidden) install.
      { op: "delete", path: ["env", "ANTHROPIC_BASE_URL"] },
      {
        op: "hook-array-delete",
        path: ["hooks", "SessionStart"],
        predicate: { kind: "command-suffix", suffix: "/hooks/session-start" },
      },
      {
        op: "hook-array-delete",
        path: ["hooks", "PreToolUse"],
        matcher: "Bash",
        predicate: { kind: "command-suffix", suffix: "/hooks/pre-bash" },
      },
      // Legacy ctx-mode integration cleanup.
      {
        op: "hook-array-delete",
        path: ["hooks", "SessionStart"],
        predicate: { kind: "command-suffix", suffix: "/hooks/ctx-mode-guard" },
      },
      {
        op: "hook-array-delete",
        path: ["hooks", "SessionStart"],
        predicate: { kind: "command-suffix", suffix: "/session-start.js" },
      },
      {
        op: "hook-array-delete",
        path: ["hooks", "PreToolUse"],
        predicate: { kind: "matcher-equals", matcher: CTX_MATCHER },
      },
      {
        op: "hook-array-delete",
        path: ["hooks", "PreToolUse"],
        matcher: "Bash",
        predicate: { kind: "command-suffix", suffix: "/hooks/bash-gate.sh" },
      },
    ];

    // Pre-filter to only effective mutations (idempotency: re-plan after apply = 0).
    const w = new ConfigFileWriter(this.settingsPath, "json");
    const filePlan = await w.plan(allMutations);
    const effectiveMutations = filePlan.mutations;

    steps.push({
      kind: "config-file",
      path: this.settingsPath,
      shape: "json",
      mutations: effectiveMutations,
    });

    steps.push({ kind: "hint-surface", surfaceId: "claude-md" });
    return { installerId: this.id, fewtokHome: _ctx.fewtokHome, steps };
  }

  async apply(plan: InstallPlan): Promise<InstallReceipt> {
    if (existsSync(this.settingsPath)) {
      const prior = JSON.parse(readFileSync(this.settingsPath, "utf8")) as Record<string, unknown>;
      const deleted: SnapshotEntry[] = [];
      const hooks = (prior.hooks ?? {}) as Record<string, unknown>;
      const sessionStart = (hooks["SessionStart"] ?? []) as Array<{ hooks?: Array<{ command?: string }> }>;
      for (const e of sessionStart) {
        for (const h of e.hooks ?? []) {
          if (
            h.command?.endsWith("/hooks/ctx-mode-guard") ||
            h.command?.endsWith("/session-start.js")
          ) {
            deleted.push({ path: ["hooks", "SessionStart"], entry: { hooks: [h] } });
          }
        }
      }
      const preToolUse = (hooks["PreToolUse"] ?? []) as Array<{
        matcher?: string;
        hooks?: Array<{ command?: string }>;
      }>;
      for (const e of preToolUse) {
        if (e.matcher === CTX_MATCHER) {
          deleted.push({ path: ["hooks", "PreToolUse"], block: e });
        }
        if (e.matcher === "Bash") {
          for (const h of e.hooks ?? []) {
            if (h.command?.endsWith("/hooks/bash-gate.sh")) {
              deleted.push({ path: ["hooks", "PreToolUse", "Bash"], entry: h });
            }
          }
        }
      }
      const ts = new Date().toISOString().replace(/:/g, "-");
      const snapDir = join(plan.fewtokHome, "state", "install-snapshots", "claude");
      const snapPath = join(snapDir, `${ts}.json`);
      mkdirSync(snapDir, { recursive: true });
      const snap: InstallerSnapshot = {
        version: 1,
        createdAt: new Date().toISOString(),
        deleted,
      };
      writeSnapshot(snapPath, snap);
    }

    const receipts: StepReceipt[] = [];
    const { backupDir } = receiptPaths(plan.fewtokHome, this.id);

    for (const step of plan.steps) {
      if (step.kind === "config-file") {
        const w = new ConfigFileWriter(step.path, step.shape);
        const filePlan = await w.plan(step.mutations);
        const r = await w.apply(filePlan, backupDir);
        receipts.push(r);
      } else if (step.kind === "wrapper") {
        const w = new WrapperScriptInstaller(step.symlink, step.target);
        receipts.push(await w.apply());
      } else if (step.kind === "hint-surface") {
        receipts.push({ kind: "hint-surface", surfaceId: step.surfaceId });
      } else if (step.kind === "env-var") {
        receipts.push({ kind: "env-var", shellRc: step.shellRc, lineRangeAdded: [0, 0] });
      }
    }

    const receipt: InstallReceipt = {
      installerId: this.id,
      version: 1,
      appliedAt: new Date().toISOString(),
      steps: receipts,
    };

    saveReceipt(plan.fewtokHome, receipt);
    return receipt;
  }

  async revert(receipt: InstallReceipt, _opts?: { force?: boolean }): Promise<void> {
    for (const step of receipt.steps) {
      if (step.kind === "config-file") {
        const w = new ConfigFileWriter(step.path, "json");
        await w.revertForce(step);
      } else if (step.kind === "wrapper") {
        const w = new WrapperScriptInstaller(step.symlink, step.priorTarget ?? "");
        await w.revert(step);
      }
    }
  }

  // Inverted check (2026-05-22): ft activates per-session via the `cld` wrapper.
  // Healthy state = ~/.claude/settings.json is CLEAN of ANTHROPIC_BASE_URL and ft hooks.
  // Any presence of ft entries in settings.json is accidental pollution —
  // run `fewtok uninstall` to scrub.
  async check(): Promise<HealthReport> {
    const findings: HealthReport["findings"] = [];

    if (!existsSync(this.settingsPath)) {
      findings.push({ level: "ok", message: "settings.json absent (clean state)" });
      return { installerId: this.id, ok: true, findings };
    }

    try {
      const cfg = JSON.parse(readFileSync(this.settingsPath, "utf8")) as Record<string, unknown>;
      const env = cfg.env as Record<string, string> | undefined;
      if (env?.ANTHROPIC_BASE_URL) {
        findings.push({
          level: "error",
          message: `ANTHROPIC_BASE_URL present in settings.json (= ${env.ANTHROPIC_BASE_URL}) — global proxy injection forbidden`,
          remediation: "run: fewtok uninstall  (then activate per-session via `cld`)",
        });
      } else {
        findings.push({ level: "ok", message: "settings.json clean (no ANTHROPIC_BASE_URL)" });
      }

      const hooks = (cfg.hooks ?? {}) as Record<string, unknown>;
      const allEntries = [
        ...((hooks["SessionStart"] ?? []) as Array<{ hooks?: Array<{ command?: string }> }>),
        ...((hooks["PreToolUse"] ?? []) as Array<{ hooks?: Array<{ command?: string }> }>),
      ];
      const ftHookHit = allEntries.some((e) =>
        (e.hooks ?? []).some((h) => h.command?.includes("/.fewtok/hooks/")),
      );
      if (ftHookHit) {
        findings.push({
          level: "error",
          message: "ft hook entries detected in settings.json",
          remediation: "run: fewtok uninstall",
        });
      } else {
        findings.push({ level: "ok", message: "no ft hook entries in settings.json" });
      }
    } catch (e) {
      findings.push({ level: "error", message: `settings.json parse error: ${e}` });
    }

    const cldPath = join(this.userHome, ".local", "bin", "cld");
    const cldRepoPath = join(this.userHome, "Projects", "fewtok", "bin", "cld");
    const cldExists = existsSync(cldPath) || existsSync(cldRepoPath);
    findings.push(
      cldExists
        ? { level: "ok", message: "`cld` wrapper available (per-session activation path)" }
        : { level: "warn", message: "`cld` wrapper not found on PATH — run via repo `bin/cld`" },
    );

    const ok = findings.every((f) => f.level !== "error");
    return { installerId: this.id, ok, findings };
  }
}
