/** Owns runtime-side request/result pairing: emit, await, abort, idempotent on retry. */
import { createFailedHostToolResult } from '../contracts/host-tool-api.js';
import { InFlightOpKind } from './core-types.js';
export const PENDING_HOST_TOOL_REQUESTS_DISCARDED_MESSAGE = 'Host tool request was discarded before a result arrived.';
const defaultLogger = {
    warn(message, meta) {
        if (meta && Object.keys(meta).length > 0) {
            console.warn(`[pending-host-tool-requests] ${message}`, meta);
            return;
        }
        console.warn(`[pending-host-tool-requests] ${message}`);
    }
};
function defaultSchedule(cb, ms) {
    const handle = setTimeout(cb, ms);
    return () => clearTimeout(handle);
}
export function createPendingHostToolRequests(options = {}) {
    const logger = options.logger ?? defaultLogger;
    const schedule = options.schedule ?? defaultSchedule;
    const getTimeoutMs = options.getTimeoutMs ?? (() => 0);
    const getHeartbeatWrap = options.getHeartbeatWrap;
    const listeners = new Set();
    const pendingRequests = new Map();
    const resolvedRequestIds = new Set();
    const discardedRequestIds = new Set();
    return {
        request(request) {
            if (pendingRequests.has(request.requestId)) {
                throw new Error(`Duplicate host tool request id: ${request.requestId}`);
            }
            const inner = new Promise((resolve, reject) => {
                const timeoutMs = getTimeoutMs(request.kind);
                let cancelTimeout = () => { };
                if (timeoutMs > 0) {
                    cancelTimeout = schedule(() => {
                        const entry = pendingRequests.get(request.requestId);
                        if (!entry)
                            return;
                        pendingRequests.delete(request.requestId);
                        resolvedRequestIds.add(request.requestId);
                        entry.resolve(createFailedHostToolResult(request.kind, request.requestId, `Host tool "${request.kind}" timed out after ${Math.round(timeoutMs / 1000)}s.`));
                    }, timeoutMs);
                }
                pendingRequests.set(request.requestId, {
                    kind: request.kind,
                    resolve,
                    reject,
                    cancelTimeout
                });
                for (const listener of listeners) {
                    try {
                        listener(request);
                    }
                    catch (error) {
                        const entry = pendingRequests.get(request.requestId);
                        entry?.cancelTimeout();
                        pendingRequests.delete(request.requestId);
                        reject(error instanceof Error ? error : new Error(String(error)));
                        return;
                    }
                }
            });
            const wrap = getHeartbeatWrap?.();
            return wrap
                ? wrap(inner, InFlightOpKind.HostTool, {
                    toolName: request.heartbeat?.toolName ?? request.kind,
                    toolCallId: request.requestId,
                    label: request.heartbeat?.label ?? request.heartbeat?.toolName ?? request.kind
                })
                : inner;
        },
        applyResult(result) {
            const pendingRequest = pendingRequests.get(result.requestId);
            if (!pendingRequest) {
                // Idempotent: duplicate, post-discard, or unknown — log + no-op.
                const reason = resolvedRequestIds.has(result.requestId)
                    ? 'duplicate result for already-resolved request'
                    : discardedRequestIds.has(result.requestId)
                        ? 'late result for discarded request'
                        : 'result for unknown request';
                logger.warn(reason, { requestId: result.requestId, kind: result.kind });
                return;
            }
            if (pendingRequest.kind !== result.kind) {
                logger.warn('host tool result kind mismatch', {
                    requestId: result.requestId,
                    expectedKind: pendingRequest.kind,
                    actualKind: result.kind
                });
                return;
            }
            pendingRequest.cancelTimeout();
            pendingRequests.delete(result.requestId);
            resolvedRequestIds.add(result.requestId);
            pendingRequest.resolve(result);
        },
        discardPending() {
            const pendingEntries = Array.from(pendingRequests.entries());
            pendingRequests.clear();
            for (const [requestId, pendingRequest] of pendingEntries) {
                discardedRequestIds.add(requestId);
                pendingRequest.cancelTimeout();
                pendingRequest.reject(new Error(PENDING_HOST_TOOL_REQUESTS_DISCARDED_MESSAGE));
            }
        },
        subscribe(listener) {
            listeners.add(listener);
            return () => {
                listeners.delete(listener);
            };
        },
        dispose() {
            this.discardPending();
            listeners.clear();
            resolvedRequestIds.clear();
            discardedRequestIds.clear();
        }
    };
}
//# sourceMappingURL=pending-host-tool-requests.js.map