/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { FunctionCallingConfigMode } from '@google/genai';
import { DefaultHookOutput, BeforeToolHookOutput, BeforeModelHookOutput, BeforeToolSelectionHookOutput, AfterModelHookOutput, AfterAgentHookOutput, HookEventName, } from './types.js';
/**
 * Hook aggregator that merges results from multiple hooks using event-specific strategies
 */
export class HookAggregator {
    /**
     * Aggregate results from multiple hook executions
     */
    aggregateResults(results, eventName) {
        const allOutputs = [];
        const errors = [];
        let totalDuration = 0;
        // Collect all outputs and errors
        for (const result of results) {
            totalDuration += result.duration;
            if (result.error) {
                errors.push(result.error);
            }
            if (result.output) {
                allOutputs.push(result.output);
            }
        }
        // Merge outputs using event-specific strategy
        const mergedOutput = this.mergeOutputs(allOutputs, eventName);
        const finalOutput = mergedOutput
            ? this.createSpecificHookOutput(mergedOutput, eventName)
            : undefined;
        return {
            success: errors.length === 0,
            finalOutput,
            allOutputs,
            errors,
            totalDuration,
        };
    }
    /**
     * Merge hook outputs using event-specific strategies
     *
     * Note: We always use the merge logic even for single hooks to ensure
     * consistent default behaviors (e.g., default decision='allow' for OR logic)
     */
    mergeOutputs(outputs, eventName) {
        if (outputs.length === 0) {
            return undefined;
        }
        switch (eventName) {
            case HookEventName.BeforeTool:
            case HookEventName.AfterTool:
            case HookEventName.BeforeAgent:
            case HookEventName.AfterAgent:
            case HookEventName.SessionStart:
                return this.mergeWithOrDecision(outputs);
            case HookEventName.BeforeModel:
            case HookEventName.AfterModel:
                return this.mergeWithFieldReplacement(outputs);
            case HookEventName.BeforeToolSelection:
                return this.mergeToolSelectionOutputs(
                // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                outputs);
            default:
                // For other events, use simple merge
                return this.mergeSimple(outputs);
        }
    }
    /**
     * Merge outputs with OR decision logic and message concatenation
     */
    mergeWithOrDecision(outputs) {
        const merged = {
            continue: true,
            suppressOutput: false,
        };
        const messages = [];
        const reasons = [];
        const systemMessages = [];
        const additionalContexts = [];
        let hasBlockDecision = false;
        let hasContinueFalse = false;
        for (const output of outputs) {
            // Handle continue flag
            if (output.continue === false) {
                hasContinueFalse = true;
                merged.continue = false;
                if (output.stopReason) {
                    messages.push(output.stopReason);
                }
            }
            // Handle decision (OR logic for blocking)
            const tempOutput = new DefaultHookOutput(output);
            if (tempOutput.isBlockingDecision()) {
                hasBlockDecision = true;
                merged.decision = output.decision;
            }
            // Collect messages
            if (output.reason) {
                reasons.push(output.reason);
            }
            if (output.systemMessage) {
                systemMessages.push(output.systemMessage);
            }
            // Handle suppress output (any true wins)
            if (output.suppressOutput) {
                merged.suppressOutput = true;
            }
            // Handle clearContext (any true wins) - for AfterAgent hooks
            if (output.hookSpecificOutput?.['clearContext'] === true) {
                merged.hookSpecificOutput = {
                    ...(merged.hookSpecificOutput || {}),
                    clearContext: true,
                };
            }
            // Merge hookSpecificOutput (excluding clearContext which is handled above)
            if (output.hookSpecificOutput) {
                const { clearContext: _clearContext, ...restSpecificOutput } = output.hookSpecificOutput;
                merged.hookSpecificOutput = {
                    ...(merged.hookSpecificOutput || {}),
                    ...restSpecificOutput,
                };
            }
            // Collect additional context from hook-specific outputs
            this.extractAdditionalContext(output, additionalContexts);
        }
        // Set final decision if no blocking decision was found
        if (!hasBlockDecision && !hasContinueFalse) {
            merged.decision = 'allow';
        }
        // Merge messages
        if (messages.length > 0) {
            merged.stopReason = messages.join('\n');
        }
        if (reasons.length > 0) {
            merged.reason = reasons.join('\n');
        }
        if (systemMessages.length > 0) {
            merged.systemMessage = systemMessages.join('\n');
        }
        // Add merged additional context
        if (additionalContexts.length > 0) {
            merged.hookSpecificOutput = {
                ...(merged.hookSpecificOutput || {}),
                additionalContext: additionalContexts.join('\n'),
            };
        }
        return merged;
    }
    /**
     * Merge outputs with later fields replacing earlier fields
     */
    mergeWithFieldReplacement(outputs) {
        let merged = {};
        for (const output of outputs) {
            // Later outputs override earlier ones
            merged = {
                ...merged,
                ...output,
                hookSpecificOutput: {
                    ...merged.hookSpecificOutput,
                    ...output.hookSpecificOutput,
                },
            };
        }
        return merged;
    }
    /**
     * Merge tool selection outputs with specific logic for tool config
     *
     * Tool Selection Strategy:
     * - The intent is to provide a UNION of tools from all hooks
     * - If any hook specifies NONE mode, no tools are available (most restrictive wins)
     * - If any hook specifies ANY mode (and no NONE), ANY mode is used
     * - Otherwise AUTO mode is used
     * - Function names are collected from all hooks and sorted for deterministic caching
     *
     * This means hooks can only add/enable tools, not filter them out individually.
     * If one hook restricts and another re-enables, the union takes the re-enabled tool.
     */
    mergeToolSelectionOutputs(outputs) {
        const merged = {};
        const allFunctionNames = new Set();
        let hasNoneMode = false;
        let hasAnyMode = false;
        for (const output of outputs) {
            const toolConfig = output.hookSpecificOutput?.toolConfig;
            if (!toolConfig) {
                continue;
            }
            // Check mode (using simplified HookToolConfig format)
            if (toolConfig.mode === 'NONE') {
                hasNoneMode = true;
            }
            else if (toolConfig.mode === 'ANY') {
                hasAnyMode = true;
            }
            // Collect function names (union of all hooks)
            if (toolConfig.allowedFunctionNames) {
                for (const name of toolConfig.allowedFunctionNames) {
                    allFunctionNames.add(name);
                }
            }
        }
        // Determine final mode and function names
        let finalMode;
        let finalFunctionNames = [];
        if (hasNoneMode) {
            // NONE mode wins - most restrictive
            finalMode = FunctionCallingConfigMode.NONE;
            finalFunctionNames = [];
        }
        else if (hasAnyMode) {
            // ANY mode if present (and no NONE)
            finalMode = FunctionCallingConfigMode.ANY;
            // Sort for deterministic output to ensure consistent caching
            finalFunctionNames = Array.from(allFunctionNames).sort();
        }
        else {
            // Default to AUTO mode
            finalMode = FunctionCallingConfigMode.AUTO;
            // Sort for deterministic output to ensure consistent caching
            finalFunctionNames = Array.from(allFunctionNames).sort();
        }
        merged.hookSpecificOutput = {
            hookEventName: 'BeforeToolSelection',
            toolConfig: {
                mode: finalMode,
                allowedFunctionNames: finalFunctionNames,
            },
        };
        return merged;
    }
    /**
     * Simple merge for events without special logic
     */
    mergeSimple(outputs) {
        let merged = {};
        for (const output of outputs) {
            merged = { ...merged, ...output };
        }
        return merged;
    }
    /**
     * Create the appropriate specific hook output class based on event type
     */
    createSpecificHookOutput(output, eventName) {
        switch (eventName) {
            case HookEventName.BeforeTool:
                return new BeforeToolHookOutput(output);
            case HookEventName.BeforeModel:
                return new BeforeModelHookOutput(output);
            case HookEventName.BeforeToolSelection:
                return new BeforeToolSelectionHookOutput(output);
            case HookEventName.AfterModel:
                return new AfterModelHookOutput(output);
            case HookEventName.AfterAgent:
                return new AfterAgentHookOutput(output);
            default:
                return new DefaultHookOutput(output);
        }
    }
    /**
     * Extract additional context from hook-specific outputs
     */
    extractAdditionalContext(output, contexts) {
        const specific = output.hookSpecificOutput;
        if (!specific) {
            return;
        }
        // Extract additionalContext from various hook types
        if ('additionalContext' in specific &&
            typeof specific['additionalContext'] === 'string') {
            contexts.push(specific['additionalContext']);
        }
    }
}
//# sourceMappingURL=hookAggregator.js.map