/**
 * Hook Manager
 *
 * Central orchestrator for the hooks system. Handles configuration loading,
 * validation, and hook execution across all supported events.
 */
import { HookConfigurationError, isValidHookEvent, isValidHookEventConfig, } from "../types/hooks.js";
import { HookMatcher } from "../utils/hookMatcher.js";
import { executeCommand, isCommandSafe } from "../services/hook.js";
import { MessageSource } from "../types/index.js";
import { logger } from "../utils/globalLogger.js";
export class HookManager {
    constructor(container, workdir, matcher = new HookMatcher()) {
        this.container = container;
        this.workdir = workdir;
        this.matcher = matcher;
    }
    /**
     * Load and merge hook configurations from user and project settings
     * Project settings take precedence over user settings
     */
    loadConfiguration(userHooks, projectHooks) {
        const merged = {};
        // Start with user hooks
        if (userHooks) {
            this.mergeHooksConfiguration(merged, userHooks);
        }
        // Override with project hooks (project settings take precedence)
        if (projectHooks) {
            this.mergeHooksConfiguration(merged, projectHooks);
        }
        // Validate merged configuration
        const validation = this.validatePartialConfiguration(merged);
        if (!validation.valid) {
            throw new HookConfigurationError("merged configuration", validation.errors);
        }
        this.configuration = merged;
    }
    /**
     * Load hooks configuration from a pre-loaded WaveConfiguration
     * Configuration loading is now handled by ConfigurationService
     */
    loadConfigurationFromWaveConfig(waveConfig) {
        try {
            this.configuration = waveConfig?.hooks || undefined;
            // Validate the loaded configuration if it exists
            if (waveConfig?.hooks) {
                const validation = this.validatePartialConfiguration(waveConfig.hooks);
                if (!validation.valid) {
                    throw new HookConfigurationError("provided configuration", validation.errors);
                }
            }
        }
        catch (error) {
            // If loading fails, start with undefined configuration (no hooks)
            this.configuration = undefined;
            // Re-throw configuration errors, but handle other errors gracefully
            if (error instanceof HookConfigurationError) {
                throw error;
            }
            else {
                logger?.warn(`[HookManager] Failed to load configuration, continuing with no hooks: ${error.message}`);
            }
        }
    }
    /**
     * Execute hooks for a specific event
     */
    async executeHooks(event, context) {
        // Validate execution context
        const contextValidation = this.validateExecutionContext(event, context);
        if (!contextValidation.valid) {
            logger?.error(`[HookManager] Invalid execution context for ${event}: ${contextValidation.errors.join(", ")}`);
            return [
                {
                    success: false,
                    stderr: `Invalid execution context: ${contextValidation.errors.join(", ")}`,
                    duration: 0,
                    timedOut: false,
                },
            ];
        }
        if (!this.configuration) {
            return [];
        }
        const eventConfigs = this.configuration[event];
        if (!eventConfigs || eventConfigs.length === 0) {
            return [];
        }
        const results = [];
        for (let configIndex = 0; configIndex < eventConfigs.length; configIndex++) {
            const config = eventConfigs[configIndex];
            // Check if this config applies to the current context
            if (!this.configApplies(config, event, context.toolName)) {
                continue;
            }
            // Execute all commands for this configuration
            for (let commandIndex = 0; commandIndex < config.hooks.length; commandIndex++) {
                const hookCommand = config.hooks[commandIndex];
                try {
                    const options = hookCommand.timeout
                        ? { timeout: hookCommand.timeout * 1000 }
                        : undefined;
                    if (hookCommand.async) {
                        // Execute async command without awaiting
                        executeCommand(hookCommand.command, context, options).catch((error) => {
                            const errorMessage = error instanceof Error
                                ? error.message
                                : "Unknown execution error";
                            logger?.error(`[HookManager] Async hook command ${commandIndex + 1} failed: ${errorMessage}`);
                        });
                        // Async hooks are not included in results to prevent blocking
                        continue;
                    }
                    const result = await executeCommand(hookCommand.command, context, options);
                    results.push(result);
                    // Continue with next command even if this one fails
                    // This allows for non-critical hooks to fail without stopping the workflow
                }
                catch (error) {
                    // This should be rare as executor handles most errors
                    const errorMessage = error instanceof Error ? error.message : "Unknown execution error";
                    logger?.error(`[HookManager] Unexpected error in command ${commandIndex + 1}: ${errorMessage}`);
                    results.push({
                        success: false,
                        stderr: errorMessage,
                        duration: 0,
                        timedOut: false,
                    });
                }
            }
        }
        return results;
    }
    /**
     * Process hook execution results and determine appropriate actions
     * based on exit codes and hook event type
     */
    processHookResults(event, results, messageManager, toolId, toolParameters) {
        if (!messageManager || results.length === 0) {
            return { shouldBlock: false };
        }
        // First pass: Check for any blocking errors (exit code 2)
        // Blocking errors take precedence and stop all processing
        for (const result of results) {
            if (result.exitCode === 2) {
                // Handle blocking error immediately and return
                const blockingResult = this.handleBlockingError(event, result, messageManager, toolId, toolParameters);
                return {
                    shouldBlock: blockingResult.shouldBlock,
                    errorMessage: blockingResult.errorMessage,
                };
            }
        }
        // Second pass: Process all non-blocking results
        for (const result of results) {
            if (result.exitCode === undefined) {
                continue; // Skip results without exit codes
            }
            // Handle exit code interpretation
            if (result.exitCode === 0) {
                // Success case - handle stdout based on hook type
                this.handleHookSuccess(event, result, messageManager);
            }
            else {
                // Non-blocking error case (any exit code except 0 and 2)
                this.handleNonBlockingError(result, messageManager);
            }
        }
        return { shouldBlock: false };
    }
    /**
     * Handle successful hook execution (exit code 0)
     */
    handleHookSuccess(event, result, messageManager) {
        if (event === "UserPromptSubmit" && result.stdout?.trim()) {
            // Inject stdout as user message context for UserPromptSubmit
            messageManager.addUserMessage({
                content: result.stdout.trim(),
                source: MessageSource.HOOK,
            });
        }
        // For other hook types (PreToolUse, PostToolUse, Stop, PermissionRequest), ignore stdout
    }
    /**
     * Handle blocking error (exit code 2) - behavior varies by hook type
     */
    handleBlockingError(event, result, messageManager, toolId, toolParameters) {
        const errorMessage = result.stderr?.trim() || "Hook execution failed";
        switch (event) {
            case "UserPromptSubmit":
                // Block prompt processing, show error to user, erase prompt
                messageManager.addErrorBlock(errorMessage);
                messageManager.removeLastUserMessage();
                return {
                    shouldBlock: true,
                    errorMessage,
                };
            case "PreToolUse":
                // Block tool execution and show error to Wave Agent via tool block
                if (toolId) {
                    messageManager.updateToolBlock({
                        id: toolId,
                        parameters: toolParameters || "",
                        result: errorMessage,
                        success: false,
                        error: "Hook blocked tool execution",
                        stage: "end", // Hook blocking results in end stage with error
                    });
                }
                return { shouldBlock: true };
            case "PostToolUse":
                // Show error to Wave Agent via user message and allow AI to continue
                messageManager.addUserMessage({
                    content: errorMessage,
                    source: MessageSource.HOOK,
                });
                return { shouldBlock: false };
            case "Stop":
                // Show error to Wave Agent via user message and block stopping to continue conversation
                messageManager.addUserMessage({
                    content: errorMessage,
                    source: MessageSource.HOOK,
                });
                return { shouldBlock: true, errorMessage };
            case "PermissionRequest":
                // For permission request hooks with exit code 2, show stderr in error block and block (deny) permission
                messageManager.addErrorBlock(errorMessage);
                return { shouldBlock: true, errorMessage };
            case "SubagentStop":
                // Similar to Stop, show error and allow blocking
                messageManager.addUserMessage({
                    content: errorMessage,
                    source: MessageSource.HOOK,
                });
                return { shouldBlock: true, errorMessage };
            case "WorktreeCreate":
                // Non-blocking for now, just show error in error block
                messageManager.addErrorBlock(errorMessage);
                return { shouldBlock: false };
            default:
                return { shouldBlock: false };
        }
    }
    /**
     * Handle non-blocking error (other exit codes)
     */
    handleNonBlockingError(result, messageManager) {
        const errorMessage = result.stderr?.trim() || "Hook execution failed";
        messageManager.addErrorBlock(errorMessage);
    }
    /**
     * Check if hooks are configured for an event/tool combination
     */
    hasHooks(event, toolName) {
        if (!this.configuration)
            return false;
        const eventConfigs = this.configuration[event];
        if (!eventConfigs || eventConfigs.length === 0)
            return false;
        return eventConfigs.some((config) => this.configApplies(config, event, toolName));
    }
    /**
     * Validate Wave configuration structure and content
     */
    validateConfiguration(config) {
        const errors = [];
        if (!config || typeof config !== "object") {
            return { valid: false, errors: ["Configuration must be an object"] };
        }
        // Validate hooks if present
        if (config.hooks) {
            if (typeof config.hooks !== "object") {
                errors.push("hooks property must be an object");
            }
            else {
                // Validate each hook event
                for (const [eventName, eventConfigs] of Object.entries(config.hooks)) {
                    // Validate event name
                    if (!isValidHookEvent(eventName)) {
                        errors.push(`Invalid hook event: ${eventName}`);
                        continue;
                    }
                    // Validate event configurations
                    if (!Array.isArray(eventConfigs)) {
                        errors.push(`Hook event ${eventName} must be an array of configurations`);
                        continue;
                    }
                    eventConfigs.forEach((eventConfig, index) => {
                        const configErrors = this.validateEventConfig(eventName, eventConfig, index);
                        errors.push(...configErrors);
                    });
                }
            }
        }
        // Validate environment variables if present
        if (config.env) {
            if (typeof config.env !== "object" || Array.isArray(config.env)) {
                errors.push("env property must be an object");
            }
            else {
                for (const [key, value] of Object.entries(config.env)) {
                    if (typeof key !== "string" || key.trim() === "") {
                        errors.push(`Invalid environment variable key: ${key}`);
                    }
                    if (typeof value !== "string") {
                        errors.push(`Environment variable ${key} must have a string value`);
                    }
                }
            }
        }
        return {
            valid: errors.length === 0,
            errors,
        };
    }
    /**
     * Validate partial hook configuration structure and content
     */
    validatePartialConfiguration(config) {
        const errors = [];
        if (!config || typeof config !== "object") {
            return { valid: false, errors: ["Configuration must be an object"] };
        }
        // Validate each hook event that is present
        for (const [eventName, eventConfigs] of Object.entries(config)) {
            // Validate event name
            if (!isValidHookEvent(eventName)) {
                errors.push(`Invalid hook event: ${eventName}`);
                continue;
            }
            // Validate event configurations
            if (!Array.isArray(eventConfigs)) {
                errors.push(`Hook event ${eventName} must be an array of configurations`);
                continue;
            }
            eventConfigs.forEach((eventConfig, index) => {
                const configErrors = this.validateEventConfig(eventName, eventConfig, index);
                errors.push(...configErrors);
            });
        }
        return {
            valid: errors.length === 0,
            errors,
        };
    }
    /**
     * Get current configuration
     */
    getConfiguration() {
        if (!this.configuration)
            return undefined;
        // Deep clone to prevent external modification
        return JSON.parse(JSON.stringify(this.configuration));
    }
    /**
     * Clear current configuration
     */
    clearConfiguration() {
        this.configuration = undefined;
    }
    /**
     * Validate execution context for a specific event
     */
    validateExecutionContext(event, context) {
        const errors = [];
        // Validate basic context structure
        if (!context || typeof context !== "object") {
            return { valid: false, errors: ["Context must be an object"] };
        }
        // Warn about event mismatch but don't fail validation
        if (context.event !== event) {
            logger?.warn(`[HookManager] Context event '${context.event}' does not match requested event '${event}'`);
        }
        // Validate project directory
        if (!context.projectDir || typeof context.projectDir !== "string") {
            errors.push("Context must have a valid projectDir string");
        }
        // Validate timestamp
        if (!context.timestamp || !(context.timestamp instanceof Date)) {
            errors.push("Context must have a valid timestamp Date object");
        }
        // Validate tool-specific requirements
        if (event === "PreToolUse" ||
            event === "PostToolUse" ||
            event === "PermissionRequest") {
            if (!context.toolName || typeof context.toolName !== "string") {
                errors.push(`${event} event requires a valid toolName in context`);
            }
        }
        // Validate non-tool events don't have unexpected tool names
        if ((event === "UserPromptSubmit" ||
            event === "Stop" ||
            event === "SubagentStop" ||
            event === "WorktreeCreate") &&
            context.toolName !== undefined) {
            logger?.warn(`[HookManager] ${event} event has unexpected toolName in context: ${context.toolName}`);
        }
        return {
            valid: errors.length === 0,
            errors,
        };
    }
    /**
     * Generate a summary of hook execution results
     */
    generateExecutionSummary(event, results, totalDuration) {
        if (results.length === 0) {
            return `No hooks executed for ${event}`;
        }
        const successful = results.filter((r) => r.success).length;
        const failed = results.length - successful;
        const timedOut = results.filter((r) => r.timedOut).length;
        const avgDuration = results.reduce((sum, r) => sum + r.duration, 0) / results.length;
        let summary = `${successful}/${results.length} commands successful`;
        if (failed > 0) {
            summary += `, ${failed} failed`;
        }
        if (timedOut > 0) {
            summary += `, ${timedOut} timed out`;
        }
        summary += ` (avg: ${Math.round(avgDuration)}ms, total: ${totalDuration}ms)`;
        return summary;
    }
    /**
     * Merge hook configurations, with the second taking precedence
     */
    mergeHooksConfiguration(target, source) {
        for (const [event, configs] of Object.entries(source)) {
            if (isValidHookEvent(event)) {
                // For now, completely replace event configs rather than merging
                // This ensures project settings completely override user settings for each event
                target[event] = [...configs];
            }
        }
    }
    /**
     * Check if a hook configuration applies to the current context
     */
    configApplies(config, event, toolName) {
        // For events that don't use matchers, config always applies
        if (event === "UserPromptSubmit" ||
            event === "Stop" ||
            event === "SubagentStop" ||
            event === "WorktreeCreate") {
            return true;
        }
        // For tool-based events, check matcher if present
        if (event === "PreToolUse" ||
            event === "PostToolUse" ||
            event === "PermissionRequest") {
            if (!config.matcher) {
                // No matcher means applies to all tools
                return true;
            }
            if (!toolName) {
                // No tool name provided, cannot match
                return false;
            }
            return this.matcher.matches(config.matcher, toolName);
        }
        return false;
    }
    /**
     * Validate a single event configuration
     */
    validateEventConfig(event, config, index) {
        const errors = [];
        const prefix = `Hook event ${event}[${index}]`;
        if (!isValidHookEventConfig(config)) {
            errors.push(`${prefix}: Invalid hook event configuration structure`);
            return errors;
        }
        // Validate matcher requirements
        if ((event === "PreToolUse" ||
            event === "PostToolUse" ||
            event === "PermissionRequest") &&
            config.matcher) {
            if (!this.matcher.isValidPattern(config.matcher)) {
                errors.push(`${prefix}: Invalid matcher pattern: ${config.matcher}`);
            }
        }
        // Validate that non-tool events don't have matchers
        if ((event === "UserPromptSubmit" ||
            event === "Stop" ||
            event === "SubagentStop" ||
            event === "WorktreeCreate") &&
            config.matcher) {
            errors.push(`${prefix}: Event ${event} should not have a matcher`);
        }
        // Validate commands
        config.hooks.forEach((hookCommand, cmdIndex) => {
            if (!isCommandSafe(hookCommand.command)) {
                errors.push(`${prefix}.hooks[${cmdIndex}]: Command may be unsafe: ${hookCommand.command}`);
            }
        });
        return errors;
    }
    /**
     * Get statistics about current configuration
     */
    getConfigurationStats() {
        if (!this.configuration) {
            return {
                totalEvents: 0,
                totalConfigs: 0,
                totalCommands: 0,
                eventBreakdown: {
                    PreToolUse: 0,
                    PostToolUse: 0,
                    UserPromptSubmit: 0,
                    Stop: 0,
                    SubagentStop: 0,
                    PermissionRequest: 0,
                    WorktreeCreate: 0,
                },
            };
        }
        const eventBreakdown = {
            PreToolUse: 0,
            PostToolUse: 0,
            UserPromptSubmit: 0,
            Stop: 0,
            SubagentStop: 0,
            PermissionRequest: 0,
            WorktreeCreate: 0,
        };
        let totalConfigs = 0;
        let totalCommands = 0;
        Object.entries(this.configuration).forEach(([event, configs]) => {
            if (isValidHookEvent(event)) {
                eventBreakdown[event] = configs.length;
                totalConfigs += configs.length;
                totalCommands += configs.reduce((sum, config) => sum + config.hooks.length, 0);
            }
        });
        return {
            totalEvents: Object.keys(this.configuration).length,
            totalConfigs,
            totalCommands,
            eventBreakdown,
        };
    }
    /**
     * Register hooks provided by a plugin
     */
    registerPluginHooks(hooks) {
        if (!this.configuration) {
            this.configuration = {};
        }
        this.mergeHooksConfiguration(this.configuration, hooks);
    }
}
