/**
 * Manage Claude Code's SessionStart hook in `~/.claude/settings.json`.
 *
 * mcpctl needs `mcpctl skills sync --quiet` to run on every Claude
 * invocation. We do this via Claude Code's SessionStart hook mechanism;
 * to coexist with hooks the user added by hand, every entry we write
 * carries a `_mcpctl_managed: true` marker (which Claude Code ignores
 * but we use to identify our row on subsequent runs).
 *
 * Defensive against `~/.claude/settings.json` being missing, empty, or
 * shaped differently than expected (e.g. comments — JSON-with-comments
 * is allowed by some editors, though Claude Code itself only writes
 * pure JSON).
 */
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';

export const MARKER_KEY = '_mcpctl_managed';

interface HookEntry {
  type: 'command';
  command: string;
  // Markers — Claude Code ignores extra fields; we use them to identify ours.
  [k: string]: unknown;
}

interface HookGroup {
  hooks: HookEntry[];
  [k: string]: unknown;
}

interface Settings {
  hooks?: {
    SessionStart?: HookGroup[];
    [k: string]: unknown;
  };
  [k: string]: unknown;
}

function defaultSettingsPath(): string {
  return join(homedir(), '.claude', 'settings.json');
}

async function readSettings(path: string): Promise<Settings> {
  try {
    const raw = await readFile(path, 'utf-8');
    if (raw.trim().length === 0) return {};
    // Strip line comments so files written by VS Code etc still parse.
    // This is a heuristic — JSON-with-comments isn't a real spec — but it
    // covers the common case. Block comments ("/* ... */") are not stripped.
    const stripped = raw.replace(/^\s*\/\/.*$/gm, '');
    return JSON.parse(stripped) as Settings;
  } catch (err: unknown) {
    if (isNotFoundError(err)) return {};
    throw err;
  }
}

async function writeSettings(path: string, settings: Settings): Promise<void> {
  await mkdir(dirname(path), { recursive: true });
  const tmp = `${path}.tmp.${String(process.pid)}`;
  await writeFile(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
  await rename(tmp, path);
}

/**
 * Insert or update the managed SessionStart hook. Idempotent — running
 * `mcpctl config claude --project X` twice does not create duplicate
 * entries.
 */
export async function installManagedSessionHook(
  command: string,
  settingsPath: string = defaultSettingsPath(),
): Promise<{ updated: boolean; settingsPath: string }> {
  const settings = await readSettings(settingsPath);
  if (!settings.hooks) settings.hooks = {};
  if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];

  const groups = settings.hooks.SessionStart;
  let foundEntry = false;
  let entryChanged = false;

  for (const group of groups) {
    if (!Array.isArray(group?.hooks)) continue;
    for (let i = 0; i < group.hooks.length; i++) {
      const entry = group.hooks[i];
      if (entry !== undefined && entry[MARKER_KEY] === true) {
        foundEntry = true;
        if (entry.command !== command) {
          group.hooks[i] = { type: 'command', command, [MARKER_KEY]: true };
          entryChanged = true;
        }
      }
    }
  }

  if (!foundEntry) {
    groups.push({
      hooks: [{ type: 'command', command, [MARKER_KEY]: true }],
    });
    entryChanged = true;
  }

  if (entryChanged) {
    await writeSettings(settingsPath, settings);
  }
  return { updated: entryChanged, settingsPath };
}

/**
 * Remove the managed SessionStart hook (used by `mcpctl config claude
 * --uninstall` in a later PR). Returns whether anything was changed.
 */
export async function removeManagedSessionHook(
  settingsPath: string = defaultSettingsPath(),
): Promise<{ removed: boolean; settingsPath: string }> {
  const settings = await readSettings(settingsPath);
  const groups = settings.hooks?.SessionStart;
  if (!Array.isArray(groups)) return { removed: false, settingsPath };

  let changed = false;
  for (const group of groups) {
    if (!Array.isArray(group?.hooks)) continue;
    const before = group.hooks.length;
    group.hooks = group.hooks.filter((entry) => entry?.[MARKER_KEY] !== true);
    if (group.hooks.length !== before) changed = true;
  }
  // Drop any group that became empty.
  settings.hooks!.SessionStart = groups.filter((g) => Array.isArray(g.hooks) && g.hooks.length > 0);

  if (changed) {
    await writeSettings(settingsPath, settings);
  }
  return { removed: changed, settingsPath };
}

function isNotFoundError(err: unknown): boolean {
  return typeof err === 'object' && err !== null && (err as { code?: string }).code === 'ENOENT';
}
