import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { eventNeedsToolMatcher, normalizeHookId, parseHooksConfig, renderHooksConfig, validateHooksConfig } from "../../hooks/config.js";
import { isHookEvent } from "../../hooks/events.js";
import { enabledPluginManifests } from "../../plugins/extensions.js";
import { safeOptionalJoin } from "../../tools/pathSafety.js";
export class HookService {
    projectPath;
    dataDir;
    constructor(projectPath, dataDir = projectPath) {
        this.projectPath = projectPath;
        this.dataDir = dataDir;
    }
    get configPath() {
        return path.join(this.projectPath, ".deepseekcode", "hooks.json");
    }
    list() {
        return [...this.readConfig().hooks, ...this.pluginHooks()];
    }
    add(input) {
        const id = normalizeHookId(input.id);
        if (!id)
            throw new Error("hook id is empty");
        if (!isHookEvent(input.event))
            throw new Error(`unknown hook event: ${input.event}`);
        const command = input.command.trim();
        if (!command)
            throw new Error("hook command is empty");
        const config = this.readConfig();
        if (config.hooks.some((hook) => hook.id === id))
            throw new Error(`hook already exists: ${id}`);
        const hook = {
            id,
            event: input.event,
            matcher: input.matcher?.trim() || undefined,
            command,
            description: input.description?.trim() || undefined,
            timeout_ms: input.timeoutMs ?? 10_000,
            enabled: true,
        };
        config.hooks.push(hook);
        this.writeConfig(config);
        return hook;
    }
    remove(id) {
        const config = this.readConfig();
        const before = config.hooks.length;
        config.hooks = config.hooks.filter((hook) => hook.id !== id);
        if (config.hooks.length === before)
            return false;
        this.writeConfig(config);
        return true;
    }
    validate() {
        return validateHooksConfig(this.configPath, fs.existsSync(this.configPath) ? fs.readFileSync(this.configPath, "utf8") : null);
    }
    async runEvent(event, payload, options = { allowShell: false }) {
        const hooks = this.list().filter((hook) => hook.enabled && hook.event === event && hookMatches(hook, payload));
        const results = [];
        for (const hook of hooks) {
            results.push(await runHookCommand(this.projectPath, hook, payload, {
                allowShell: options.allowShell,
                maxOutputChars: options.maxOutputChars ?? 8_000,
            }));
        }
        return results;
    }
    async runPreToolUse(payload, options = { allowShell: false }) {
        const results = await this.runEvent("PreToolUse", payload, options);
        const explicitBlock = results
            .map((result) => ({ result, decision: parseHookDecision(result.stdout) }))
            .find((item) => item.decision?.decision === "block" || item.decision?.decision === "deny");
        if (explicitBlock) {
            return {
                event: "PreToolUse",
                blocked: true,
                reason: explicitBlock.decision?.reason || `blocked by hook ${explicitBlock.result.id}`,
                results,
            };
        }
        const failed = results.find((result) => result.status === "failed");
        if (failed) {
            return {
                event: "PreToolUse",
                blocked: true,
                reason: failed.stderr.trim() || failed.stdout.trim() || `hook ${failed.id} failed`,
                results,
            };
        }
        return { event: "PreToolUse", blocked: false, results };
    }
    readConfig() {
        if (!fs.existsSync(this.configPath))
            return { hooks: [] };
        return parseHooksConfig(JSON.parse(fs.readFileSync(this.configPath, "utf8")));
    }
    writeConfig(config) {
        fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
        fs.writeFileSync(this.configPath, renderHooksConfig(config), "utf8");
    }
    pluginHooks() {
        const hooks = [];
        for (const plugin of enabledPluginManifests(this.projectPath, this.dataDir)) {
            for (const hook of plugin.manifest?.hooks ?? []) {
                hooks.push({
                    ...hook,
                    id: `${plugin.name}:${normalizeHookId(hook.id)}`,
                    description: hook.description ?? `Hook from plugin ${plugin.name}`,
                });
            }
        }
        return hooks;
    }
}
function parseHookDecision(stdout) {
    const trimmed = stdout.trim();
    if (!trimmed.startsWith("{"))
        return null;
    try {
        const parsed = JSON.parse(trimmed);
        return {
            decision: typeof parsed.decision === "string" ? parsed.decision.toLowerCase() : undefined,
            reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
        };
    }
    catch {
        return null;
    }
}
function hookMatches(hook, payload) {
    if (!eventNeedsToolMatcher(hook.event) || !hook.matcher)
        return true;
    const toolName = String(payload.tool_name ?? "");
    return hook.matcher === "*" || hook.matcher === toolName;
}
function runHookCommand(root, hook, payload, options) {
    if (!options.allowShell) {
        return Promise.resolve({
            id: hook.id,
            event: hook.event,
            matcher: hook.matcher,
            command: hook.command,
            status: "skipped",
            exitCode: null,
            stdout: "",
            stderr: "",
            timedOut: false,
            message: "shell execution is disabled; enable it with /shell on",
        });
    }
    return new Promise((resolve, reject) => {
        const child = spawn(hook.command, {
            cwd: safeOptionalJoin(root, ""),
            shell: true,
            windowsHide: true,
            stdio: ["pipe", "pipe", "pipe"],
        });
        let stdout = "";
        let stderr = "";
        let timedOut = false;
        const collect = (chunk, current) => (current + chunk.toString()).slice(-options.maxOutputChars);
        child.stdout.on("data", (chunk) => {
            stdout = collect(chunk, stdout);
        });
        child.stderr.on("data", (chunk) => {
            stderr = collect(chunk, stderr);
        });
        child.on("error", reject);
        const timer = setTimeout(() => {
            timedOut = true;
            child.kill();
        }, hook.timeout_ms);
        child.stdin.end(`${JSON.stringify({
            hook_id: hook.id,
            event: hook.event,
            matcher: hook.matcher,
            payload,
        })}\n`);
        child.on("close", (exitCode) => {
            clearTimeout(timer);
            resolve({
                id: hook.id,
                event: hook.event,
                matcher: hook.matcher,
                command: hook.command,
                status: exitCode === 0 && !timedOut ? "succeeded" : "failed",
                exitCode,
                stdout,
                stderr,
                timedOut,
            });
        });
    });
}
