/**
 * Shared utilities for Google Generative AI and Google Cloud Code Assist providers.
 */
import { FinishReason, FunctionCallingConfigMode } from "@google/genai";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { transformMessages } from "./transform-messages.js";
/**
 * Determines whether a streamed Gemini `Part` should be treated as "thinking".
 *
 * Protocol note (Gemini / Vertex AI thought signatures):
 * - `thought: true` is the definitive marker for thinking content (thought summaries).
 * - `thoughtSignature` is an encrypted representation of the model's internal thought process
 *   used to preserve reasoning context across multi-turn interactions.
 * - `thoughtSignature` can appear on ANY part type (text, functionCall, etc.) - it does NOT
 *   indicate the part itself is thinking content.
 * - For non-functionCall responses, the signature appears on the last part for context replay.
 * - When persisting/replaying model outputs, signature-bearing parts must be preserved as-is;
 *   do not merge/move signatures across parts.
 *
 * See: https://ai.google.dev/gemini-api/docs/thought-signatures
 */
export function isThinkingPart(part) {
    return part.thought === true;
}
/**
 * Retain thought signatures during streaming.
 *
 * Some backends only send `thoughtSignature` on the first delta for a given part/block; later deltas may omit it.
 * This helper preserves the last non-empty signature for the current block.
 *
 * Note: this does NOT merge or move signatures across distinct response parts. It only prevents
 * a signature from being overwritten with `undefined` within the same streamed block.
 */
export function retainThoughtSignature(existing, incoming) {
    if (typeof incoming === "string" && incoming.length > 0)
        return incoming;
    return existing;
}
// Thought signatures must be base64 for Google APIs (TYPE_BYTES).
const base64SignaturePattern = /^[A-Za-z0-9+/]+={0,2}$/;
// Sentinel value that tells the Gemini API to skip thought signature validation.
// Used for unsigned function call parts (e.g. replayed from providers without thought signatures).
// See: https://ai.google.dev/gemini-api/docs/thought-signatures
const SKIP_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
function isValidThoughtSignature(signature) {
    if (!signature)
        return false;
    if (signature.length % 4 !== 0)
        return false;
    return base64SignaturePattern.test(signature);
}
/**
 * Only keep signatures from the same provider/model and with valid base64.
 */
function resolveThoughtSignature(isSameProviderAndModel, signature) {
    return isSameProviderAndModel && isValidThoughtSignature(signature) ? signature : undefined;
}
/**
 * Models via Google APIs that require explicit tool call IDs in function calls/responses.
 */
export function requiresToolCallId(modelId) {
    return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-");
}
function getGeminiMajorVersion(modelId) {
    const match = modelId.toLowerCase().match(/^gemini(?:-live)?-(\d+)/);
    if (!match)
        return undefined;
    return Number.parseInt(match[1], 10);
}
function supportsMultimodalFunctionResponse(modelId) {
    const geminiMajorVersion = getGeminiMajorVersion(modelId);
    if (geminiMajorVersion !== undefined) {
        return geminiMajorVersion >= 3;
    }
    return true;
}
/**
 * Convert internal messages to Gemini Content[] format.
 */
export function convertMessages(model, context) {
    const contents = [];
    const normalizeToolCallId = (id) => {
        if (!requiresToolCallId(model.id))
            return id;
        return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
    };
    const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
    for (const msg of transformedMessages) {
        if (msg.role === "user") {
            if (typeof msg.content === "string") {
                contents.push({
                    role: "user",
                    parts: [{ text: sanitizeSurrogates(msg.content) }],
                });
            }
            else {
                const parts = msg.content.map((item) => {
                    if (item.type === "text") {
                        return { text: sanitizeSurrogates(item.text) };
                    }
                    else {
                        return {
                            inlineData: {
                                mimeType: item.mimeType,
                                data: item.data,
                            },
                        };
                    }
                });
                const filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts;
                if (filteredParts.length === 0)
                    continue;
                contents.push({
                    role: "user",
                    parts: filteredParts,
                });
            }
        }
        else if (msg.role === "assistant") {
            const parts = [];
            // Check if message is from same provider and model - only then keep thinking blocks
            const isSameProviderAndModel = msg.provider === model.provider && msg.model === model.id;
            for (const block of msg.content) {
                if (block.type === "text") {
                    // Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity)
                    if (!block.text || block.text.trim() === "")
                        continue;
                    const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.textSignature);
                    parts.push({
                        text: sanitizeSurrogates(block.text),
                        ...(thoughtSignature && { thoughtSignature }),
                    });
                }
                else if (block.type === "thinking") {
                    // Skip empty thinking blocks
                    if (!block.thinking || block.thinking.trim() === "")
                        continue;
                    // Only keep as thinking block if same provider AND same model
                    // Otherwise convert to plain text (no tags to avoid model mimicking them)
                    if (isSameProviderAndModel) {
                        const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature);
                        parts.push({
                            thought: true,
                            text: sanitizeSurrogates(block.thinking),
                            ...(thoughtSignature && { thoughtSignature }),
                        });
                    }
                    else {
                        parts.push({
                            text: sanitizeSurrogates(block.thinking),
                        });
                    }
                }
                else if (block.type === "toolCall") {
                    const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
                    // Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled.
                    // Use the skip_thought_signature_validator sentinel for unsigned function calls
                    // (e.g. replayed from providers without thought signatures like Claude via Antigravity).
                    const isGemini3 = model.id.toLowerCase().includes("gemini-3");
                    const effectiveSignature = thoughtSignature || (isGemini3 ? SKIP_THOUGHT_SIGNATURE : undefined);
                    const part = {
                        functionCall: {
                            name: block.name,
                            args: block.arguments ?? {},
                            ...(requiresToolCallId(model.id) ? { id: block.id } : {}),
                        },
                        ...(effectiveSignature && { thoughtSignature: effectiveSignature }),
                    };
                    parts.push(part);
                }
            }
            if (parts.length === 0)
                continue;
            contents.push({
                role: "model",
                parts,
            });
        }
        else if (msg.role === "toolResult") {
            // Extract text and image content
            const textContent = msg.content.filter((c) => c.type === "text");
            const textResult = textContent.map((c) => c.text).join("\n");
            const imageContent = model.input.includes("image")
                ? msg.content.filter((c) => c.type === "image")
                : [];
            const hasText = textResult.length > 0;
            const hasImages = imageContent.length > 0;
            // Gemini 3+ models support multimodal function responses with images nested inside
            // functionResponse.parts. Claude and other non-Gemini models behind Cloud Code Assist /
            // Antigravity also accept this shape. Gemini < 3 still needs a separate user image turn.
            const modelSupportsMultimodalFunctionResponse = supportsMultimodalFunctionResponse(model.id);
            // Use "output" key for success, "error" key for errors as per SDK documentation
            const responseValue = hasText ? sanitizeSurrogates(textResult) : hasImages ? "(see attached image)" : "";
            const imageParts = imageContent.map((imageBlock) => ({
                inlineData: {
                    mimeType: imageBlock.mimeType,
                    data: imageBlock.data,
                },
            }));
            const includeId = requiresToolCallId(model.id);
            const functionResponsePart = {
                functionResponse: {
                    name: msg.toolName,
                    response: msg.isError ? { error: responseValue } : { output: responseValue },
                    ...(hasImages && modelSupportsMultimodalFunctionResponse && { parts: imageParts }),
                    ...(includeId ? { id: msg.toolCallId } : {}),
                },
            };
            // Cloud Code Assist API requires all function responses to be in a single user turn.
            // Check if the last content is already a user turn with function responses and merge.
            const lastContent = contents[contents.length - 1];
            if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) {
                lastContent.parts.push(functionResponsePart);
            }
            else {
                contents.push({
                    role: "user",
                    parts: [functionResponsePart],
                });
            }
            // For Gemini < 3, add images in a separate user message
            if (hasImages && !modelSupportsMultimodalFunctionResponse) {
                contents.push({
                    role: "user",
                    parts: [{ text: "Tool result image:" }, ...imageParts],
                });
            }
        }
    }
    return contents;
}
/**
 * Convert tools to Gemini function declarations format.
 *
 * By default uses `parametersJsonSchema` which supports full JSON Schema (including
 * anyOf, oneOf, const, etc.). Set `useParameters` to true to use the legacy `parameters`
 * field instead (OpenAPI 3.03 Schema). This is needed for Cloud Code Assist with Claude
 * models, where the API translates `parameters` into Anthropic's `input_schema`.
 */
export function convertTools(tools, useParameters = false) {
    if (tools.length === 0)
        return undefined;
    return [
        {
            functionDeclarations: tools.map((tool) => ({
                name: tool.name,
                description: tool.description,
                ...(useParameters ? { parameters: tool.parameters } : { parametersJsonSchema: tool.parameters }),
            })),
        },
    ];
}
/**
 * Map tool choice string to Gemini FunctionCallingConfigMode.
 */
export function mapToolChoice(choice) {
    switch (choice) {
        case "auto":
            return FunctionCallingConfigMode.AUTO;
        case "none":
            return FunctionCallingConfigMode.NONE;
        case "any":
            return FunctionCallingConfigMode.ANY;
        default:
            return FunctionCallingConfigMode.AUTO;
    }
}
/**
 * Map Gemini FinishReason to our StopReason.
 */
export function mapStopReason(reason) {
    switch (reason) {
        case FinishReason.STOP:
            return "stop";
        case FinishReason.MAX_TOKENS:
            return "length";
        case FinishReason.BLOCKLIST:
        case FinishReason.PROHIBITED_CONTENT:
        case FinishReason.SPII:
        case FinishReason.SAFETY:
        case FinishReason.IMAGE_SAFETY:
        case FinishReason.IMAGE_PROHIBITED_CONTENT:
        case FinishReason.IMAGE_RECITATION:
        case FinishReason.IMAGE_OTHER:
        case FinishReason.RECITATION:
        case FinishReason.FINISH_REASON_UNSPECIFIED:
        case FinishReason.OTHER:
        case FinishReason.LANGUAGE:
        case FinishReason.MALFORMED_FUNCTION_CALL:
        case FinishReason.UNEXPECTED_TOOL_CALL:
        case FinishReason.NO_IMAGE:
            return "error";
        default: {
            const _exhaustive = reason;
            throw new Error(`Unhandled stop reason: ${_exhaustive}`);
        }
    }
}
/**
 * Map string finish reason to our StopReason (for raw API responses).
 */
export function mapStopReasonString(reason) {
    switch (reason) {
        case "STOP":
            return "stop";
        case "MAX_TOKENS":
            return "length";
        default:
            return "error";
    }
}
//# sourceMappingURL=google-shared.js.map