#!/usr/bin/env node
// Merge Clawd hooks into Kiro agent configs under ~/.kiro/agents/
// Kiro hooks are per-agent (no global hooks yet), so we inject into every
// custom agent config file and maintain a dedicated "clawd" agent config.
// Built-in agents are not backed by editable JSON files, so we cannot
// "override" kiro_default by creating ~/.kiro/agents/kiro_default.json.
// Users who want hooks must explicitly use the generated "clawd" agent.
// Docs: https://kiro.dev/docs/cli/hooks/
// Config reference: https://kiro.dev/docs/cli/custom-agents/configuration-reference/

const fs = require("fs");
const path = require("path");
const os = require("os");
const { spawnSync } = require("child_process");
const { resolveNodeBin } = require("./server-config");
const {
  readJsonFile,
  writeJsonAtomic,
  writeJsonAtomicWithBackup,
  commandMatchesMarker,
  extractExistingNodeBin,
  formatNodeHookCommand,
  removeMatchingCommandHooks,
} = require("./json-utils");
const MARKER = "kiro-hook.js";
const CLAWD_AGENT_NAME = "clawd";
const CLAWD_AGENT_DESCRIPTION = "Clawd desktop pet hook integration";
const BUILTIN_DEFAULT_AGENT = "kiro_default";
const DEFAULT_PARENT_DIR = path.join(os.homedir(), ".kiro");
const DEFAULT_AGENTS_DIR = path.join(DEFAULT_PARENT_DIR, "agents");

const KIRO_HOOK_EVENTS = [
  "agentSpawn",
  "userPromptSubmit",
  "preToolUse",
  "postToolUse",
  "stop",
];

/**
 * Inject Clawd hooks into a single agent config file.
 * @param {string} filePath
 * @param {object} [options]
 * @returns {{ added: number, skipped: number, updated: number, created: boolean }}
 */
function injectHooksIntoFile(filePath, options = {}) {
  let settings = {};
  let created = false;
  try {
    settings = JSON.parse(fs.readFileSync(filePath, "utf-8"));
  } catch (err) {
    if (err.code !== "ENOENT") {
      throw new Error(`Failed to read ${path.basename(filePath)}: ${err.message}`);
    }
    created = true;
  }

  let changed = false;
  const baseName = path.basename(filePath, ".json");

  // Ensure name field (required by Kiro).
  if (!settings.name) {
    settings.name = baseName;
    changed = true;
  }
  if (created) {
    settings.description = baseName === CLAWD_AGENT_NAME
      ? CLAWD_AGENT_DESCRIPTION
      : `${baseName} agent with Clawd desktop pet hooks`;
  }

  // Resolve node path; if detection fails, preserve existing absolute path
  const resolved = options.nodeBin !== undefined ? options.nodeBin : resolveNodeBin();
  const nodeBin = resolved
    || extractExistingNodeBin(settings, MARKER)
    || "node";
  const desiredCommand = formatHookCommand(nodeBin, getHookScriptPath(), options.platform);

  if (!settings.hooks || typeof settings.hooks !== "object") settings.hooks = {};

  let added = 0;
  let skipped = 0;
  let updated = 0;

  for (const event of KIRO_HOOK_EVENTS) {
    if (!Array.isArray(settings.hooks[event])) {
      settings.hooks[event] = [];
      changed = true;
    }

    const arr = settings.hooks[event];
    let found = false;
    let stalePath = false;
    for (const entry of arr) {
      if (!entry || typeof entry !== "object" || typeof entry.command !== "string") continue;
      if (!entry.command.includes(MARKER)) continue;
      found = true;
      if (entry.command !== desiredCommand) {
        entry.command = desiredCommand;
        stalePath = true;
      }
      break;
    }

    if (found) {
      if (stalePath) {
        updated++;
        changed = true;
      } else {
        skipped++;
      }
      continue;
    }

    arr.push({ command: desiredCommand });
    added++;
    changed = true;
  }

  if (changed) {
    writeJsonAtomic(filePath, settings);
  }

  return { added, skipped, updated, created };
}

function getHookScriptPath() {
  let hookScript = path.resolve(__dirname, "kiro-hook.js").replace(/\\/g, "/");
  hookScript = hookScript.replace("app.asar/", "app.asar.unpacked/");
  return hookScript;
}

// PowerShell parses bare quoted strings as literals ("node" => the string
// "node" rather than an exec), so the leading `& ` call operator is required
// on Windows to invoke the binary. POSIX shells need no such prefix.
function formatHookCommand(nodeBin, scriptPath, platformOverride) {
  const platform = platformOverride || process.platform;
  return formatNodeHookCommand(nodeBin, scriptPath, {
    platform,
    windowsWrapper: "powershell",
  });
}

function getKiroCliCandidates(homeDir = os.homedir(), platformOverride, env = process.env) {
  const platform = platformOverride || process.platform;
  if (platform === "win32") {
    const localAppData = env.LOCALAPPDATA || path.win32.join(homeDir, "AppData", "Local");
    const programFiles = env.ProgramFiles || "C:\\Program Files";
    return [
      path.win32.join(localAppData, "Kiro-Cli", "kiro-cli.exe"),
      path.win32.join(programFiles, "Kiro-Cli", "kiro-cli.exe"),
      "kiro-cli.exe",
      "kiro-cli",
    ];
  }
  return [
    path.join(homeDir, ".local", "bin", "kiro-cli"),
    "/opt/homebrew/bin/kiro-cli",
    "/usr/local/bin/kiro-cli",
    "kiro-cli",
  ];
}

// Properties excluded from kiro_default template.
const EXCLUDED_KEYS = new Set(["model", "includeMcpJson", "description", "hooks", "name"]);

function generateClawdTemplateFromBuiltin(options = {}) {
  const homeDir = options.homeDir || os.homedir();
  const platform = options.platform || process.platform;
  const env = options.env || process.env;
  const kiroCliCandidates = options.kiroCliCandidates || getKiroCliCandidates(homeDir, platform, env);
  // Kiro writes the agent JSON to disk *before* invoking $EDITOR, so the no-op
  // editor only needs to exit cleanly. `true` ships with macOS/Linux but not
  // Windows; `cmd /c exit` is the closest portable equivalent on Windows.
  const noopEditor = platform === "win32" ? "cmd /c exit" : "true";
  let lastError = null;
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawd-kiro-seed-"));
  const tempName = `clawd-seed-${process.pid}-${Date.now()}`;
  const templatePath = path.join(tempDir, `${tempName}.json`);

  try {
    for (const candidate of kiroCliCandidates) {
      const result = spawnSync(
        candidate,
        ["agent", "create", tempName, "--directory", tempDir, "--from", BUILTIN_DEFAULT_AGENT],
        {
          stdio: "ignore",
          env: { ...env, EDITOR: noopEditor },
          windowsHide: true,
        }
      );

      if (result.error && result.error.code === "ENOENT") {
        lastError = result.error;
        continue;
      }
      if (result.error) lastError = result.error;

      // Trust the file, not the exit code: Kiro writes the JSON before $EDITOR
      // runs, so a non-zero exit (e.g. EDITOR mis-fire on Windows) still
      // leaves a usable template behind.
      try {
        const template = JSON.parse(fs.readFileSync(templatePath, "utf-8"));
        return { template, command: candidate };
      } catch (err) {
        lastError = err;
      }
    }
  } finally {
    try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch {}
  }

  return { template: null, error: lastError };
}

function syncClawdAgentFromBuiltin(filePath, options = {}) {
  let current = null;
  try {
    current = JSON.parse(fs.readFileSync(filePath, "utf-8"));
  } catch (err) {
    if (err.code !== "ENOENT") throw err;
  }

  const result = generateClawdTemplateFromBuiltin(options);

  if (!result.template) {
    // Preserve existing file — may have prompt/tools/resources from a prior successful sync.
    if (!options.silent) {
      const fate = current
        ? `preserving existing ${path.basename(filePath)}`
        : "seeding minimal clawd agent (no prompt/tools/resources)";
      console.warn(`Clawd: kiro-cli template generation failed — ${fate}. Reason: ${result.error?.message || "unknown"}`);
    }
    if (current) return { synced: true, changed: false };
    const minimal = {
      name: CLAWD_AGENT_NAME,
      description: CLAWD_AGENT_DESCRIPTION,
      hooks: {},
    };
    writeJsonAtomic(filePath, minimal);
    return { synced: true, changed: true };
  }

  const desired = {
    name: CLAWD_AGENT_NAME,
    description: CLAWD_AGENT_DESCRIPTION,
  };
  for (const key of Object.keys(result.template)) {
    if (!EXCLUDED_KEYS.has(key)) {
      desired[key] = result.template[key];
    }
  }
  desired.hooks = current && current.hooks && typeof current.hooks === "object"
    ? current.hooks
    : {};

  if (!current || JSON.stringify(current) !== JSON.stringify(desired)) {
    writeJsonAtomic(filePath, desired);
    return { synced: true, changed: true };
  }

  return { synced: true, changed: false };
}

/**
 * Register Clawd hooks into Kiro agent configs under ~/.kiro/agents/
 * @param {object} [options]
 * @param {boolean} [options.silent]
 * @returns {{ added: number, skipped: number, updated: number, files: string[] }}
 */
function registerKiroHooks(options = {}) {
  const homeDir = options.homeDir || os.homedir();
  const agentsDir = options.agentsDir || path.join(homeDir, ".kiro", "agents");

  // Skip if ~/.kiro/ doesn't exist (Kiro CLI not installed)
  if (!fs.existsSync(agentsDir)) {
    if (!options.silent) console.log("Clawd: ~/.kiro/ not found — skipping Kiro hook registration");
    return { added: 0, skipped: 0, updated: 0, files: [] };
  }

  let totalAdded = 0;
  let totalSkipped = 0;
  let totalUpdated = 0;
  const files = [];

  // Scan all .json files in ~/.kiro/agents/ (skip example files)
  let entries;
  try {
    entries = fs.readdirSync(agentsDir);
  } catch {
    entries = [];
  }

  const jsonFiles = entries.filter(f =>
    f.endsWith(".json") && !f.includes(".example")
  );

  // Inject hooks into every existing custom agent config.
  for (const file of jsonFiles) {
    if (file === `${BUILTIN_DEFAULT_AGENT}.json`) continue;
    const filePath = path.join(agentsDir, file);
    try {
      const result = injectHooksIntoFile(filePath, options);
      totalAdded += result.added;
      totalSkipped += result.skipped;
      totalUpdated += result.updated;
      if (result.added > 0 || result.updated > 0 || result.created) {
        files.push(file);
      }
    } catch (err) {
      if (!options.silent) console.warn(`Clawd: failed to process ${file}: ${err.message}`);
    }
  }

  const clawdPath = path.join(agentsDir, `${CLAWD_AGENT_NAME}.json`);
  let clawdTemplateChanged = false;
  try {
    const seedFn = typeof options.syncClawdAgent === "function"
      ? options.syncClawdAgent
      : syncClawdAgentFromBuiltin;
    const syncResult = seedFn(clawdPath, options);
    clawdTemplateChanged = !!(syncResult && syncResult.changed);
  } catch (err) {
    if (!options.silent) console.warn(`Clawd: failed to sync ${CLAWD_AGENT_NAME}.json from ${BUILTIN_DEFAULT_AGENT}: ${err.message}`);
  }
  try {
    const result = injectHooksIntoFile(clawdPath, options);
    totalAdded += result.added;
    totalSkipped += result.skipped;
    totalUpdated += result.updated + (clawdTemplateChanged ? 1 : 0);
    if (result.added > 0 || result.updated > 0 || clawdTemplateChanged) {
      files.push(result.created ? `${CLAWD_AGENT_NAME}.json (created)` : `${CLAWD_AGENT_NAME}.json`);
    }
  } catch (err) {
    if (!options.silent) console.warn(`Clawd: failed to sync ${CLAWD_AGENT_NAME}.json: ${err.message}`);
  }

  if (!options.silent) {
    if (files.length > 0) {
      console.log(`Clawd: Kiro hooks injected into ${files.length} agent config(s): ${files.join(", ")}`);
      console.log(`  Added: ${totalAdded}, updated: ${totalUpdated}, skipped: ${totalSkipped}`);
    } else {
      console.log("Clawd: all Kiro agent configs already up to date");
    }
    console.log(`Clawd: use "kiro-cli --agent ${CLAWD_AGENT_NAME}" or run "/agent swap ${CLAWD_AGENT_NAME}" inside Kiro to enable hooks`);
  }

  return { added: totalAdded, skipped: totalSkipped, updated: totalUpdated, files };
}

function removeHooksFromKiroFile(filePath, options = {}) {
  let settings = {};
  try {
    settings = readJsonFile(filePath);
  } catch (err) {
    if (err.code === "ENOENT") return { removed: 0, changed: false, filePath };
    throw new Error(`Failed to read ${path.basename(filePath)}: ${err.message}`);
  }

  if (!settings.hooks || typeof settings.hooks !== "object") {
    return { removed: 0, changed: false, filePath };
  }

  let removed = 0;
  let changed = false;
  for (const event of KIRO_HOOK_EVENTS) {
    const entries = settings.hooks[event];
    if (!Array.isArray(entries)) continue;
    const result = removeMatchingCommandHooks(entries, (command) => commandMatchesMarker(command, MARKER));
    if (!result.changed) continue;
    removed += result.removed;
    changed = true;
    if (result.entries.length > 0) settings.hooks[event] = result.entries;
    else delete settings.hooks[event];
  }

  let backupPath = null;
  if (changed) backupPath = writeJsonAtomicWithBackup(filePath, settings, options);
  const result = { removed, changed, filePath };
  if (options.backup === true) result.backupPath = backupPath;
  return result;
}

function unregisterKiroHooks(options = {}) {
  const homeDir = options.homeDir || os.homedir();
  const agentsDir = options.agentsDir || path.join(homeDir, ".kiro", "agents");

  if (!fs.existsSync(agentsDir)) {
    if (!options.silent) console.log("Clawd: ~/.kiro/ not found - skipping Kiro hook cleanup");
    return { removed: 0, changed: false, files: [], warnings: [], agentsDir };
  }

  let entries = [];
  try {
    entries = fs.readdirSync(agentsDir);
  } catch (err) {
    return {
      removed: 0,
      changed: false,
      files: [],
      warnings: [`Failed to list ${agentsDir}: ${err.message}`],
      agentsDir,
    };
  }

  const jsonFiles = entries.filter((file) =>
    file.endsWith(".json")
    && !file.includes(".example")
    && file !== `${BUILTIN_DEFAULT_AGENT}.json`
  );

  let removed = 0;
  let changed = false;
  const files = [];
  const backupPaths = [];
  const warnings = [];

  for (const file of jsonFiles) {
    const filePath = path.join(agentsDir, file);
    try {
      const result = removeHooksFromKiroFile(filePath, options);
      removed += result.removed;
      if (result.changed) {
        changed = true;
        files.push(file);
        if (result.backupPath) backupPaths.push(result.backupPath);
      }
    } catch (err) {
      warnings.push(`Failed to clean ${file}: ${err.message}`);
    }
  }

  const retainedClawdAgent = fs.existsSync(path.join(agentsDir, `${CLAWD_AGENT_NAME}.json`));
  if (!options.silent) {
    console.log(`Clawd Kiro hooks removed: ${removed}`);
    for (const warning of warnings) console.warn(`  Warning: ${warning}`);
  }
  const result = { removed, changed, files, warnings, agentsDir, retainedClawdAgent };
  if (options.backup === true) result.backupPaths = backupPaths;
  return result;
}

module.exports = {
  DEFAULT_PARENT_DIR,
  DEFAULT_AGENTS_DIR,
  registerKiroHooks,
  unregisterKiroHooks,
  KIRO_HOOK_EVENTS,
  __test: {
    formatHookCommand,
    generateClawdTemplateFromBuiltin,
    getKiroCliCandidates,
    injectHooksIntoFile,
    removeHooksFromKiroFile,
    syncClawdAgentFromBuiltin,
  },
};

if (require.main === module) {
  try {
    if (process.argv.includes("--uninstall")) unregisterKiroHooks({});
    else registerKiroHooks({});
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }
}
