import { REASONING_BUDGET_MAP } from './interface.js';
import { mapGeminiStopReason } from './stop-reason-maps.js';
import { ProviderError } from '../types/errors.js';
import { withRetry } from '../utils/retry.js';
import { logger } from '../utils/logger.js';
import { fetchWithTimeout, instrumentedFetch } from '../utils/fetch-with-timeout.js';
import { instrumentedLlmCall } from '../runtime/llm-observability.js';
import { toGeminiFunctionDeclarations, toGeminiContents, fromGeminiParts, } from './tool-formats.js';
import { summarizeError, toProviderError } from '../utils/error-display.js';
const GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta';
const GEMINI_CACHE_TTL_SECONDS = 3600;
const NOOP_CACHE_HIT_TRACKER = {
    recordTurn: () => { },
};
/**
 * GeminiProvider — calls the Gemini generateContent API directly via fetch.
 * Tools are `functionDeclarations` inside a `tools` array.
 * Tool calls come as `functionCall` parts; results as `functionResponse` parts.
 * Uses streamGenerateContent for real-time token delivery when onDelta is provided.
 */
export class GeminiProvider {
    name = 'gemini';
    /** Maps function call name → thoughtSignature for the current turn. */
    thoughtSignatures = new Map();
    models = [];
    apiKey;
    embeddingModel = 'gemini-embedding-001';
    cacheHitTracker;
    /** Active cached content resource name (e.g., "cachedContents/abc123") */
    cachedContentName = null;
    /** Hash of the content that was cached (systemPrompt + tools + model) */
    cachedContentHash = null;
    /** When the cache expires (epoch ms) */
    cachedContentExpiry = 0;
    /** Hashes known to be below the 32K cache minimum — skip API call */
    uncacheableHashes = new Set();
    constructor(apiKey, cacheHitTracker = NOOP_CACHE_HIT_TRACKER) {
        this.apiKey = apiKey;
        this.cacheHitTracker = cacheHitTracker;
    }
    computeCacheHash(systemPrompt, tools, model) {
        const raw = (systemPrompt ?? '') + JSON.stringify(tools ?? []) + model;
        const hasher = new Bun.CryptoHasher('sha256');
        hasher.update(raw);
        return hasher.digest('hex');
    }
    async ensureCachedContent(systemPrompt, tools, model) {
        // Skip if no system prompt and no tools
        if (!systemPrompt && (!tools || tools.length === 0))
            return null;
        const hash = this.computeCacheHash(systemPrompt, tools, model);
        // Skip if previously determined to be below 32K threshold
        if (this.uncacheableHashes.has(hash))
            return null;
        // Reuse existing cache if hash matches and not expired (with 60s buffer)
        if (this.cachedContentName &&
            this.cachedContentHash === hash &&
            this.cachedContentExpiry > Date.now() + 60_000) {
            return this.cachedContentName;
        }
        // Estimate tokens — skip if below 28K (conservative buffer below 32K minimum)
        const estimatedChars = (systemPrompt?.length ?? 0) + JSON.stringify(tools ?? []).length;
        if (estimatedChars / 3 < 28_000) {
            if (this.uncacheableHashes.size >= 50)
                this.uncacheableHashes.clear();
            this.uncacheableHashes.add(hash);
            logger.debug('[Gemini] Content below 32K cache threshold, skipping cache', {
                estimatedTokens: Math.round(estimatedChars / 3),
            });
            return null;
        }
        // Delete the previous cache if the prompt hash changed.
        if (this.cachedContentName && this.cachedContentHash !== hash) {
            const oldName = this.cachedContentName;
            fetchWithTimeout(`${GEMINI_API_BASE}/${oldName}`, {
                method: 'DELETE',
                headers: { 'x-goog-api-key': this.apiKey },
            }).catch(err => logger.warn('[Gemini] Failed to delete previous cache', { error: summarizeError(err) }));
        }
        // Create new cached content
        try {
            const cacheBody = {
                model: `models/${model}`,
                ttl: `${GEMINI_CACHE_TTL_SECONDS}s`,
            };
            if (systemPrompt) {
                cacheBody['systemInstruction'] = { parts: [{ text: systemPrompt }] };
            }
            if (tools && tools.length > 0) {
                cacheBody['tools'] = [{ functionDeclarations: toGeminiFunctionDeclarations(tools) }];
            }
            const res = await fetchWithTimeout(`${GEMINI_API_BASE}/cachedContents`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-goog-api-key': this.apiKey,
                },
                body: JSON.stringify(cacheBody),
            });
            if (!res.ok) {
                const text = await res.text().catch(() => '');
                if (text.includes('too few tokens') || text.includes('minimum')) {
                    if (this.uncacheableHashes.size >= 50)
                        this.uncacheableHashes.clear();
                    this.uncacheableHashes.add(hash);
                    logger.debug('[Gemini] Content below cache minimum', { status: res.status, error: text.slice(0, 200) });
                }
                else {
                    logger.warn('[Gemini] Cache creation failed', { status: res.status, error: text.slice(0, 200) });
                }
                return null;
            }
            const data = await res.json();
            this.cachedContentName = data.name;
            this.cachedContentHash = hash;
            this.cachedContentExpiry = new Date(data.expireTime).getTime();
            logger.info(`[Gemini] Created cache: ${data.name} (expires ${data.expireTime})`);
            return data.name;
        }
        catch (err) {
            logger.warn('[Gemini] Cache creation error', { error: summarizeError(err) });
            return null;
        }
    }
    async chat(params) {
        const { messages, tools, model, maxTokens, signal, systemPrompt, onDelta, reasoningEffort } = params;
        return (await instrumentedLlmCall(() => withRetry(async () => {
            const { contents, systemInstruction } = toGeminiContents(messages, systemPrompt);
            // Inject thoughtSignatures into both model functionCall parts and user functionResponse parts
            // (Gemini thinking models require the signature on both sides of the round-trip)
            for (const c of contents) {
                for (const part of c.parts) {
                    const p = part;
                    if (p.functionCall) {
                        const fc = p.functionCall;
                        const sig = this.thoughtSignatures.get(fc.name);
                        if (sig)
                            p.thoughtSignature = sig;
                    }
                    if (p.functionResponse) {
                        const fr = p.functionResponse;
                        const sig = this.thoughtSignatures.get(fr.name);
                        if (sig)
                            p.thoughtSignature = sig;
                    }
                }
            }
            const body = { contents };
            const cachedName = await this.ensureCachedContent(systemPrompt, tools, model);
            if (cachedName) {
                // Cached content already contains systemInstruction and tools — do NOT resend them
                body['cachedContent'] = cachedName;
            }
            else {
                if (systemInstruction) {
                    body['systemInstruction'] = systemInstruction;
                }
                if (tools && tools.length > 0) {
                    body['tools'] = [{
                            functionDeclarations: toGeminiFunctionDeclarations(tools),
                        }];
                }
            }
            if (maxTokens) {
                body['generationConfig'] = { maxOutputTokens: maxTokens };
            }
            if (reasoningEffort) {
                const budget = REASONING_BUDGET_MAP[reasoningEffort];
                if (budget !== undefined) {
                    body['generationConfig'] = {
                        ...(body['generationConfig'] ?? {}),
                        thinking_config: { thinking_budget: budget },
                    };
                }
            }
            // Always use streaming endpoint; parse NDJSON chunks
            const url = `${GEMINI_API_BASE}/models/${model}:streamGenerateContent?alt=sse`;
            let res;
            try {
                res = await instrumentedFetch(url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'x-goog-api-key': this.apiKey,
                    },
                    body: JSON.stringify(body),
                    ...(signal !== undefined ? { signal } : {}),
                });
            }
            catch (err) {
                throw toProviderError(err, {
                    provider: this.name,
                    operation: 'chat',
                    phase: 'request',
                });
            }
            if (!res.ok) {
                const text = await res.text().catch(() => 'unknown error');
                throw new ProviderError(`Gemini API error ${res.status}: ${text}`, {
                    statusCode: res.status,
                    provider: this.name,
                    operation: 'chat',
                    phase: 'request',
                });
            }
            // Accumulate state from streaming chunks
            const allParts = [];
            let inputTokens = 0;
            let outputTokens = 0;
            let cacheReadTokens = 0;
            let lastFinishReason = '';
            let streamedText = '';
            const reader = res.body?.getReader();
            if (!reader) {
                throw new ProviderError('Gemini chat returned no response body.', {
                    statusCode: 502,
                    provider: this.name,
                    operation: 'chat',
                    phase: 'response',
                });
            }
            const decoder = new TextDecoder();
            let buffer = '';
            try {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done)
                        break;
                    buffer += decoder.decode(value, { stream: true });
                    const lines = buffer.split('\n');
                    buffer = lines.pop() ?? '';
                    for (const line of lines) {
                        if (!line.startsWith('data: '))
                            continue;
                        const data = line.slice(6).trim();
                        if (!data || data === '[DONE]')
                            continue;
                        let chunk;
                        try {
                            chunk = JSON.parse(data);
                        }
                        catch {
                            logger.warn('Gemini SSE: failed to parse JSON chunk', {
                                chunkPreview: data.slice(0, 200),
                                chunkLength: data.length,
                            });
                            continue;
                        }
                        const candidate = chunk.candidates?.[0];
                        if (candidate) {
                            const parts = candidate.content?.parts ?? [];
                            for (const part of parts) {
                                allParts.push(part);
                                if (part.text && onDelta) {
                                    streamedText += part.text;
                                    onDelta({ content: part.text });
                                }
                                if (part.functionCall) {
                                    // Capture thoughtSignature if present (Gemini thinking models)
                                    if (part.thoughtSignature) {
                                        this.thoughtSignatures.set(part.functionCall.name, part.thoughtSignature);
                                    }
                                    if (onDelta) {
                                        onDelta({ toolCalls: [{ index: 0, name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args) }] });
                                    }
                                }
                            }
                            if (candidate.finishReason) {
                                lastFinishReason = candidate.finishReason;
                            }
                        }
                        if (chunk.usageMetadata) {
                            inputTokens = chunk.usageMetadata.promptTokenCount ?? inputTokens;
                            outputTokens = chunk.usageMetadata.candidatesTokenCount ?? outputTokens;
                            cacheReadTokens = chunk.usageMetadata.cachedContentTokenCount ?? cacheReadTokens;
                        }
                    }
                }
            }
            finally {
                reader.releaseLock();
            }
            // Use streamed text directly if available (avoids re-parsing duplicated text parts)
            const { text: parsedText, toolCalls } = fromGeminiParts(allParts);
            // Prefer streamedText for content; fall back to parsed if no streaming happened
            const text = streamedText || parsedText;
            let stopReason = toolCalls.length > 0
                ? 'tool_call'
                : mapGeminiStopReason(lastFinishReason);
            // Clear stale signatures; fresh ones were captured from this response's functionCall parts.
            // (kept across calls within a tool-use loop, cleared when no new functionCalls arrive)
            if (toolCalls.length === 0) {
                this.thoughtSignatures.clear();
            }
            this.cacheHitTracker.recordTurn({
                inputTokens,
                cacheReadTokens,
            });
            return {
                content: text,
                toolCalls,
                usage: {
                    inputTokens,
                    outputTokens,
                    ...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
                    // cacheWriteTokens is omitted: Gemini does not charge separately for cache writes
                },
                stopReason,
                ...(lastFinishReason ? { providerStopReason: lastFinishReason } : {}),
            };
        }), { provider: 'gemini', model: model })).result;
    }
    async embed(request) {
        const model = request.model ?? this.embeddingModel;
        const body = {
            content: { parts: [{ text: request.text }] },
        };
        if (request.dimensions) {
            body['config'] = { outputDimensionality: request.dimensions };
        }
        let res;
        try {
            res = await instrumentedFetch(`${GEMINI_API_BASE}/models/${model}:embedContent`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-goog-api-key': this.apiKey,
                },
                body: JSON.stringify(body),
                ...(request.signal !== undefined ? { signal: request.signal } : {}),
            });
        }
        catch (error) {
            throw toProviderError(error, {
                provider: this.name,
                operation: 'embed',
                phase: 'request',
            });
        }
        if (!res.ok) {
            const text = await res.text().catch(() => 'unknown error');
            throw new ProviderError(`Gemini embeddings API error ${res.status}: ${text}`, {
                statusCode: res.status,
                provider: this.name,
                operation: 'embed',
                phase: 'request',
            });
        }
        const data = await res.json();
        const values = data.embedding?.values ?? [];
        return {
            vector: Float32Array.from(values),
            dimensions: values.length,
            modelId: model,
            metadata: {
                usage: request.usage,
                provider: this.name,
            },
        };
    }
    async describeRuntime(deps) {
        const { buildStandardProviderAuthRoutes } = await import('./runtime-metadata.js');
        const authRoutes = await buildStandardProviderAuthRoutes({
            providerId: 'gemini',
            apiKeyEnvVars: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_GEMINI_API_KEY'],
            serviceNames: ['gemini'],
        }, deps);
        return {
            auth: {
                mode: 'api-key',
                configured: Boolean(this.apiKey),
                detail: this.apiKey ? 'Gemini API key available' : 'Gemini API key is not configured',
                envVars: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_GEMINI_API_KEY'],
                routes: authRoutes,
            },
            models: {
                models: this.models,
                embeddingModel: this.embeddingModel,
                embeddingDimensions: 384,
            },
            usage: {
                streaming: true,
                toolCalling: true,
                parallelTools: true,
                notes: ['Embeddings use Gemini embedContent with reduced output dimensionality when requested.'],
            },
            policy: {
                local: false,
                streamProtocol: 'gemini-sse',
                reasoningMode: 'thinking_budget',
                supportedReasoningEfforts: ['instant', 'low', 'medium', 'high'],
                cacheStrategy: 'gemini-cached-content',
            },
        };
    }
}
