// ---------------------------------------------------------------------------
// LLM Adapter — OpenAI implementation
// ---------------------------------------------------------------------------
import OpenAI from 'openai';
import { DEFAULT_OPENAI_MODEL } from '../../../../core/config.js';
import { OPENAI_CODEX_RESPONSES_ENDPOINT, OpenAiOAuthService, } from '../../../../core/auth/openai-oauth.js';
import { ProviderCredentialRepository, } from '../../../../core/auth/index.js';
import { ModelPolicyService } from '../../../../core/llm/models/index.js';
import { OpenAiCodec } from './openai-codec.js';
/**
 * OpenAI implementation of the LLM port. It owns OpenAI Responses streaming,
 * tool-call parsing, reasoning summary conversion, and OAuth fetch wiring.
 */
export class OpenAiAdapter {
    static capabilities = {
        toolCalls: true,
        systemMessages: true,
        reasoningSummaries: true,
        parallelToolCalls: true,
    };
    info;
    client;
    model;
    oauthCredential;
    reasoningEffort;
    constructor(options = {}) {
        const credentials = options.credentials;
        const runtime = options.runtime;
        this.oauthCredential = credentials?.credential?.type === 'oauth' ? credentials.credential : undefined;
        this.model = options.model ?? DEFAULT_OPENAI_MODEL;
        this.reasoningEffort = runtime?.reasoningEffort;
        this.client = new OpenAI({
            apiKey: this.oauthCredential ? 'heddle-oauth-placeholder'
                : OpenAiAdapter.firstDefinedNonEmpty(credentials?.apiKey, process.env.OPENAI_API_KEY, process.env.PERSONAL_OPENAI_API_KEY),
            fetch: this.oauthCredential ? OpenAiOAuthFetchService.create(this.oauthCredential, {
                storePath: credentials?.credentialStorePath,
                fetchImpl: runtime?.fetchImpl,
            }) : runtime?.fetchImpl,
        });
        this.info = {
            provider: 'openai',
            model: this.model,
            capabilities: OpenAiAdapter.capabilities,
        };
    }
    async chat(messages, tools, signal, onStreamEvent) {
        if (this.oauthCredential) {
            ModelPolicyService.assertCredentialCompatibility({
                model: this.model,
                provider: 'openai',
                credentialMode: 'oauth',
            });
        }
        const request = OpenAiCodec.buildResponsesRequest(messages, {
            model: this.model,
            tools,
            oauthMode: Boolean(this.oauthCredential),
            reasoningEffort: this.reasoningEffort,
        });
        const stream = await this.client.responses.stream({
            ...request,
        }, { signal });
        let streamedContent = '';
        const streamedToolCalls = new Map();
        let completedResponse;
        try {
            for await (const event of stream) {
                if (event.type === 'response.output_text.delta' && event.delta) {
                    streamedContent += event.delta;
                    onStreamEvent?.({ type: 'content.delta', delta: event.delta });
                    continue;
                }
                if (event.type === 'response.output_text.done') {
                    streamedContent = event.text;
                    onStreamEvent?.({ type: 'content.done', content: event.text });
                    continue;
                }
                if (event.type === 'response.reasoning_summary_text.delta' && event.delta) {
                    onStreamEvent?.({ type: 'reasoning_summary.delta', delta: event.delta });
                    continue;
                }
                if (event.type === 'response.reasoning_summary_text.done') {
                    onStreamEvent?.({ type: 'reasoning_summary.done', text: event.text });
                    continue;
                }
                if (event.type === 'response.reasoning_summary.delta') {
                    const delta = OpenAiCodec.readReasoningSummaryDeltaText(event.delta);
                    if (delta) {
                        onStreamEvent?.({ type: 'reasoning_summary.delta', delta });
                    }
                    continue;
                }
                if (event.type === 'response.reasoning_summary.done') {
                    onStreamEvent?.({ type: 'reasoning_summary.done', text: event.text });
                    continue;
                }
                if (event.type === 'response.output_item.added' || event.type === 'response.output_item.done') {
                    const item = event.item;
                    if (item.type === 'function_call'
                        && typeof item.id === 'string'
                        && typeof item.call_id === 'string'
                        && typeof item.name === 'string') {
                        const existing = streamedToolCalls.get(item.id);
                        streamedToolCalls.set(item.id, {
                            id: item.call_id,
                            tool: item.name,
                            argumentsText: typeof item.arguments === 'string' && item.arguments.length > 0 ? item.arguments
                                : existing?.argumentsText ?? '',
                        });
                    }
                    continue;
                }
                if (event.type === 'response.function_call_arguments.delta') {
                    const existing = streamedToolCalls.get(event.item_id);
                    if (existing) {
                        existing.argumentsText += event.delta;
                    }
                    continue;
                }
                if (event.type === 'response.function_call_arguments.done') {
                    const existing = streamedToolCalls.get(event.item_id);
                    if (existing) {
                        existing.argumentsText = event.arguments;
                    }
                    continue;
                }
                if (event.type === 'response.completed') {
                    completedResponse = event.response;
                }
            }
        }
        catch (error) {
            // Once the API has emitted a completed response, prefer that captured
            // response over failing the turn on SDK-side stream finalization/parsing.
            if (!completedResponse) {
                throw error;
            }
        }
        const response = completedResponse ?? await stream.finalResponse();
        const outputItems = Array.isArray(response.output) ? response.output : [];
        const finalResponseToolCalls = outputItems.flatMap((item) => {
            if (item.type !== 'function_call'
                || typeof item.call_id !== 'string'
                || typeof item.name !== 'string'
                || typeof item.arguments !== 'string') {
                return [];
            }
            return [{
                    id: item.call_id,
                    tool: item.name,
                    input: JSON.parse(item.arguments),
                }];
        });
        const toolCalls = finalResponseToolCalls.length > 0 ? finalResponseToolCalls : OpenAiCodec.parseStreamedToolCalls(streamedToolCalls);
        const diagnostics = OpenAiCodec.extractAssistantDiagnostics(response, toolCalls.length > 0);
        const content = streamedContent || (diagnostics?.rationale ?? OpenAiCodec.extractAssistantContent(response, toolCalls.length > 0));
        if (!streamedContent && content) {
            onStreamEvent?.({ type: 'content.delta', delta: content });
            onStreamEvent?.({ type: 'content.done', content });
        }
        return {
            content,
            diagnostics,
            toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
            usage: OpenAiCodec.extractUsage(response),
        };
    }
    static firstDefinedNonEmpty(...values) {
        return values.find((value) => typeof value === 'string' && value.trim().length > 0);
    }
}
/**
 * Builds the OpenAI OAuth fetch implementation and refreshes stored account
 * credentials before expiry.
 */
export class OpenAiOAuthFetchService {
    static create(initialCredential, options = {}) {
        let credential = initialCredential;
        const fetcher = options.fetchImpl ?? fetch;
        const refreshBeforeMs = options.refreshBeforeMs ?? 60_000;
        const oauthFetch = async (requestInput, init) => {
            if (credential.expiresAt <= Date.now() + refreshBeforeMs) {
                credential = await OpenAiOAuthService.refreshCredential(credential, { fetchImpl: fetcher });
                new ProviderCredentialRepository({ storePath: options.storePath }).set(credential);
            }
            const request = requestInput instanceof Request ? requestInput : undefined;
            const requestInit = init;
            const headers = new Headers(requestInit?.headers ?? request?.headers);
            headers.delete('authorization');
            headers.delete('Authorization');
            headers.set('authorization', `Bearer ${credential.accessToken}`);
            if (credential.accountId) {
                headers.set('ChatGPT-Account-Id', credential.accountId);
            }
            const url = OpenAiOAuthFetchService.normalizeRequestUrl(requestInput);
            const rewrittenUrl = OpenAiOAuthFetchService.shouldRouteToCodexResponses(url) ? OPENAI_CODEX_RESPONSES_ENDPOINT : url.toString();
            return await fetcher(rewrittenUrl, {
                ...requestInit,
                method: requestInit?.method ?? request?.method,
                body: requestInit?.body ?? request?.body ?? undefined,
                headers,
            });
        };
        return oauthFetch;
    }
    static normalizeRequestUrl(requestInput) {
        if (requestInput instanceof URL) {
            return requestInput;
        }
        if (requestInput instanceof Request) {
            return new URL(requestInput.url);
        }
        return new URL(String(requestInput));
    }
    static shouldRouteToCodexResponses(url) {
        return url.pathname.endsWith('/responses') || url.pathname.endsWith('/chat/completions');
    }
}
/**
 * Executes and parses Codex account-mode SSE requests routed through OpenAI
 * OAuth. This is provider-specific support for external context tools.
 */
export class OpenAiCodexSseService {
    static async execute(args) {
        if (!args.oauthFetch) {
            throw new Error('Missing OAuth fetch implementation for OpenAI Codex request.');
        }
        const response = await args.oauthFetch(args.endpoint ?? OPENAI_CODEX_RESPONSES_ENDPOINT, {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify(args.body),
        });
        if (!response.ok) {
            const failureBody = await response.text();
            const failure = new Error(failureBody || `${response.status} status code (no body)`);
            failure.status = response.status;
            throw failure;
        }
        return await response.text();
    }
    static extractOutputText(text) {
        const matches = [...text.matchAll(/^data: (\{.*"type":"response\.output_text\.done".*\})$/gm)];
        for (let index = matches.length - 1; index >= 0; index -= 1) {
            const raw = matches[index]?.[1];
            if (!raw) {
                continue;
            }
            try {
                const parsed = JSON.parse(raw);
                if (typeof parsed.text === 'string' && parsed.text.trim()) {
                    return parsed.text;
                }
            }
            catch {
                continue;
            }
        }
        return '';
    }
    static extractItems(text, predicate) {
        const items = [];
        const matches = [...text.matchAll(/^data: (\{.*"type":"response\.output_item\.done".*\})$/gm)];
        for (const match of matches) {
            const raw = match[1];
            if (!raw) {
                continue;
            }
            try {
                const parsed = JSON.parse(raw);
                if (predicate(parsed.item)) {
                    items.push(parsed.item);
                }
            }
            catch {
                continue;
            }
        }
        return items;
    }
}
//# sourceMappingURL=openai-adapter.js.map