/**
 * Worker-port protocol — the structured-clone-safe message channel between
 * the worker-side runtime controller and the main-thread client adapter.
 * Owns request/response correlation and the abort-side detachment rule for
 * results that arrive after the requesting tool has been cancelled.
 */
import { AgentEventKind, AgentProtocolErrorKind, toStructuredCloneSafeAgentEvent } from '../../contracts/agent-api.js';
import { WorkerMessageKind, isWorkerMessage } from '../../contracts/wire.js';
import { isOutOfCreditsError } from '../out-of-credits-error.js';
function createWorkerPortProtocolError(message) {
    return new Error(`Worker port protocol error: ${message}`);
}
function createProtocolFatalEvent(kind, message, requestId) {
    return {
        kind: AgentEventKind.FatalError,
        error: {
            kind,
            message: `Worker port protocol error: ${message}`,
            requestId
        }
    };
}
/** Convenience constructor without protocol-error reporting. */
export function createWorkerPortHostToolExecutor(port, registry) {
    return createWorkerPortHostToolExecutorWithOptions(port, { registry });
}
/** Build a `HostToolExecutor` that ferries requests over `port` and resolves on response. */
export function createWorkerPortHostToolExecutorWithOptions(port, options) {
    const { registry } = options;
    const ignoredResponses = new Set();
    const pending = new Map();
    const emitProtocolError = (message, requestId) => {
        options.onProtocolError?.(createProtocolFatalEvent(AgentProtocolErrorKind.WorkerPortProtocol, message, requestId));
    };
    const listener = (event) => {
        if (!isWorkerMessage(event.data, registry) ||
            event.data.kind !== WorkerMessageKind.HostToolResult) {
            return;
        }
        const pendingRequest = pending.get(event.data.result.requestId);
        if (!pendingRequest) {
            // Abort invariant: a response for an aborted request is still expected
            // to arrive (the host may not see the cancel in time). It is silently
            // dropped via the ignored-responses set rather than treated as a
            // protocol violation or rerouted into the runtime.
            if (ignoredResponses.delete(event.data.result.requestId)) {
                return;
            }
            emitProtocolError(`received response for unknown request ${event.data.result.requestId}`, event.data.result.requestId);
            return;
        }
        pending.delete(event.data.result.requestId);
        pendingRequest.removeAbortListener();
        pendingRequest.resolve(event.data.result);
    };
    port.addEventListener('message', listener);
    port.start?.();
    return {
        hostToolExecutor: {
            executeHostTool(request, requestOptions) {
                if (pending.has(request.requestId)) {
                    emitProtocolError(`duplicate pending request ${request.requestId}`, request.requestId);
                    return Promise.reject(createWorkerPortProtocolError(`duplicate pending request ${request.requestId}`));
                }
                if (requestOptions?.signal?.aborted) {
                    return Promise.reject(new Error('Worker port request was aborted.'));
                }
                return new Promise((resolve, reject) => {
                    const abortRequest = () => {
                        const pendingRequest = pending.get(request.requestId);
                        if (!pendingRequest) {
                            return;
                        }
                        // Mark this request id as ignored before settling the promise so
                        // the eventual late response (when it does arrive) is detached
                        // rather than reported as an unknown-request protocol error.
                        pending.delete(request.requestId);
                        pendingRequest.removeAbortListener();
                        ignoredResponses.add(request.requestId);
                        reject(new Error('Worker port request was aborted.'));
                    };
                    const removeAbortListener = () => {
                        requestOptions?.signal?.removeEventListener('abort', abortRequest);
                    };
                    requestOptions?.signal?.addEventListener('abort', abortRequest, { once: true });
                    pending.set(request.requestId, { resolve, reject, removeAbortListener });
                    try {
                        port.postMessage({
                            kind: WorkerMessageKind.HostToolRequest,
                            request
                        });
                    }
                    catch (error) {
                        pending.delete(request.requestId);
                        removeAbortListener();
                        emitProtocolError(`failed to post request ${request.requestId}`, request.requestId);
                        reject(error instanceof Error ? error : new Error(String(error)));
                    }
                });
            }
        },
        dispose() {
            for (const pendingRequest of pending.values()) {
                pendingRequest.removeAbortListener();
                pendingRequest.reject(new Error('Worker port disposed before host tool response arrived'));
            }
            ignoredResponses.clear();
            pending.clear();
            port.removeEventListener('message', listener);
        }
    };
}
/** Pump runtime events out and `AgentCommand`s in across the worker port. */
export function attachEmbeddedAgentDispatchToWorkerPort(port, controller, registry) {
    const unsubscribe = controller.subscribe((event) => {
        const message = {
            kind: WorkerMessageKind.AgentEvent,
            event: toStructuredCloneSafeAgentEvent(event, registry)
        };
        port.postMessage(message);
    });
    const listener = (event) => {
        if (!isWorkerMessage(event.data, registry) ||
            event.data.kind !== WorkerMessageKind.AgentCommand) {
            return;
        }
        const message = event.data;
        void controller
            .handleCommand(message.command)
            .then(() => {
            const ackMessage = {
                kind: WorkerMessageKind.AgentCommandAck,
                commandId: message.commandId,
                ok: true
            };
            port.postMessage(ackMessage);
        })
            .catch((error) => {
            const errorMessage = error instanceof Error ? error.message : String(error);
            const ackMessage = {
                kind: WorkerMessageKind.AgentCommandAck,
                commandId: message.commandId,
                ok: false,
                error: errorMessage
            };
            port.postMessage(ackMessage);
            if (message.fatalOnError === false || isOutOfCreditsError(error)) {
                return;
            }
            const eventMessage = {
                kind: WorkerMessageKind.AgentEvent,
                event: createProtocolFatalEvent(AgentProtocolErrorKind.ControllerProtocol, errorMessage)
            };
            port.postMessage(eventMessage);
        });
    };
    port.addEventListener('message', listener);
    port.start?.();
    return {
        dispose() {
            unsubscribe();
            port.removeEventListener('message', listener);
        }
    };
}
//# sourceMappingURL=worker-port.js.map