/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { SchedulerStateManager } from './state-manager.js';
import { resolveConfirmation } from './confirmation.js';
import { checkPolicy, updatePolicy } from './policy.js';
import { ToolExecutor } from './tool-executor.js';
import { ToolModificationHandler } from './tool-modifier.js';
import {} from './types.js';
import { ToolErrorType } from '../tools/tool-error.js';
import { PolicyDecision } from '../policy/types.js';
import { ToolConfirmationOutcome, } from '../tools/tools.js';
import { getToolSuggestion } from '../utils/tool-utils.js';
import { runInDevTraceSpan } from '../telemetry/trace.js';
import { logToolCall } from '../telemetry/loggers.js';
import { ToolCallEvent } from '../telemetry/types.js';
import { MessageBusType, } from '../confirmation-bus/types.js';
const createErrorResponse = (request, error, errorType) => ({
    callId: request.callId,
    error,
    responseParts: [
        {
            functionResponse: {
                id: request.callId,
                name: request.name,
                response: { error: error.message },
            },
        },
    ],
    resultDisplay: error.message,
    errorType,
    contentLength: error.message.length,
});
/**
 * Event-Driven Orchestrator for Tool Execution.
 * Coordinates execution via state updates and event listening.
 */
export class Scheduler {
    // Tracks which MessageBus instances have the legacy listener attached to prevent duplicates.
    static subscribedMessageBuses = new WeakSet();
    state;
    executor;
    modifier;
    config;
    messageBus;
    getPreferredEditor;
    isProcessing = false;
    isCancelling = false;
    requestQueue = [];
    constructor(options) {
        this.config = options.config;
        this.messageBus = options.messageBus;
        this.getPreferredEditor = options.getPreferredEditor;
        this.state = new SchedulerStateManager(this.messageBus);
        this.executor = new ToolExecutor(this.config);
        this.modifier = new ToolModificationHandler();
        this.setupMessageBusListener(this.messageBus);
    }
    setupMessageBusListener(messageBus) {
        if (Scheduler.subscribedMessageBuses.has(messageBus)) {
            return;
        }
        // TODO: Optimize policy checks. Currently, tools check policy via
        // MessageBus even though the Scheduler already checked it.
        messageBus.subscribe(MessageBusType.TOOL_CONFIRMATION_REQUEST, async (request) => {
            await messageBus.publish({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId: request.correlationId,
                confirmed: false,
                requiresUserConfirmation: true,
            });
        });
        Scheduler.subscribedMessageBuses.add(messageBus);
    }
    /**
     * Schedules a batch of tool calls.
     * @returns A promise that resolves with the results of the completed batch.
     */
    async schedule(request, signal) {
        return runInDevTraceSpan({ name: 'schedule' }, async ({ metadata: spanMetadata }) => {
            const requests = Array.isArray(request) ? request : [request];
            spanMetadata.input = requests;
            if (this.isProcessing || this.state.isActive) {
                return this._enqueueRequest(requests, signal);
            }
            return this._startBatch(requests, signal);
        });
    }
    _enqueueRequest(requests, signal) {
        return new Promise((resolve, reject) => {
            const abortHandler = () => {
                const index = this.requestQueue.findIndex((item) => item.requests === requests);
                if (index > -1) {
                    this.requestQueue.splice(index, 1);
                    reject(new Error('Tool call cancelled while in queue.'));
                }
            };
            if (signal.aborted) {
                reject(new Error('Operation cancelled'));
                return;
            }
            signal.addEventListener('abort', abortHandler, { once: true });
            this.requestQueue.push({
                requests,
                signal,
                resolve: (results) => {
                    signal.removeEventListener('abort', abortHandler);
                    resolve(results);
                },
                reject: (err) => {
                    signal.removeEventListener('abort', abortHandler);
                    reject(err);
                },
            });
        });
    }
    cancelAll() {
        if (this.isCancelling)
            return;
        this.isCancelling = true;
        // Clear scheduler request queue
        while (this.requestQueue.length > 0) {
            const next = this.requestQueue.shift();
            next?.reject(new Error('Operation cancelled by user'));
        }
        // Cancel active call
        const activeCall = this.state.firstActiveCall;
        if (activeCall && !this.isTerminal(activeCall.status)) {
            this.state.updateStatus(activeCall.request.callId, 'cancelled', 'Operation cancelled by user');
        }
        // Clear queue
        this.state.cancelAllQueued('Operation cancelled by user');
    }
    get completedCalls() {
        return this.state.completedBatch;
    }
    isTerminal(status) {
        return status === 'success' || status === 'error' || status === 'cancelled';
    }
    // --- Phase 1: Ingestion & Resolution ---
    async _startBatch(requests, signal) {
        this.isProcessing = true;
        this.isCancelling = false;
        this.state.clearBatch();
        try {
            const toolRegistry = this.config.getToolRegistry();
            const newCalls = requests.map((request) => {
                const tool = toolRegistry.getTool(request.name);
                if (!tool) {
                    return this._createToolNotFoundErroredToolCall(request, toolRegistry.getAllToolNames());
                }
                return this._validateAndCreateToolCall(request, tool);
            });
            this.state.enqueue(newCalls);
            await this._processQueue(signal);
            return this.state.completedBatch;
        }
        finally {
            this.isProcessing = false;
            this._processNextInRequestQueue();
        }
    }
    _createToolNotFoundErroredToolCall(request, toolNames) {
        const suggestion = getToolSuggestion(request.name, toolNames);
        return {
            status: 'error',
            request,
            response: createErrorResponse(request, new Error(`Tool "${request.name}" not found.${suggestion}`), ToolErrorType.TOOL_NOT_REGISTERED),
            durationMs: 0,
        };
    }
    _validateAndCreateToolCall(request, tool) {
        try {
            const invocation = tool.build(request.args);
            return {
                status: 'validating',
                request,
                tool,
                invocation,
                startTime: Date.now(),
            };
        }
        catch (e) {
            return {
                status: 'error',
                request,
                tool,
                response: createErrorResponse(request, e instanceof Error ? e : new Error(String(e)), ToolErrorType.INVALID_TOOL_PARAMS),
                durationMs: 0,
            };
        }
    }
    // --- Phase 2: Processing Loop ---
    async _processQueue(signal) {
        while (this.state.queueLength > 0 || this.state.isActive) {
            const shouldContinue = await this._processNextItem(signal);
            if (!shouldContinue)
                break;
        }
    }
    /**
     * Processes the next item in the queue.
     * @returns true if the loop should continue, false if it should terminate.
     */
    async _processNextItem(signal) {
        if (signal.aborted || this.isCancelling) {
            this.state.cancelAllQueued('Operation cancelled');
            return false;
        }
        if (!this.state.isActive) {
            const next = this.state.dequeue();
            if (!next)
                return false;
            if (next.status === 'error') {
                this.state.updateStatus(next.request.callId, 'error', next.response);
                this.state.finalizeCall(next.request.callId);
                return true;
            }
        }
        const active = this.state.firstActiveCall;
        if (!active)
            return false;
        if (active.status === 'validating') {
            await this._processValidatingCall(active, signal);
        }
        return true;
    }
    async _processValidatingCall(active, signal) {
        try {
            await this._processToolCall(active, signal);
        }
        catch (error) {
            const err = error instanceof Error ? error : new Error(String(error));
            // If the signal aborted while we were waiting on something, treat as
            // cancelled. Otherwise, it's a genuine unhandled system exception.
            if (signal.aborted || err.name === 'AbortError') {
                this.state.updateStatus(active.request.callId, 'cancelled', 'Operation cancelled');
            }
            else {
                this.state.updateStatus(active.request.callId, 'error', createErrorResponse(active.request, err, ToolErrorType.UNHANDLED_EXCEPTION));
            }
        }
        // Fetch the updated call from state before finalizing to capture the
        // terminal status.
        const terminalCall = this.state.getToolCall(active.request.callId);
        if (terminalCall && this.isTerminal(terminalCall.status)) {
            logToolCall(this.config, new ToolCallEvent(terminalCall));
        }
        this.state.finalizeCall(active.request.callId);
    }
    // --- Phase 3: Single Call Orchestration ---
    async _processToolCall(toolCall, signal) {
        const callId = toolCall.request.callId;
        // Policy & Security
        const decision = await checkPolicy(toolCall, this.config);
        if (decision === PolicyDecision.DENY) {
            this.state.updateStatus(callId, 'error', createErrorResponse(toolCall.request, new Error('Tool execution denied by policy.'), ToolErrorType.POLICY_VIOLATION));
            return;
        }
        // User Confirmation Loop
        let outcome = ToolConfirmationOutcome.ProceedOnce;
        let lastDetails;
        if (decision === PolicyDecision.ASK_USER) {
            const result = await resolveConfirmation(toolCall, signal, {
                config: this.config,
                messageBus: this.messageBus,
                state: this.state,
                modifier: this.modifier,
                getPreferredEditor: this.getPreferredEditor,
            });
            outcome = result.outcome;
            lastDetails = result.lastDetails;
        }
        else {
            this.state.setOutcome(callId, ToolConfirmationOutcome.ProceedOnce);
        }
        // Handle Policy Updates
        await updatePolicy(toolCall.tool, outcome, lastDetails, {
            config: this.config,
            messageBus: this.messageBus,
        });
        // Handle cancellation (cascades to entire batch)
        if (outcome === ToolConfirmationOutcome.Cancel) {
            this.state.updateStatus(callId, 'cancelled', 'User denied execution.');
            this.state.cancelAllQueued('User cancelled operation');
            return; // Skip execution
        }
        // Execution
        await this._execute(callId, signal);
    }
    // --- Sub-phase Handlers ---
    /**
     * Executes the tool and records the result.
     */
    async _execute(callId, signal) {
        this.state.updateStatus(callId, 'scheduled');
        if (signal.aborted)
            throw new Error('Operation cancelled');
        this.state.updateStatus(callId, 'executing');
        const result = await this.executor.execute({
            call: this.state.firstActiveCall,
            signal,
            outputUpdateHandler: (id, out) => this.state.updateStatus(id, 'executing', { liveOutput: out }),
            onUpdateToolCall: (updated) => {
                if (updated.status === 'executing' && updated.pid) {
                    this.state.updateStatus(callId, 'executing', { pid: updated.pid });
                }
            },
        });
        if (result.status === 'success') {
            this.state.updateStatus(callId, 'success', result.response);
        }
        else if (result.status === 'cancelled') {
            this.state.updateStatus(callId, 'cancelled', 'Operation cancelled');
        }
        else {
            this.state.updateStatus(callId, 'error', result.response);
        }
    }
    _processNextInRequestQueue() {
        if (this.requestQueue.length > 0) {
            const next = this.requestQueue.shift();
            this.schedule(next.requests, next.signal)
                .then(next.resolve)
                .catch(next.reject);
        }
    }
}
//# sourceMappingURL=scheduler.js.map