/**
 * hook-installer.ts
 *
 * 把 botmux 的 askUserQuestion hook 写入各 CLI 的配置文件。
 * 幂等：写前比对内容，相同则跳过；展开 ~ 路径；出错只 warn 不抛。
 */
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
import { atomicWriteFileSync } from '../utils/atomic-write.js';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';
import { logger } from '../utils/logger.js';
import { hookCommandParts } from './hook-command.js';
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
/** 展开路径中的 ~ 为当前用户 home 目录。 */
function expandHome(p) {
    return p.startsWith('~') ? join(homedir(), p.slice(1)) : p;
}
/** 读 JSON 文件，失败返回 null。 */
function readJsonFile(filePath) {
    try {
        const raw = readFileSync(filePath, 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
/** 幂等写文件：若内容与现有相同则跳过；自动创建目录。 */
function writeIfChanged(filePath, content) {
    try {
        if (existsSync(filePath)) {
            const existing = readFileSync(filePath, 'utf-8');
            if (existing === content)
                return false; // 内容相同，无需写入
        }
        mkdirSync(dirname(filePath), { recursive: true });
        // 原子写：目标是 ~/.claude/settings.json 这类被 CLI 并发读写的热配置，
        // 裸写半截会让并发读者拿到坏 JSON 再整文件覆写回来（cjadk 事故同类）。
        atomicWriteFileSync(filePath, content);
        return true;
    }
    catch (err) {
        throw new Error(`写入 ${filePath} 失败：${err.message}`);
    }
}
/**
 * 从完整 hookCommand 中提取 `hook <cliId>` 尾签名。
 * hookCommand 形如：`"<node>" "<...dist/cli.js>" hook claude-code`，
 * 尾部 `hook <cliId>` 不随 node / cli.js 安装路径变化。
 */
function botmuxHookSuffix(hookCommand) {
    const idx = hookCommand.lastIndexOf(' hook ');
    return idx === -1 ? hookCommand : hookCommand.slice(idx + 1); // "hook <cliId>"
}
/**
 * 判断某个 hook group 是否是 botmux ask hook（用于幂等替换）。
 *
 * 不能只按命令字符串完全相等比对：同一台机器上 dev 源码 checkout 与 npm global
 * 安装的 cli.js 绝对路径不同，命令字符串就不同，会导致两条 botmux hook 同时残留、
 * 同一次 AskUserQuestion 触发两次 → 飞书发出两张卡。
 * 因此结构化识别：命令引用了 botmux 的 `cli.js` 且尾部是相同的 `hook <cliId>` 签名，
 * 即视为 botmux hook，无论它指向哪个安装路径。
 */
function isBotmuxAskHookGroup(group, hookCommand) {
    const suffix = botmuxHookSuffix(hookCommand); // e.g. "hook claude-code"
    return group.hooks.some((e) => e.type === 'command' &&
        (e.command === hookCommand ||
            (e.command.includes('cli.js') && e.command.trimEnd().endsWith(suffix))));
}
function removeBotmuxAskHookGroups(hooks, eventName, hookCommand) {
    const existing = hooks[eventName] ?? [];
    const filtered = existing.filter((g) => !isBotmuxAskHookGroup(g, hookCommand));
    if (filtered.length === 0) {
        delete hooks[eventName];
    }
    else {
        hooks[eventName] = filtered;
    }
}
/**
 * 判断某 hook group 是否是 botmux SessionStart 就绪 hook（用于幂等替换）。
 * 同 ask hook：结构化识别（命令引用 botmux 的 cli.js 且尾部是 `session-ready`），
 * 不按完整字符串比对——dev checkout 与 npm global 的 cli.js 绝对路径不同。
 */
function isBotmuxReadyHookGroup(group) {
    return group.hooks.some((e) => e.type === 'command' &&
        e.command.includes('cli.js') &&
        e.command.trimEnd().endsWith('session-ready'));
}
function removeBotmuxReadyHookGroups(hooks, eventName) {
    const existing = hooks[eventName] ?? [];
    const filtered = existing.filter((g) => !isBotmuxReadyHookGroup(g));
    if (filtered.length === 0)
        delete hooks[eventName];
    else
        hooks[eventName] = filtered;
}
/**
 * 向 Claude settings.json 的 hooks.PreToolUse 合并 botmux ask hook entry。
 * AskUserQuestion 在 bypassPermissions 模式下不会经过 PermissionRequest，
 * 但 PreToolUse 仍会在工具执行前触发，因此这里必须挂 PreToolUse。
 * 保留其他事件和 entry，不破坏无关配置。
 *
 * 若提供 sessionStartCommand，再把 SessionStart「真就绪」hook 也写进全局 settings.json
 * （为 wrapperCli=`aiden x claude` 这类剥 --settings 的启动器提供就绪信号；原生 claude
 * 会同时收到进程级 --settings 那份，二者幂等无害）。
 */
function installClaudeSettings(configPath, hookCommand, sessionStartCommand) {
    const settings = readJsonFile(configPath) ?? {};
    const existingHooks = settings.hooks ?? {};
    // 构造 botmux PreToolUse hook group（只拦截 AskUserQuestion）
    const newEntry = { type: 'command', command: hookCommand, timeout: 86400 };
    const newGroup = { matcher: 'AskUserQuestion', hooks: [newEntry] };
    // 过滤掉旧的 botmux ask hook group（幂等 + 从 PermissionRequest 迁移到 PreToolUse）
    removeBotmuxAskHookGroups(existingHooks, 'PermissionRequest', hookCommand);
    removeBotmuxAskHookGroups(existingHooks, 'PreToolUse', hookCommand);
    existingHooks['PreToolUse'] = [...(existingHooks['PreToolUse'] ?? []), newGroup];
    // SessionStart 就绪 hook（幂等替换旧的 botmux 条目）
    if (sessionStartCommand) {
        removeBotmuxReadyHookGroups(existingHooks, 'SessionStart');
        existingHooks['SessionStart'] = [
            ...(existingHooks['SessionStart'] ?? []),
            { hooks: [{ type: 'command', command: sessionStartCommand }] },
        ];
    }
    settings.hooks = existingHooks;
    const content = JSON.stringify(settings, null, 2) + '\n';
    const changed = writeIfChanged(configPath, content);
    if (changed) {
        logger.info(`[hook] 已写入 Claude hook → ${configPath}`);
    }
    else {
        logger.info(`[hook] Claude hook 已是最新，跳过写入 → ${configPath}`);
    }
}
// ─── OpenCode plugin 格式 ─────────────────────────────────────────────────────
/**
 * 构造 botmux ask 的 OpenCode 插件内容。
 * 插件监听 question.asked 事件，将 payload JSON 写入 botmux hook opencode 的 stdin，
 * 读取 stdout 作为回答，返回给 OpenCode。
 *
 * TODO(dogfood): 校验 OpenCode 插件 question.asked API
 * OpenCode 插件 API 目前处于 dogfood 阶段，`question.asked` 钩子形状和返回值约定
 * 需要在真实会话中实测确认（stdin/stdout 格式、同步/异步、返回值结构等）。
 */
function buildOpenCodePlugin(parts) {
    // 用 argv 形式嵌入（不拼 shell 字符串、不 split）：含空格/引号的路径也不会被拆坏。
    const cmdLit = JSON.stringify(parts.cmd);
    const argsLit = JSON.stringify(parts.args);
    return `// botmux-ask opencode plugin
// 将 OpenCode 的 question.asked 事件转发到 botmux hook opencode。
// TODO(dogfood): 校验 OpenCode 插件 question.asked API
import { spawnSync } from "child_process";

export default {
  name: "botmux-ask",

  // question.asked: OpenCode 向用户提问时触发。
  // payload 形状待实测（见 TODO 注释）。
  // 返回值 { type:'answer', answers } 或 undefined（undefined = 由 OpenCode 自行处理）。
  "question.asked"(payload) {
    try {
      const input = JSON.stringify(payload);
      const result = spawnSync(${cmdLit}, ${argsLit}, {
        input,
        encoding: "utf-8",
        timeout: 86400000, // 24h，等待用户在飞书回答
      });
      // stdout 为空 = hook 客户端 passthrough（放行）→ 返回 undefined，OpenCode 原生处理。
      if (result.status === 0 && result.stdout && result.stdout.trim()) {
        return JSON.parse(result.stdout.trim());
      }
    } catch {
      // 任何失败都降级放行：返回 undefined 让 OpenCode 回退原生终端提问
    }
    return undefined;
  },
};
`;
}
/**
 * 写入 OpenCode 插件文件。幂等：内容相同则跳过。
 */
function installOpenCodePlugin(configPath, parts) {
    const content = buildOpenCodePlugin(parts);
    const changed = writeIfChanged(configPath, content);
    if (changed) {
        logger.info(`[hook] 已写入 OpenCode 插件 → ${configPath}`);
    }
    else {
        logger.info(`[hook] OpenCode 插件已是最新，跳过写入 → ${configPath}`);
    }
}
// ─── 主入口 ───────────────────────────────────────────────────────────────────
/**
 * 幂等地将 botmux ask hook 安装到指定 CLI 的配置文件。
 *
 * @param cliId - CLI 标识符（用于日志）
 * @param hookInstall - adapter 提供的安装描述（configPath + format）
 * @param hookCommand - botmux hook 子命令的完整调用字符串
 *                      例如："/usr/bin/node /path/to/cli.js hook claude-code"
 */
export function installHook(cliId, hookInstall, hookCommand) {
    try {
        const configPath = expandHome(hookInstall.configPath);
        switch (hookInstall.format) {
            case 'claude-settings':
                installClaudeSettings(configPath, hookCommand, hookInstall.sessionStartCommand);
                break;
            case 'opencode-plugin':
                // OpenCode 插件走 argv parts（spawnSync），不复用 shell 字符串，避免被 split 拆坏。
                installOpenCodePlugin(configPath, hookCommandParts(cliId));
                break;
            default: {
                // TypeScript exhaustiveness（编译时保障，运行时防御）
                const _exhaustive = hookInstall.format;
                logger.warn(`[hook] 未知 format：${_exhaustive}，跳过 ${cliId}`);
            }
        }
    }
    catch (err) {
        logger.warn(`[hook] install failed for ${cliId}: ${err instanceof Error ? err.message : String(err)}`);
    }
}
//# sourceMappingURL=hook-installer.js.map