#!/usr/bin/env node

/**
 * Installs Claude Code hooks that forward events to the Agent Dashboard.
 * Modifies ~/.claude/settings.json to add hook entries.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const fs = require("fs");
const path = require("path");

const { getSettingsPath } = require("../server/lib/claude-home");
const SETTINGS_PATH = getSettingsPath();
const HOOK_HANDLER = path.resolve(__dirname, "hook-handler.js").replace(/\\/g, "/");

/**
 * True when the plugin bootstrap has run on this machine — i.e. the `ccam`
 * plugin is (or was) installed and supplies its own hook entries.
 * Read-only and never throws; `plugin-bootstrap.js` is not required here to
 * keep this module free of a circular dependency.
 *
 * @returns {boolean}
 */
function pluginBootstrapRan() {
  try {
    const { getDataDir } = require("../server/lib/claude-home");
    return fs.existsSync(path.join(getDataDir(), "runtime", "state.json"));
  } catch {
    return false;
  }
}

function envFlag(name) {
  return ["1", "true", "yes", "on"].includes(String(process.env[name] || "").toLowerCase());
}

/**
 * True when this process is running inside a container (Docker, Podman, or a
 * Kubernetes pod). Detected via the Docker/Podman marker files, the OCI/systemd
 * `container` env var, and a Linux cgroup heuristic. `CCAM_FORCE_CONTAINER=1`
 * forces a positive result and `CCAM_FORCE_HOST=1` forces a negative result
 * (used by tests / to override misfiring detection).
 *
 * Why this matters (GitHub #193): the hook command written into
 * `~/.claude/settings.json` embeds the absolute handler path resolved here.
 * Inside a container that path (e.g. `/app/scripts/hook-handler.js`) does not
 * exist on the host. When `~/.claude` is bind-mounted, installing from the
 * container poisons the host settings and every host hook fails with
 * `MODULE_NOT_FOUND`. Claude Code runs on the host, so hooks must be installed
 * on the host.
 *
 * @returns {boolean}
 */
function isInsideContainer() {
  if (envFlag("CCAM_FORCE_CONTAINER")) return true;
  if (envFlag("CCAM_FORCE_HOST")) return false;
  try {
    if (fs.existsSync("/.dockerenv")) return true; // Docker
    if (fs.existsSync("/run/.containerenv")) return true; // Podman
  } catch {
    /* fs probe failed — fall through to other signals */
  }
  // systemd-nspawn / Podman (and often Docker) export `container`.
  if (typeof process.env.container === "string" && process.env.container.length > 0) return true;
  // Linux cgroup heuristic — covers Docker, containerd, Kubernetes, Podman.
  try {
    const cgroup = fs.readFileSync("/proc/self/cgroup", "utf8");
    if (/\b(docker|containerd|kubepods|libpod|podman)\b/.test(cgroup)) return true;
  } catch {
    /* not Linux / no cgroup file — not a container by this signal */
  }
  return false;
}

/** Multi-line message explaining why a container install is refused. */
function containerRefusalMessage() {
  return [
    "✖ Refusing to install Claude Code hooks from inside a container.",
    "",
    `  The hook command would embed this handler path:`,
    `      ${HOOK_HANDLER}`,
    `  written into:`,
    `      ${SETTINGS_PATH}`,
    "",
    "  Claude Code runs on the HOST. When ~/.claude is bind-mounted, a",
    "  container-internal handler path does not exist on the host, so every host",
    "  hook fails with MODULE_NOT_FOUND (e.g. the SessionEnd hook). See issue #193.",
    "",
    "  → Install hooks ON THE HOST instead:",
    "        npm run install-hooks",
    "        # or: node /path/to/Claude-Code-Agent-Monitor/scripts/install-hooks.js",
    "",
    "  The host handler POSTs to http://localhost:4820, which the container already",
    "  publishes — so a host-installed hook reaches the containerized dashboard.",
    "",
    "  If you genuinely run Claude Code inside this same container, override with:",
    "        CCAM_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks",
  ].join("\n");
}

// Hook types to install. Some support matchers, some don't.
const HOOKS_WITH_MATCHER = ["PreToolUse", "PostToolUse", "Stop", "SubagentStop", "Notification"];
// UserPromptSubmit fires the instant the user hits enter — the only reliable
// signal that the user has resumed for *text-only* turns (no PreToolUse will
// fire until Claude calls a tool, which never happens for plain-text replies).
// Without it the Waiting badge persists through the entire generation of a
// text response. SessionStart / SessionEnd / UserPromptSubmit don't take
// tool-name matchers, hence the separate list.
const HOOKS_WITHOUT_MATCHER = ["SessionStart", "SessionEnd", "UserPromptSubmit"];
const HOOK_TYPES = [...HOOKS_WITH_MATCHER, ...HOOKS_WITHOUT_MATCHER];

function makeHookEntry(hookType) {
  const entry = {
    hooks: [
      {
        type: "command",
        command: `node "${HOOK_HANDLER}" ${hookType}`,
      },
    ],
  };
  if (HOOKS_WITH_MATCHER.includes(hookType)) {
    entry.matcher = "*";
  }
  return entry;
}

function isOurEntry(entry) {
  // Matches old format (entry.command) and new format (entry.hooks[].command)
  if (entry.command && entry.command.includes("hook-handler.js")) return true;
  if (Array.isArray(entry.hooks)) {
    return entry.hooks.some((h) => h.command && h.command.includes("hook-handler.js"));
  }
  return false;
}

function installHooks(silent = false) {
  // Host-only guard (issue #193): never write a container-internal handler path
  // into a (potentially bind-mounted) host settings file. Honors an explicit
  // opt-out for the rare case of running Claude Code inside this same container.
  if (isInsideContainer() && !envFlag("CCAM_ALLOW_CONTAINER_HOOKS")) {
    if (!silent) console.error(containerRefusalMessage());
    return false;
  }

  // The `ccam` plugin installs the same eight hooks itself. Running both means
  // every event is POSTed twice and token/cost figures double — warn loudly
  // rather than silently double-count. (`/ccam-doctor` reports the same state.)
  if (!silent && pluginBootstrapRan()) {
    console.warn(
      "WARNING: the ccam plugin is installed and already provides these hooks.\n" +
        "         Installing them again double-counts every event. Uninstall the\n" +
        "         plugin first, or skip this step. Run /ccam-doctor to check."
    );
  }

  let settings = {};
  if (fs.existsSync(SETTINGS_PATH)) {
    try {
      const raw = fs.readFileSync(SETTINGS_PATH, "utf8");
      settings = JSON.parse(raw);
    } catch (err) {
      if (!silent) console.error(`Failed to parse ${SETTINGS_PATH}:`, err.message);
      return false;
    }
  }

  if (!settings.hooks) settings.hooks = {};

  let installed = 0;
  let updated = 0;

  for (const hookType of HOOK_TYPES) {
    if (!settings.hooks[hookType]) settings.hooks[hookType] = [];

    const existing = settings.hooks[hookType].findIndex(isOurEntry);
    const entry = makeHookEntry(hookType);

    if (existing >= 0) {
      settings.hooks[hookType][existing] = entry;
      updated++;
    } else {
      settings.hooks[hookType].push(entry);
      installed++;
    }
  }

  const dir = path.dirname(SETTINGS_PATH);
  fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");

  if (!silent) {
    console.log(`Hook handler: ${HOOK_HANDLER}`);
    console.log(`Settings file: ${SETTINGS_PATH}`);
    console.log(`Installed: ${installed} new, updated: ${updated} existing`);
    console.log("Claude Code hooks configured. Start a new Claude Code session to begin tracking.");
  }

  return true;
}

if (require.main === module) {
  // Non-zero exit on refusal/failure so CI and shell users notice it.
  if (!installHooks(false)) process.exitCode = 1;
}

// isOurEntry is also used by scripts/plugin-bootstrap.js to strip hook entries
// a previous checkout install left behind (they would double-count events).
module.exports = { installHooks, isInsideContainer, isOurEntry };
