/**
 * Tool Lifecycle Hooks System
 *
 * OpenClaw-inspired hook system for intercepting tool execution at multiple stages:
 * - before_tool_call: Modify parameters before execution
 * - after_tool_call: Process results after execution
 * - tool_result_persist: Transform results before transcript storage
 *
 * Features:
 * - Priority-based hook ordering
 * - Async hook execution
 * - Hook chaining with result passing
 * - Error isolation (one hook failure doesn't stop others)
 * - Plugin-prefixed hooks for tracking
 */
import { EventEmitter } from 'events';
import { logger } from '../../utils/logger.js';
/**
 * Default configuration
 */
export const DEFAULT_TOOL_HOOKS_CONFIG = {
    enabled: true,
    continueOnError: true,
    hookTimeoutMs: 5000,
    maxHooksPerStage: 50,
    enableMetrics: true,
};
// ============================================================================
// Tool Hooks Manager
// ============================================================================
/**
 * Manages tool lifecycle hooks
 */
export class ToolHooksManager extends EventEmitter {
    config;
    beforeHooks = new Map();
    afterHooks = new Map();
    persistHooks = new Map();
    errorHooks = new Map();
    timeoutHooks = new Map();
    deniedHooks = new Map();
    metrics = new Map();
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_TOOL_HOOKS_CONFIG, ...config };
    }
    // ==========================================================================
    // Hook Registration
    // ==========================================================================
    /**
     * Register a before_tool_call hook
     */
    registerBeforeHook(id, handler, options = {}) {
        this.registerHook(this.beforeHooks, 'before_tool_call', id, handler, options);
    }
    /**
     * Register an after_tool_call hook
     */
    registerAfterHook(id, handler, options = {}) {
        this.registerHook(this.afterHooks, 'after_tool_call', id, handler, options);
    }
    /**
     * Register a tool_result_persist hook (synchronous)
     */
    registerPersistHook(id, handler, options = {}) {
        this.registerHook(this.persistHooks, 'tool_result_persist', id, handler, options);
    }
    /**
     * Register a tool_error hook
     */
    registerErrorHook(id, handler, options = {}) {
        this.registerHook(this.errorHooks, 'tool_error', id, handler, options);
    }
    /**
     * Register a tool_timeout hook
     */
    registerTimeoutHook(id, handler, options = {}) {
        this.registerHook(this.timeoutHooks, 'tool_timeout', id, handler, options);
    }
    /**
     * Register a tool_denied hook
     */
    registerDeniedHook(id, handler, options = {}) {
        this.registerHook(this.deniedHooks, 'tool_denied', id, handler, options);
    }
    /**
     * Generic hook registration
     */
    registerHook(registry, stage, id, handler, options) {
        if (registry.size >= this.config.maxHooksPerStage) {
            throw new Error(`Maximum hooks (${this.config.maxHooksPerStage}) reached for stage ${stage}`);
        }
        const fullId = options.pluginId ? `plugin:${options.pluginId}:${id}` : id;
        registry.set(fullId, {
            id: fullId,
            name: options.name || id,
            priority: options.priority ?? 100,
            handler,
            pluginId: options.pluginId,
            enabled: true,
        });
        this.initMetrics(fullId);
        this.emit('hook:registered', stage, fullId);
        logger.debug(`Registered ${stage} hook: ${fullId}`, { priority: options.priority });
    }
    /**
     * Unregister a hook
     */
    unregisterHook(stage, id) {
        const registry = this.getRegistry(stage);
        if (registry.delete(id)) {
            this.emit('hook:unregistered', stage, id);
            return true;
        }
        return false;
    }
    /**
     * Unregister all hooks from a plugin
     */
    unregisterPluginHooks(pluginId) {
        let count = 0;
        const prefix = `plugin:${pluginId}:`;
        for (const registry of [
            this.beforeHooks,
            this.afterHooks,
            this.persistHooks,
            this.errorHooks,
            this.timeoutHooks,
            this.deniedHooks,
        ]) {
            for (const [id] of registry) {
                if (id.startsWith(prefix)) {
                    registry.delete(id);
                    count++;
                }
            }
        }
        return count;
    }
    /**
     * Enable/disable a hook
     */
    setHookEnabled(stage, id, enabled) {
        const registry = this.getRegistry(stage);
        const hook = registry.get(id);
        if (hook) {
            hook.enabled = enabled;
            return true;
        }
        return false;
    }
    // ==========================================================================
    // Hook Execution
    // ==========================================================================
    /**
     * Execute before_tool_call hooks
     * Returns modified context or original if no modifications
     */
    async executeBeforeHooks(context) {
        if (!this.config.enabled)
            return context;
        let currentContext = { ...context };
        const sortedHooks = this.getSortedHooks(this.beforeHooks);
        for (const hook of sortedHooks) {
            if (!hook.enabled)
                continue;
            try {
                const start = Date.now();
                const result = await this.executeWithTimeout(hook.handler(currentContext), hook.id);
                if (result) {
                    currentContext = { ...currentContext, ...result };
                }
                this.recordMetrics(hook.id, Date.now() - start, false);
                this.emit('hook:executed', 'before_tool_call', hook.id, Date.now() - start);
            }
            catch (error) {
                this.handleHookError('before_tool_call', hook.id, error);
                if (!this.config.continueOnError)
                    throw error;
            }
        }
        return currentContext;
    }
    /**
     * Execute after_tool_call hooks
     * Returns modified result or original if no modifications
     */
    async executeAfterHooks(context, result) {
        if (!this.config.enabled)
            return result;
        let currentResult = { ...result };
        const sortedHooks = this.getSortedHooks(this.afterHooks);
        for (const hook of sortedHooks) {
            if (!hook.enabled)
                continue;
            try {
                const start = Date.now();
                const hookResult = await this.executeWithTimeout(hook.handler(context, currentResult), hook.id);
                if (hookResult) {
                    currentResult = { ...currentResult, ...hookResult, modified: true };
                }
                this.recordMetrics(hook.id, Date.now() - start, false);
                this.emit('hook:executed', 'after_tool_call', hook.id, Date.now() - start);
            }
            catch (error) {
                this.handleHookError('after_tool_call', hook.id, error);
                if (!this.config.continueOnError)
                    throw error;
            }
        }
        return currentResult;
    }
    /**
     * Execute tool_result_persist hooks (synchronous)
     * Returns transformed result for transcript storage
     */
    executePersistHooks(context, result) {
        if (!this.config.enabled)
            return result;
        let currentResult = { ...result };
        const sortedHooks = this.getSortedHooks(this.persistHooks);
        for (const hook of sortedHooks) {
            if (!hook.enabled)
                continue;
            try {
                const start = Date.now();
                currentResult = hook.handler(context, currentResult);
                this.recordMetrics(hook.id, Date.now() - start, false);
            }
            catch (error) {
                this.handleHookError('tool_result_persist', hook.id, error);
                if (!this.config.continueOnError)
                    throw error;
            }
        }
        return currentResult;
    }
    /**
     * Execute tool_error hooks
     */
    async executeErrorHooks(context, error) {
        await this.executeNotificationHooks(this.errorHooks, 'tool_error', context, error);
    }
    /**
     * Execute tool_timeout hooks
     */
    async executeTimeoutHooks(context, error) {
        await this.executeNotificationHooks(this.timeoutHooks, 'tool_timeout', context, error);
    }
    /**
     * Execute tool_denied hooks
     */
    async executeDeniedHooks(context, error) {
        await this.executeNotificationHooks(this.deniedHooks, 'tool_denied', context, error);
    }
    /**
     * Execute notification-style hooks (error, timeout, denied)
     */
    async executeNotificationHooks(registry, stage, context, error) {
        if (!this.config.enabled)
            return;
        const sortedHooks = this.getSortedHooks(registry);
        for (const hook of sortedHooks) {
            if (!hook.enabled)
                continue;
            try {
                const start = Date.now();
                await this.executeWithTimeout(hook.handler(context, error), hook.id);
                this.recordMetrics(hook.id, Date.now() - start, false);
            }
            catch (hookError) {
                this.handleHookError(stage, hook.id, hookError);
                // Always continue for notification hooks
            }
        }
    }
    // ==========================================================================
    // Utility Methods
    // ==========================================================================
    /**
     * Get sorted hooks by priority (descending)
     */
    getSortedHooks(registry) {
        return Array.from(registry.values()).sort((a, b) => b.priority - a.priority);
    }
    /**
     * Get registry for a stage
     */
    getRegistry(stage) {
        switch (stage) {
            case 'before_tool_call':
                return this.beforeHooks;
            case 'after_tool_call':
                return this.afterHooks;
            case 'tool_result_persist':
                return this.persistHooks;
            case 'tool_error':
                return this.errorHooks;
            case 'tool_timeout':
                return this.timeoutHooks;
            case 'tool_denied':
                return this.deniedHooks;
            default:
                throw new Error(`Unknown hook stage: ${stage}`);
        }
    }
    /**
     * Execute with timeout
     */
    async executeWithTimeout(promise, hookId) {
        if (!(promise instanceof Promise)) {
            return promise;
        }
        return Promise.race([
            promise,
            new Promise((_, reject) => {
                setTimeout(() => {
                    this.emit('hook:timeout', 'unknown', hookId);
                    reject(new Error(`Hook ${hookId} timed out after ${this.config.hookTimeoutMs}ms`));
                }, this.config.hookTimeoutMs);
            }),
        ]);
    }
    /**
     * Handle hook error
     */
    handleHookError(stage, hookId, error) {
        this.recordMetrics(hookId, 0, true);
        this.emit('hook:error', stage, hookId, error);
        logger.error(`Hook error in ${stage}:${hookId}`, { error: error.message });
    }
    /**
     * Initialize metrics for a hook
     */
    initMetrics(hookId) {
        if (!this.config.enableMetrics)
            return;
        this.metrics.set(hookId, {
            executions: 0,
            errors: 0,
            timeouts: 0,
            avgExecutionTimeMs: 0,
            lastExecutionTimeMs: 0,
        });
    }
    /**
     * Record hook execution metrics
     */
    recordMetrics(hookId, durationMs, isError) {
        if (!this.config.enableMetrics)
            return;
        const metrics = this.metrics.get(hookId);
        if (!metrics)
            return;
        metrics.executions++;
        metrics.lastExecutionTimeMs = durationMs;
        if (isError) {
            metrics.errors++;
        }
        else {
            // Update average execution time
            metrics.avgExecutionTimeMs =
                (metrics.avgExecutionTimeMs * (metrics.executions - 1) + durationMs) /
                    metrics.executions;
        }
    }
    // ==========================================================================
    // Statistics and Info
    // ==========================================================================
    /**
     * Get hook count by stage
     */
    getHookCounts() {
        return {
            before_tool_call: this.beforeHooks.size,
            after_tool_call: this.afterHooks.size,
            tool_result_persist: this.persistHooks.size,
            tool_error: this.errorHooks.size,
            tool_timeout: this.timeoutHooks.size,
            tool_denied: this.deniedHooks.size,
        };
    }
    /**
     * Get all registered hooks info
     */
    getRegisteredHooks() {
        const hooks = [];
        const stages = [
            ['before_tool_call', this.beforeHooks],
            ['after_tool_call', this.afterHooks],
            ['tool_result_persist', this.persistHooks],
            ['tool_error', this.errorHooks],
            ['tool_timeout', this.timeoutHooks],
            ['tool_denied', this.deniedHooks],
        ];
        for (const [stage, registry] of stages) {
            for (const hook of registry.values()) {
                hooks.push({
                    stage,
                    id: hook.id,
                    name: hook.name,
                    priority: hook.priority,
                    enabled: hook.enabled,
                    pluginId: hook.pluginId,
                });
            }
        }
        return hooks;
    }
    /**
     * Get metrics for a hook
     */
    getHookMetrics(hookId) {
        return this.metrics.get(hookId);
    }
    /**
     * Get all metrics
     */
    getAllMetrics() {
        return new Map(this.metrics);
    }
    /**
     * Clear all hooks
     */
    clearAll() {
        this.beforeHooks.clear();
        this.afterHooks.clear();
        this.persistHooks.clear();
        this.errorHooks.clear();
        this.timeoutHooks.clear();
        this.deniedHooks.clear();
        this.metrics.clear();
    }
}
// ============================================================================
// Singleton
// ============================================================================
let toolHooksInstance = null;
/**
 * Get the tool hooks manager instance
 */
export function getToolHooksManager(config) {
    if (!toolHooksInstance) {
        toolHooksInstance = new ToolHooksManager(config);
    }
    return toolHooksInstance;
}
/**
 * Reset the tool hooks manager
 */
export function resetToolHooksManager() {
    if (toolHooksInstance) {
        toolHooksInstance.clearAll();
        toolHooksInstance = null;
    }
}
export default ToolHooksManager;
//# sourceMappingURL=tool-hooks.js.map