/**
 * ============================================================
 *  src/plugins/hookEngine.ts —— Claude Code 风格 hook 派发引擎
 * ------------------------------------------------------------
 *  统一加载所有插件目录下的 hooks/hooks.json，按事件名建表，
 *  在 minimal-agent 的关键节点（Stop / PreToolUse / PostToolUse /
 *  UserPromptSubmit / SessionStart / SessionEnd / PreCompact /
 *  Notification / SubagentStop）触发对应的 shell 命令。
 *
 *  契约（与 Claude Code 对齐）：
 *    - hook 命令通过 stdin 收到 JSON payload（包含 session_id、
 *      transcript_path、cwd、hook_event_name 等基础字段 + 事件特定字段）
 *    - 命令在 stdout 输出 JSON：
 *        { decision: "block" | "approve" | "pass",
 *          reason?, systemMessage?, output?, suppressOutput? }
 *    - 解析失败 / 非 0 退出 / 空输出 → 视为 pass
 *    - 多 hook 串行执行，遇到第一个 block 立即短路返回
 *
 *  Windows 兼容：
 *    - 以 .sh 结尾的命令直接跳过，并 warn 一次（bash 通常不可用）
 *    - 其他命令（.ps1 / .cmd / .bat / 可执行文件）走 shell:true
 *    - POSIX 上一律 `bash -c <command>`，避免依赖文件可执行位
 *
 *  Matcher 支持（PreToolUse / PostToolUse）：
 *    - 在 matcher 组级别声明 `matcher: 'Bash|Edit'` 等正则
 *    - payload.tool_name 不匹配则跳过这组所有命令
 *    - 缺省 matcher 视为通配
 * ============================================================
 */
import { spawn } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
const HOOK_EVENT_NAMES = new Set([
    'PreToolUse',
    'PostToolUse',
    'Stop',
    'SubagentStop',
    'UserPromptSubmit',
    'SessionStart',
    'SessionEnd',
    'PreCompact',
    'Notification',
]);
const HOOK_TIMEOUT_MS = 30_000;
const hookTable = new Map();
let warnedWindowsShellSkip = false;
function pluginNameFromRoot(pluginRoot) {
    const norm = pluginRoot.replace(/\\/g, '/');
    const parts = norm.split('/').filter(Boolean);
    return parts[parts.length - 1] ?? pluginRoot;
}
async function loadOnePluginHooks(pluginRoot) {
    const hooksJsonPath = join(pluginRoot, 'hooks', 'hooks.json');
    let parsed;
    try {
        const raw = await readFile(hooksJsonPath, 'utf8');
        parsed = JSON.parse(raw);
    }
    catch {
        return;
    }
    if (!parsed || typeof parsed !== 'object')
        return;
    const hooksObj = parsed.hooks;
    if (!hooksObj || typeof hooksObj !== 'object')
        return;
    const pluginName = pluginNameFromRoot(pluginRoot);
    for (const [eventKey, groups] of Object.entries(hooksObj)) {
        if (!HOOK_EVENT_NAMES.has(eventKey))
            continue;
        if (!Array.isArray(groups))
            continue;
        const eventName = eventKey;
        for (const group of groups) {
            if (!group || typeof group !== 'object')
                continue;
            const matcher = typeof group.matcher === 'string'
                ? (group.matcher)
                : undefined;
            const innerHooks = group.hooks;
            if (!Array.isArray(innerHooks))
                continue;
            for (const h of innerHooks) {
                if (!h || typeof h !== 'object')
                    continue;
                const type = h.type;
                const command = h.command;
                if (type !== 'command' || typeof command !== 'string' || !command)
                    continue;
                const resolved = command.replaceAll('${CLAUDE_PLUGIN_ROOT}', pluginRoot);
                const entry = {
                    command: resolved,
                    pluginRoot,
                    pluginName,
                    matcher,
                };
                const list = hookTable.get(eventName) ?? [];
                list.push(entry);
                hookTable.set(eventName, list);
            }
        }
    }
}
/** 加载所有插件目录的 hooks.json，按事件名建表（模块级缓存）。 */
export async function loadHooks(pluginRoots) {
    hookTable.clear();
    for (const root of pluginRoots) {
        await loadOnePluginHooks(root);
    }
}
/** 重置 hook 缓存（测试用） */
export function _resetHookEngine() {
    hookTable.clear();
    warnedWindowsShellSkip = false;
}
function matcherAllows(matcher, toolName) {
    if (!matcher)
        return true;
    if (typeof toolName !== 'string' || !toolName)
        return true;
    try {
        const re = new RegExp(`^(${matcher})$`);
        return re.test(toolName);
    }
    catch {
        return true;
    }
}
function isPosixShellScript(command) {
    // 仅按文件后缀判断，命令字符串可能带参数，截取首段
    const head = command.trim().split(/\s+/)[0] ?? '';
    return head.toLowerCase().endsWith('.sh');
}
function runOneHook(hook, payload) {
    return new Promise((resolve) => {
        const isWin = process.platform === 'win32';
        if (isWin && isPosixShellScript(hook.command)) {
            if (!warnedWindowsShellSkip) {
                warnedWindowsShellSkip = true;
                console.warn(`[minimal-agent] Skipping POSIX shell hook on Windows: ${hook.command}`);
            }
            resolve({ decision: 'pass' });
            return;
        }
        const env = {
            ...process.env,
            CLAUDE_PLUGIN_ROOT: hook.pluginRoot,
        };
        let child;
        try {
            if (isWin) {
                // Windows 上其他类型脚本（.ps1 / .cmd / .bat / 可执行文件）走 shell
                child = spawn(hook.command, { env, shell: true });
            }
            else {
                // POSIX 一律 bash -c，避开文件可执行位 / shebang 问题
                child = spawn('bash', ['-c', hook.command], { env });
            }
        }
        catch {
            resolve({ decision: 'pass' });
            return;
        }
        let stdout = '';
        let settled = false;
        const finalize = (decision) => {
            if (settled)
                return;
            settled = true;
            clearTimeout(timer);
            resolve(decision);
        };
        const timer = setTimeout(() => {
            try {
                child.kill('SIGTERM');
            }
            catch {
                // ignore
            }
            console.warn(`[minimal-agent] Hook timeout (>${HOOK_TIMEOUT_MS}ms): ${hook.command}`);
            finalize({ decision: 'pass' });
        }, HOOK_TIMEOUT_MS);
        child.stdout?.on('data', (data) => {
            stdout += data.toString();
        });
        child.on('error', () => {
            finalize({ decision: 'pass' });
        });
        child.on('close', (code) => {
            if (code !== 0) {
                finalize({ decision: 'pass' });
                return;
            }
            const trimmed = stdout.trim();
            if (!trimmed) {
                finalize({ decision: 'pass' });
                return;
            }
            try {
                const parsed = JSON.parse(trimmed);
                const decision = parsed.decision;
                if (decision === 'block' || decision === 'approve') {
                    const result = {
                        decision,
                        reason: typeof parsed.reason === 'string' ? parsed.reason : undefined,
                        systemMessage: typeof parsed.systemMessage === 'string' ? parsed.systemMessage : undefined,
                        output: typeof parsed.output === 'string' ? parsed.output : undefined,
                        suppressOutput: typeof parsed.suppressOutput === 'boolean' ? parsed.suppressOutput : undefined,
                    };
                    if (decision === 'block') {
                        result.blockedBy = hook.pluginName;
                    }
                    finalize(result);
                    return;
                }
                finalize({ decision: 'pass' });
            }
            catch {
                finalize({ decision: 'pass' });
            }
        });
        try {
            child.stdin?.write(JSON.stringify(payload));
            child.stdin?.end();
        }
        catch {
            // 子进程已挂掉等情况，等 'close' / 'error' 处理
        }
    });
}
/**
 * 触发某事件。payload 会与 base 字段合并写入 stdin。
 * 多 hook 同事件 → 按声明顺序串行执行，遇到 block 立即短路返回。
 * Windows 上 .sh 命令直接跳过 + 一次性 warn。
 */
export async function triggerHook(eventName, payload) {
    const hooks = hookTable.get(eventName);
    if (!hooks || hooks.length === 0) {
        return { decision: 'pass' };
    }
    const fullPayload = {
        ...payload,
        hook_event_name: eventName,
    };
    let lastDecision = { decision: 'pass' };
    for (const hook of hooks) {
        if (eventName === 'PreToolUse' || eventName === 'PostToolUse') {
            if (!matcherAllows(hook.matcher, fullPayload.tool_name))
                continue;
        }
        const result = await runOneHook(hook, fullPayload);
        if (result.decision === 'block') {
            return result;
        }
        lastDecision = result;
    }
    return lastDecision;
}
