/**
 * Hook Execution Services
 *
 * Provides hook command execution functionality and hook-specific configuration loading.
 * This module focuses on hook execution while delegating general Wave configuration
 * management to ConfigurationService.
 */
import { spawn } from "child_process";
import { generateSessionFilePath } from "./session.js";
// =============================================================================
// Hook Execution Functions
// =============================================================================
/**
 * Build JSON input data for hook stdin
 */
async function buildHookJsonInput(context) {
    const workdir = context.cwd || context.projectDir || process.cwd();
    let transcriptPath = context.transcriptPath;
    if (!transcriptPath && context.sessionId) {
        try {
            transcriptPath = await generateSessionFilePath(context.sessionId, workdir);
        }
        catch {
            transcriptPath = "";
        }
    }
    const jsonInput = {
        session_id: context.sessionId || "unknown",
        transcript_path: transcriptPath || "",
        cwd: workdir,
        hook_event_name: context.event,
    };
    // Add optional fields based on event type
    if (context.event === "PreToolUse" ||
        context.event === "PostToolUse" ||
        context.event === "PermissionRequest") {
        if (context.toolName) {
            jsonInput.tool_name = context.toolName;
        }
        if (context.toolInput !== undefined) {
            jsonInput.tool_input = context.toolInput;
        }
    }
    if (context.event === "PostToolUse" && context.toolResponse !== undefined) {
        jsonInput.tool_response = context.toolResponse;
    }
    if (context.event === "UserPromptSubmit" &&
        context.userPrompt !== undefined) {
        jsonInput.user_prompt = context.userPrompt;
    }
    // Add subagent_type if present
    if (context.subagentType !== undefined) {
        jsonInput.subagent_type = context.subagentType;
    }
    // Add name field for WorktreeCreate events
    if (context.event === "WorktreeCreate") {
        if (context.worktreeName !== undefined) {
            jsonInput.name = context.worktreeName;
        }
    }
    return jsonInput;
}
/**
 * Execute a single hook command
 */
export async function executeCommand(command, context, options) {
    const defaultTimeout = 600000; // 10 minutes
    const maxTimeout = 600000; // 10 minutes
    const skipExecution = process.env.NODE_ENV === "test" &&
        process.env.TEST_HOOK_EXECUTION !== "true";
    const startTime = Date.now();
    const timeout = Math.min(options?.timeout ?? defaultTimeout, maxTimeout);
    // Return mock result if execution is skipped
    if (skipExecution) {
        return {
            success: true,
            exitCode: 0,
            stdout: "",
            stderr: "",
            duration: 0,
            timedOut: false,
        };
    }
    // Prepare JSON input for hooks that need it
    let jsonInput = null;
    if ("sessionId" in context) {
        try {
            const hookJsonInput = await buildHookJsonInput(context);
            jsonInput = JSON.stringify(hookJsonInput, null, 2);
        }
        catch {
            // Continue execution even if JSON input preparation fails
        }
    }
    return new Promise((resolve) => {
        let stdout = "";
        let stderr = "";
        let timedOut = false;
        // Parse command for shell execution
        const isWindows = process.platform === "win32";
        const shell = isWindows ? "cmd.exe" : "/bin/sh";
        const shellFlag = isWindows ? "/c" : "-c";
        const childProcess = spawn(shell, [shellFlag, command], {
            stdio: ["pipe", "pipe", "pipe"],
            cwd: context.projectDir,
            env: {
                ...process.env, // Environment variables from process.env
                ...("env" in context ? context.env || {} : {}), // Additional environment variables from configuration (if ExtendedHookExecutionContext)
                HOOK_EVENT: context.event,
                HOOK_TOOL_NAME: context.toolName || "",
                WAVE_PROJECT_DIR: context.projectDir,
            },
        });
        // Handle stdin errors (e.g. EPIPE if the process closes stdin early)
        if (childProcess.stdin) {
            childProcess.stdin.on("error", () => {
                // Ignore stdin errors as they just mean the process closed stdin early
                // or doesn't want to read from it.
            });
        }
        // Set up timeout
        let forceKillTimeoutHandle;
        const timeoutHandle = setTimeout(() => {
            timedOut = true;
            childProcess.kill("SIGTERM");
            // Force kill after additional delay
            forceKillTimeoutHandle = setTimeout(() => {
                if (!childProcess.killed) {
                    childProcess.kill("SIGKILL");
                }
            }, 2000);
        }, timeout);
        // Handle stdout
        if (childProcess.stdout) {
            childProcess.stdout.on("data", (data) => {
                stdout += data.toString();
            });
            childProcess.stdout.on("error", () => {
                // Ignore stdout errors
            });
        }
        // Handle stderr
        if (childProcess.stderr) {
            childProcess.stderr.on("data", (data) => {
                stderr += data.toString();
            });
            childProcess.stderr.on("error", () => {
                // Ignore stderr errors
            });
        }
        // Send JSON input to stdin if we have prepared it
        if (childProcess.stdin && childProcess.stdin.writable && jsonInput) {
            try {
                childProcess.stdin.write(jsonInput);
                childProcess.stdin.end();
            }
            catch {
                // Continue execution even if JSON input fails
            }
        }
        else if (childProcess.stdin && childProcess.stdin.writable) {
            childProcess.stdin.end();
        }
        // Handle process completion
        childProcess.on("close", (code) => {
            clearTimeout(timeoutHandle);
            if (forceKillTimeoutHandle)
                clearTimeout(forceKillTimeoutHandle);
            const duration = Date.now() - startTime;
            resolve({
                success: !timedOut && (code === 0 || code === null),
                exitCode: code || 0,
                stdout: stdout.trim(),
                stderr: stderr.trim(),
                duration,
                timedOut,
            });
        });
        // Handle process errors
        childProcess.on("error", (error) => {
            clearTimeout(timeoutHandle);
            if (forceKillTimeoutHandle)
                clearTimeout(forceKillTimeoutHandle);
            const duration = Date.now() - startTime;
            resolve({
                success: false,
                exitCode: 1,
                stdout: stdout.trim(),
                stderr: error.message,
                duration,
                timedOut,
            });
        });
    });
}
/**
 * Execute multiple commands in sequence
 */
export async function executeCommands(commands, context, options) {
    const results = [];
    for (const command of commands) {
        const result = await executeCommand(command, context, options);
        results.push(result);
        // Stop on first failure unless continueOnFailure is set
        if (!result.success && !options?.continueOnFailure) {
            break;
        }
    }
    return results;
}
/**
 * Validate command safety (basic checks)
 */
export function isCommandSafe(command) {
    const trimmed = command.trim();
    // Empty commands are safe (no-op)
    if (!trimmed) {
        return true;
    }
    // Check for obviously dangerous patterns
    const dangerousPatterns = [
        /rm\s+-rf\s+\//, // rm -rf /
        /sudo\s+rm/, // sudo rm
        />\s*\/dev\/sd[a-z]/, // writing to disk devices
        /dd\s+if=.*of=\/dev/, // dd to devices
        /mkfs/, // filesystem creation
        /fdisk/, // disk partitioning
        /format\s+[a-z]:/, // Windows format command
    ];
    return !dangerousPatterns.some((pattern) => pattern.test(trimmed.toLowerCase()));
}
