import { ProviderProtocolError } from '../provider-protocol-error.js';
import { normalizeFunctionCallOutputIdWithNative as normalizeFunctionCallOutputId } from '../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
import { convertResponsesOutputToInputItemsWithNative, pickResponsesPersistedFieldsWithNative } from '../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
const TTL_MS = 1000 * 60 * 30; // 30min
function cloneDeep(value) {
    try {
        if (typeof globalThis.structuredClone === 'function') {
            return globalThis.structuredClone(value);
        }
    }
    catch {
        /* ignore */
    }
    return JSON.parse(JSON.stringify(value ?? null));
}
function isRecord(value) {
    return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function coerceInputArray(input) {
    if (!Array.isArray(input)) {
        return [];
    }
    return cloneDeep(input);
}
function coerceTools(tools) {
    if (!Array.isArray(tools)) {
        return undefined;
    }
    return cloneDeep(tools);
}
function pickPersistedFields(payload) {
    return pickResponsesPersistedFieldsWithNative(payload);
}
function convertOutputToInputItems(response) {
    return convertResponsesOutputToInputItemsWithNative(response);
}
function normalizeSubmittedToolOutputs(toolOutputs, callIdToFunctionItemId) {
    const items = [];
    const submitted = [];
    toolOutputs.forEach((entry, index) => {
        if (!entry || typeof entry !== 'object')
            return;
        const rec = entry;
        const rawId = typeof rec.tool_call_id === 'string'
            ? rec.tool_call_id
            : typeof rec.call_id === 'string'
                ? rec.call_id
                : typeof rec.id === 'string'
                    ? rec.id
                    : undefined;
        const trimmed = typeof rawId === 'string' ? rawId.trim() : '';
        const callId = trimmed.length ? trimmed : `call_resume_${index}`;
        const mappedItemId = callIdToFunctionItemId && typeof callId === 'string' && callIdToFunctionItemId.has(callId)
            ? callIdToFunctionItemId.get(callId)
            : undefined;
        const mappedIdTrimmed = typeof mappedItemId === 'string' ? mappedItemId.trim() : '';
        const outputId = mappedIdTrimmed.length
            ? normalizeFunctionCallOutputId({ callId: mappedIdTrimmed, fallback: mappedIdTrimmed })
            : normalizeFunctionCallOutputId({
                callId,
                fallback: trimmed.length ? trimmed : `fc_resume_${index}`
            });
        const outputValue = rec.output ?? null;
        const normalizedOutput = typeof outputValue === 'string'
            ? outputValue
            : (() => {
                try {
                    return JSON.stringify(outputValue ?? null);
                }
                catch {
                    return String(outputValue ?? '');
                }
            })();
        items.push({
            type: 'function_call_output',
            id: outputId,
            call_id: callId,
            output: normalizedOutput
        });
        submitted.push({ callId, originalId: rawId ?? callId, outputText: normalizedOutput });
    });
    return { items, submitted };
}
class ResponsesConversationStore {
    requestMap = new Map();
    responseIndex = new Map();
    rebindRequestId(oldId, newId) {
        if (!oldId || !newId || oldId === newId) {
            return;
        }
        const entry = this.requestMap.get(oldId);
        if (!entry) {
            return;
        }
        this.requestMap.delete(oldId);
        entry.requestId = newId;
        this.requestMap.set(newId, entry);
    }
    captureRequestContext(args) {
        const { requestId, payload, context } = args;
        if (!requestId || !payload)
            return;
        this.prune();
        const entry = {
            requestId,
            basePayload: pickPersistedFields(payload),
            input: coerceInputArray(context.input),
            tools: coerceTools(context.toolsRaw) ||
                coerceTools(Array.isArray(payload.tools) ? payload.tools : undefined),
            createdAt: Date.now(),
            updatedAt: Date.now()
        };
        if (typeof payload.model === 'string' && payload.model.trim()) {
            entry.basePayload.model = payload.model;
        }
        if (typeof payload.stream === 'boolean') {
            entry.basePayload.stream = payload.stream;
        }
        if (entry.tools) {
            entry.basePayload.tools = entry.tools;
        }
        this.requestMap.set(requestId, entry);
    }
    recordResponse(args) {
        const entry = args.requestId ? this.requestMap.get(args.requestId) : undefined;
        if (!entry)
            return;
        const response = args.response;
        const responseId = typeof response.id === 'string' ? response.id : undefined;
        if (!responseId)
            return;
        const assistantBlocks = convertOutputToInputItems(response);
        if (assistantBlocks.length) {
            entry.input.push(...assistantBlocks);
        }
        entry.lastResponseId = responseId;
        entry.updatedAt = Date.now();
        this.responseIndex.set(responseId, entry);
    }
    resumeConversation(responseId, submitPayload, options) {
        if (typeof responseId !== 'string' || !responseId.trim()) {
            throw new ProviderProtocolError('Responses conversation requires valid response_id', {
                code: 'MALFORMED_REQUEST',
                protocol: 'openai-responses',
                providerType: 'responses',
                details: {
                    context: 'responses-conversation-store.resumeConversation',
                    reason: 'missing_or_empty_response_id'
                }
            });
        }
        this.prune();
        const entry = this.responseIndex.get(responseId);
        if (!entry) {
            throw new ProviderProtocolError('Responses conversation expired or not found', {
                code: 'MALFORMED_REQUEST',
                protocol: 'openai-responses',
                providerType: 'responses',
                details: {
                    context: 'responses-conversation-store.resumeConversation',
                    reason: 'expired_or_unknown_response_id',
                    responseId
                }
            });
        }
        const toolOutputs = Array.isArray(submitPayload.tool_outputs) ? submitPayload.tool_outputs : [];
        if (!toolOutputs.length) {
            throw new ProviderProtocolError('tool_outputs array is required when submitting Responses tool results', {
                code: 'MALFORMED_REQUEST',
                protocol: 'openai-responses',
                providerType: 'responses',
                details: {
                    context: 'responses-conversation-store.resumeConversation',
                    reason: 'missing_tool_outputs',
                    responseId
                }
            });
        }
        const mergedInput = coerceInputArray(entry.input);
        const callIdToFunctionItemId = new Map();
        for (const item of mergedInput) {
            if (!item || typeof item !== 'object')
                continue;
            const type = typeof item.type === 'string' ? String(item.type) : '';
            if (type !== 'function_call')
                continue;
            const id = typeof item.id === 'string' ? String(item.id).trim() : '';
            const callId = typeof item.call_id === 'string' ? String(item.call_id).trim() : '';
            if (id) {
                callIdToFunctionItemId.set(id, id);
            }
            if (callId) {
                callIdToFunctionItemId.set(callId, id || callId);
            }
        }
        const normalizedOutputs = normalizeSubmittedToolOutputs(toolOutputs, callIdToFunctionItemId);
        mergedInput.push(...normalizedOutputs.items);
        const payload = cloneDeep(entry.basePayload);
        payload.input = mergedInput;
        // Preserve the caller's streaming intent for the resume request.
        // Do not force-enable streaming here: some upstreams return JSON even when we send `Accept: text/event-stream`,
        // and forcing `stream=true` can trip SSE decoders on non-SSE bodies.
        payload.stream =
            typeof submitPayload.stream === 'boolean'
                ? Boolean(submitPayload.stream)
                : typeof entry.basePayload.stream === 'boolean'
                    ? Boolean(entry.basePayload.stream)
                    : false;
        payload.previous_response_id = responseId;
        if (Array.isArray(entry.tools) && entry.tools.length) {
            payload.tools = cloneDeep(entry.tools);
        }
        if (typeof submitPayload.model === 'string' && submitPayload.model.trim()) {
            payload.model = submitPayload.model.trim();
        }
        if (submitPayload.metadata && isRecord(submitPayload.metadata)) {
            const baseMeta = isRecord(payload.metadata) ? payload.metadata : {};
            payload.metadata = { ...baseMeta, ...cloneDeep(submitPayload.metadata) };
        }
        delete payload.tool_outputs;
        delete payload.response_id;
        this.cleanupEntry(entry, responseId);
        return {
            payload,
            meta: {
                restoredFromResponseId: responseId,
                previousRequestId: entry.requestId,
                toolOutputs: toolOutputs.length,
                toolOutputsDetailed: normalizedOutputs.submitted,
                requestId: options?.requestId
            }
        };
    }
    clearRequest(requestId) {
        if (!requestId)
            return;
        const entry = this.requestMap.get(requestId);
        if (!entry)
            return;
        this.requestMap.delete(requestId);
        if (entry.lastResponseId) {
            this.responseIndex.delete(entry.lastResponseId);
        }
    }
    cleanupEntry(entry, responseId) {
        this.responseIndex.delete(responseId);
        this.requestMap.delete(entry.requestId);
    }
    prune() {
        const now = Date.now();
        for (const [requestId, entry] of this.requestMap.entries()) {
            if (now - entry.updatedAt > TTL_MS) {
                this.requestMap.delete(requestId);
                if (entry.lastResponseId) {
                    this.responseIndex.delete(entry.lastResponseId);
                }
            }
        }
        for (const [respId, entry] of this.responseIndex.entries()) {
            if (!this.requestMap.has(entry.requestId)) {
                this.responseIndex.delete(respId);
            }
        }
    }
}
const store = new ResponsesConversationStore();
const RESPONSES_DEBUG = (process.env.ROUTECODEX_RESPONSES_DEBUG || '').trim() === '1';
export function captureResponsesRequestContext(args) {
    try {
        if (RESPONSES_DEBUG) {
            console.log('[responses-store] capture', args.requestId);
        }
        store.captureRequestContext(args);
    }
    catch {
        /* ignore capture failures */
    }
}
export function recordResponsesResponse(args) {
    try {
        if (RESPONSES_DEBUG) {
            console.log('[responses-store] record', args.requestId, args.response?.id);
        }
        store.recordResponse(args);
    }
    catch {
        /* ignore */
    }
}
export function resumeResponsesConversation(responseId, submitPayload, options) {
    if (RESPONSES_DEBUG) {
        console.log('[responses-store] resume', responseId);
    }
    return store.resumeConversation(responseId, submitPayload, options);
}
export function clearResponsesConversationByRequestId(requestId) {
    if (RESPONSES_DEBUG && requestId) {
        console.log('[responses-store] clear', requestId);
    }
    store.clearRequest(requestId);
}
export function rebindResponsesConversationRequestId(oldId, newId) {
    if (RESPONSES_DEBUG && oldId && newId) {
        console.log('[responses-store] rebind', oldId, '->', newId);
    }
    store.rebindRequestId(oldId, newId);
}
export { store as responsesConversationStore };
