import { withRetry } from '../utils/retry.js';
import { instrumentedLlmCall } from '../runtime/llm-observability.js';
import { ProviderError } from '../types/errors.js';
import { OpenAICompatProvider } from './openai-compat.js';
import { mapLlamaCppStopReason } from './stop-reason-maps.js';
import { summarizeError, toProviderError } from '../utils/error-display.js';
import { instrumentedFetch } from '../utils/fetch-with-timeout.js';
import { extractTextToolCalls, fromOpenAIToolCalls, toOpenAIMessages, toOpenAITools, } from './tool-formats.js';
export class LlamaCppProvider {
    name;
    models;
    capabilities;
    defaultModel;
    reasoningFormat;
    nativeChatUrl;
    nativeFetch;
    compatProvider;
    apiKey;
    defaultHeaders;
    constructor(opts) {
        this.name = opts.name;
        this.models = opts.models;
        this.capabilities = opts.capabilities;
        this.defaultModel = opts.defaultModel;
        this.reasoningFormat = opts.reasoningFormat ?? 'none';
        this.nativeChatUrl = deriveLlamaCppChatUrl(opts.baseURL);
        this.nativeFetch = opts.nativeFetch ?? instrumentedFetch;
        this.compatProvider = opts.compatProvider ?? new OpenAICompatProvider(opts);
        this.apiKey = opts.apiKey;
        this.defaultHeaders = opts.defaultHeaders;
    }
    isConfigured() {
        return this.compatProvider.isConfigured?.() ?? true;
    }
    async chat(params) {
        return (await instrumentedLlmCall(() => withRetry(async () => {
            if (!shouldUseNonStreamingLlamaCpp(params)) {
                return this.compatProvider.chat(params);
            }
            return this.chatViaNonStreamingCompat(params, params.model || this.defaultModel);
        }), { provider: this.name, model: params.model || this.defaultModel })).result;
    }
    async embed(request) {
        if (!this.compatProvider.embed) {
            throw new ProviderError('llama.cpp OpenAI-compatible transport does not support embeddings.', {
                statusCode: 501,
                provider: this.name,
                operation: 'embed',
                phase: 'response',
            });
        }
        return this.compatProvider.embed(request);
    }
    async describeRuntime(deps) {
        const { buildStandardProviderAuthRoutes } = await import('./runtime-metadata.js');
        const authRoutes = await buildStandardProviderAuthRoutes({
            providerId: this.name,
            allowAnonymous: !this.apiKey,
            anonymousConfigured: !this.apiKey,
            anonymousDetail: 'Local llama.cpp servers are often exposed without authentication.',
        }, deps);
        return {
            auth: {
                mode: this.apiKey ? 'api-key' : 'anonymous',
                configured: true,
                detail: this.apiKey
                    ? 'llama.cpp OpenAI-compatible endpoint has an API key configured.'
                    : 'llama.cpp OpenAI-compatible endpoint is running without an API key.',
                routes: authRoutes,
            },
            models: {
                defaultModel: this.defaultModel,
                models: this.models,
            },
            usage: {
                streaming: true,
                toolCalling: true,
                parallelTools: true,
                promptCaching: false,
                notes: ['llama.cpp uses a non-streaming recovery path for tool turns and delegates embeddings to the configured embedding provider.'],
            },
            policy: {
                local: true,
                streamProtocol: 'openai-chat-completions',
                reasoningMode: this.reasoningFormat ?? 'provider-default',
                supportedReasoningEfforts: ['instant', 'low', 'medium', 'high'],
                cacheStrategy: 'provider-managed',
            },
        };
    }
    async chatViaNonStreamingCompat(params, model) {
        const extraBody = buildReasoningBody(this.reasoningFormat, params.reasoningEffort);
        const body = {
            model,
            messages: toOpenAIMessages(params.messages, params.systemPrompt),
            ...(params.tools && params.tools.length > 0 ? { tools: toOpenAITools(params.tools) } : {}),
            ...(params.maxTokens ? { max_tokens: params.maxTokens } : {}),
            stream: false,
            ...extraBody,
        };
        let response;
        try {
            response = await this.nativeFetch(this.nativeChatUrl, {
                method: 'POST',
                headers: buildHeaders(this.apiKey, this.defaultHeaders),
                body: JSON.stringify(body),
                ...(params.signal !== undefined ? { signal: params.signal } : {}),
            });
        }
        catch (err) {
            throw normalizeProviderError(err, this.name, 'chat', 'request');
        }
        if (!response.ok) {
            throw await buildHttpError('llama.cpp chat', response, this.name, 'chat', 'request');
        }
        let payload;
        try {
            payload = await response.json();
        }
        catch (err) {
            throw new ProviderError(`llama.cpp chat returned invalid JSON: ${summarizeError(err)}`, {
                statusCode: 502,
                provider: this.name,
                operation: 'chat',
                phase: 'response',
            });
        }
        const choice = payload.choices?.[0];
        const message = choice?.message ?? {};
        const reasoningText = extractMessageText(message.reasoning_content) ?? extractMessageText(message.reasoning) ?? '';
        let responseText = extractMessageText(message.content) ?? '';
        let structuredToolCalls = normalizeOpenAIToolCalls(message.tool_calls);
        let finalToolCalls = structuredToolCalls.length > 0 ? fromOpenAIToolCalls(structuredToolCalls) : [];
        if (reasoningText) {
            params.onDelta?.({ reasoning: reasoningText });
        }
        if (structuredToolCalls.length > 0) {
            emitToolCallDeltas(structuredToolCalls, params);
        }
        if (finalToolCalls.length === 0 && (responseText.includes('<|toolcallbegin|>') || responseText.includes('<|tool_call_begin|>'))) {
            const extracted = extractTextToolCalls(responseText);
            if (extracted.toolCalls.length > 0) {
                finalToolCalls = extracted.toolCalls;
                responseText = extracted.cleanedContent;
                emitToolCallDeltas(finalToolCalls.map((call) => ({
                    id: call.id,
                    type: 'function',
                    function: {
                        name: call.name,
                        arguments: JSON.stringify(call.arguments),
                    },
                })), params);
            }
        }
        if (responseText) {
            params.onDelta?.({ content: responseText });
        }
        const stopReason = mapLlamaCppStopReason(choice?.finish_reason, finalToolCalls.length > 0);
        const rawStopReason = choice?.finish_reason ?? undefined;
        return {
            content: responseText,
            toolCalls: finalToolCalls,
            usage: {
                inputTokens: payload.usage?.prompt_tokens ?? 0,
                outputTokens: payload.usage?.completion_tokens ?? 0,
                ...(typeof payload.usage?.prompt_tokens_details?.cached_tokens === 'number'
                    ? { cacheReadTokens: payload.usage.prompt_tokens_details.cached_tokens }
                    : {}),
            },
            stopReason,
            ...(rawStopReason !== undefined ? { providerStopReason: rawStopReason } : {}),
        };
    }
}
function shouldUseNonStreamingLlamaCpp(params) {
    if ((params.tools?.length ?? 0) > 0)
        return true;
    return params.messages.some((message) => (message.role === 'tool'
        || (message.role === 'assistant' && (message.toolCalls?.length ?? 0) > 0)));
}
function deriveLlamaCppChatUrl(baseURL) {
    const trimmed = baseURL.replace(/\/+$/, '');
    if (trimmed.endsWith('/chat/completions'))
        return trimmed;
    if (trimmed.endsWith('/v1'))
        return `${trimmed}/chat/completions`;
    return `${trimmed}/v1/chat/completions`;
}
function buildReasoningBody(reasoningFormat, reasoningEffort) {
    if (reasoningFormat === 'llamacpp') {
        return {
            enable_thinking: reasoningEffort !== undefined && reasoningEffort !== 'instant',
        };
    }
    if (reasoningFormat === 'openrouter' && reasoningEffort) {
        return { reasoning: { effort: reasoningEffort } };
    }
    if (reasoningFormat === 'mercury' && reasoningEffort) {
        return { reasoning_effort: reasoningEffort };
    }
    return {};
}
function buildHeaders(apiKey, defaultHeaders) {
    return {
        accept: 'application/json',
        'content-type': 'application/json',
        ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
        ...(defaultHeaders ?? {}),
    };
}
function extractMessageText(value) {
    if (typeof value === 'string')
        return value;
    if (!Array.isArray(value))
        return null;
    const fragments = [];
    for (const entry of value) {
        if (typeof entry === 'string') {
            fragments.push(entry);
            continue;
        }
        if (!entry || typeof entry !== 'object')
            continue;
        const record = entry;
        const text = record.text ?? record.content ?? record.delta;
        if (typeof text === 'string' && text.length > 0) {
            fragments.push(text);
        }
    }
    return fragments.length > 0 ? fragments.join('') : null;
}
function normalizeOpenAIToolCalls(value) {
    if (!Array.isArray(value))
        return [];
    return value.flatMap((entry, index) => {
        if (!entry || typeof entry !== 'object')
            return [];
        const record = entry;
        const fn = record.function;
        if (!fn || typeof fn !== 'object')
            return [];
        const functionRecord = fn;
        const name = typeof functionRecord.name === 'string' ? functionRecord.name : '';
        if (!name)
            return [];
        const rawArguments = functionRecord.arguments;
        const argumentsText = typeof rawArguments === 'string'
            ? rawArguments
            : JSON.stringify(rawArguments ?? {});
        return [{
                id: typeof record.id === 'string' && record.id.length > 0 ? record.id : `llamacpp_call_${index}`,
                type: 'function',
                function: {
                    name,
                    arguments: argumentsText,
                },
            }];
    });
}
function emitToolCallDeltas(toolCalls, params) {
    toolCalls.forEach((toolCall, index) => {
        const partial = {
            index,
            id: toolCall.id,
            name: toolCall.function.name,
            arguments: toolCall.function.arguments,
        };
        params.onDelta?.({ toolCalls: [partial] });
    });
}
async function buildHttpError(prefix, response, provider, operation, phase) {
    const text = await response.text();
    try {
        const parsed = JSON.parse(text);
        const error = toRecord(parsed.error);
        if (Object.keys(error).length > 0) {
            const code = typeof error.code === 'string' ? `${error.code}: ` : '';
            const message = typeof error.message === 'string' ? error.message : text;
            return new ProviderError(`${prefix} error ${response.status}: ${code}${message}`, {
                statusCode: response.status,
                provider,
                operation,
                phase,
            });
        }
    }
    catch {
        // fall through
    }
    return new ProviderError(`${prefix} error ${response.status}: ${text || response.statusText}`, {
        statusCode: response.status,
        provider,
        operation,
        phase,
    });
}
function normalizeProviderError(err, provider, operation, phase = 'request') {
    const status = getErrorStatus(err);
    return toProviderError(err, {
        ...(status !== undefined ? { statusCode: status } : {}),
        provider,
        operation,
        phase,
    });
}
function getErrorStatus(err) {
    if (err && typeof err === 'object') {
        const record = err;
        if (typeof record.status === 'number')
            return record.status;
        if (typeof record.statusCode === 'number')
            return record.statusCode;
    }
    return undefined;
}
function getErrorMessage(err) {
    return summarizeError(err);
}
function toRecord(value) {
    return value && typeof value === 'object' ? value : {};
}
