/**
 * GoogleAdapter -- Google Generative AI / Vertex AI transport adapter.
 *
 * Implements LlmProviderAdapter for both:
 *   - Google Generative Language API (generativelanguage.googleapis.com)
 *   - Google Vertex AI endpoint (<region>-aiplatform.googleapis.com)
 *
 * Wire format: Google Gen AI REST API v1beta
 * Spec: P3-SUB-01 AC 4-7 (GoogleAdapter)
 *
 * SDK note (AC 6): The spec mandates @google/genai SDK. That package is not
 * installed in this repository and its exact import surface could not be
 * verified without web access. This implementation uses direct REST calls to
 * the Google Gen AI v1beta REST endpoints, which expose identical semantics.
 * TODO (Vera/Legal + Kai): Once @google/genai is installed and its API shape
 * verified, replace the inline REST implementation with the SDK wrapper. Track
 * as DEFERRED-GOOGLE-SDK in the follow-up queue.
 *
 * Deployment targets:
 *   'vertex-ai'            -- enterprise Vertex AI endpoint (default, has BAA)
 *   'generative-language'  -- Google AI Studio / direct API key endpoint
 *                            (no BAA -- emits WARN audit event on init)
 *
 * Auth:
 *   vertex-ai:            apiKey = service-account JSON or OAuth2 bearer token
 *   generative-language:  apiKey = Google AI Studio API key (x-goog-api-key header)
 *
 * @connexum/ai-governance
 */
import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
import { BaseProviderAdapter, registerProviderCompliance } from './base.js';
import { LlmProviderError, LlmAuthError, LlmRateLimitError, LlmContentFilterError, LlmModelNotFoundError, } from './index.js';
import { detectUnexpectedShape, normalizeToolUseRequest, normalizeToolUseResponse, parseRetryAfterMs, } from './shared.js';
// ---------------------------------------------------------------------------
// Constants (AC 26 -- version pinning)
// ---------------------------------------------------------------------------
/**
 * Google Gen AI REST API version this adapter was tested against.
 * Corresponds to the v1beta REST surface (also used by @google/genai SDK).
 * TODO: update when AC 6 SDK migration is complete.
 */
export const TESTED_API_VERSION = 'v1beta';
const _GEMMA_PREFIXES = ['gemma-', 'paligemma-'];
const _GEMINI_PREFIX = 'gemini-';
/**
 * Lexical family detection. Returns `'unknown'` for IDs that match
 * neither prefix. Caller can still pass an unknown-family modelId to
 * the adapter; this helper just classifies.
 */
export function getGoogleModelFamily(modelId) {
    if (typeof modelId !== 'string' || modelId.length === 0)
        return 'unknown';
    const lower = modelId.toLowerCase();
    if (_GEMMA_PREFIXES.some(p => lower.startsWith(p)))
        return 'gemma';
    if (lower.startsWith(_GEMINI_PREFIX))
        return 'gemini';
    return 'unknown';
}
/**
 * Per-model metadata bundle. Combines the lexical family with the
 * deployment-target BAA fact so callers (capability registry, audit
 * chain entry) get a single record per call.
 */
export function getGoogleModelMetadata(modelId, deploymentTarget) {
    const family = getGoogleModelFamily(modelId);
    const isOpenWeight = family === 'gemma';
    // BAA inheritance is governed by deployment target, not family. Both
    // Gemini and Gemma served via Vertex AI inherit Vertex's BAA.
    const baaInherits = deploymentTarget === 'vertex-ai';
    let displayName;
    if (family === 'gemma') {
        displayName = `Google Gemma (open-weight): ${modelId}`;
    }
    else if (family === 'gemini') {
        displayName = `Google Gemini: ${modelId}`;
    }
    else {
        displayName = `Google model: ${modelId}`;
    }
    return { family, isOpenWeight, baaInherits, displayName };
}
/**
 * The list of Gemma model IDs the adapter is known to handle without
 * surprise. Customers can pass other Gemma IDs and the adapter will
 * still route the request — this list drives enumerateModels() output
 * and the capability-bundle registration.
 *
 * Source: Google AI Studio + Vertex AI Model Garden Gemma 2 / paligemma
 * listings.
 *
 * F-NEW-GEMMA-VISION (2026-05-09): paligemma family is multimodal
 * (image+text in, text out). The text-only Gemma 2 variants (2b/9b/27b)
 * remain non-multimodal. multimodal flag is queried via
 * `isGemmaMultimodal()` so the adapter can reject array-shaped content
 * for text-only models with UNSUPPORTED_MULTIMODAL.
 */
export const KNOWN_GEMMA_MODEL_IDS = [
    // Text-only Gemma 2
    'gemma-2-2b-it',
    'gemma-2-9b-it',
    'gemma-2-27b-it',
    // Multimodal paligemma (F-NEW-GEMMA-VISION)
    'paligemma-3b-mix-224',
    'paligemma-3b-mix-448',
    'paligemma-3b-pt-224',
    'paligemma-3b-pt-448',
    'paligemma-3b-pt-896',
];
/**
 * Subset of KNOWN_GEMMA_MODEL_IDS that are multimodal (image+text in).
 * The adapter consults this set to decide whether array-shaped
 * `LlmMessage.content` is permitted for the requested model.
 *
 * F-NEW-GEMMA-VISION 2026-05-09.
 */
export const KNOWN_GEMMA_VISION_MODEL_IDS = [
    'paligemma-3b-mix-224',
    'paligemma-3b-mix-448',
    'paligemma-3b-pt-224',
    'paligemma-3b-pt-448',
    'paligemma-3b-pt-896',
];
const _GEMMA_VISION_SET = new Set(KNOWN_GEMMA_VISION_MODEL_IDS);
const _PALIGEMMA_PREFIX = 'paligemma-';
/**
 * Returns true if the modelId names a known multimodal Gemma variant.
 * Falls back to a prefix check for forward compatibility with future
 * paligemma releases (paligemma2-*, etc.). Text-only Gemma 2 models
 * return false.
 *
 * F-NEW-GEMMA-VISION 2026-05-09.
 */
export function isGemmaMultimodal(modelId) {
    if (typeof modelId !== 'string' || modelId.length === 0)
        return false;
    if (_GEMMA_VISION_SET.has(modelId))
        return true;
    // Forward-compat: any paligemma-* ID is treated as multimodal.
    return modelId.toLowerCase().startsWith(_PALIGEMMA_PREFIX);
}
function httpRequest(urlStr, method, headers, body, timeoutMs) {
    return new Promise((resolve, reject) => {
        let url;
        try {
            url = new URL(urlStr);
        }
        catch {
            reject(new Error(`Invalid URL: ${urlStr}`));
            return;
        }
        const isHttps = url.protocol === 'https:';
        const transport = isHttps ? https : http;
        const options = {
            hostname: url.hostname,
            port: url.port || (isHttps ? 443 : 80),
            path: url.pathname + url.search,
            method,
            headers: {
                ...headers,
                'Content-Length': Buffer.byteLength(body).toString(),
            },
        };
        const req = transport.request(options, (res) => {
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => {
                resolve({
                    statusCode: res.statusCode ?? 0,
                    headers: res.headers,
                    body: Buffer.concat(chunks).toString('utf-8'),
                });
            });
            res.on('error', reject);
        });
        req.setTimeout(timeoutMs, () => {
            req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
        });
        req.on('error', reject);
        req.write(body);
        req.end();
    });
}
function getHeaderString(headers, name) {
    const val = headers[name.toLowerCase()];
    if (Array.isArray(val))
        return val[0];
    return val ?? undefined;
}
// ---------------------------------------------------------------------------
// Register Google compliance metadata on module load (AC 7)
// ---------------------------------------------------------------------------
registerProviderCompliance({
    providerId: 'google',
    baa: true,
    baaPath: 'vertex-ai-only',
    gdprDpa: true,
    soc2Type2: true,
    sovereignty: 'multi',
    attestationUrl: 'https://cloud.google.com/security/compliance/hipaa',
    // TODO (Vera/Legal, Kai): Verify this date against
    // https://cloud.google.com/security/compliance/hipaa before enabling
    // HIPAA pack with Google provider in any production deployment.
    // Date set to spec authoring date as a placeholder -- flag DEFERRED-GOOGLE-ATTEST.
    attestationDate: '2026-04-22',
    reviewCadence: '90d',
});
// ---------------------------------------------------------------------------
// GoogleAdapter
// ---------------------------------------------------------------------------
export class GoogleAdapter extends BaseProviderAdapter {
    adapterId = 'google';
    providerId = 'google';
    googleConfig = null;
    // ---------------------------------------------------------------------------
    // Lifecycle
    // ---------------------------------------------------------------------------
    initialize(config) {
        const cfg = config;
        const target = cfg.deploymentTarget ?? 'vertex-ai';
        // Fail-closed: credentials are required regardless of target
        if (!cfg.apiKey) {
            throw new LlmAuthError({
                message: 'GoogleAdapter: apiKey is required. ' +
                    'For vertex-ai: supply a service-account OAuth2 bearer token. ' +
                    'For generative-language: supply your Google AI Studio API key.',
                providerId: this.providerId,
                adapterId: this.adapterId,
            });
        }
        // vertex-ai requires projectId and region
        if (target === 'vertex-ai') {
            if (!cfg.projectId) {
                throw new LlmAuthError({
                    message: 'GoogleAdapter: projectId is required when deploymentTarget is vertex-ai.',
                    providerId: this.providerId,
                    adapterId: this.adapterId,
                });
            }
            if (!cfg.region) {
                throw new LlmAuthError({
                    message: 'GoogleAdapter: region is required when deploymentTarget is vertex-ai (e.g. us-central1).',
                    providerId: this.providerId,
                    adapterId: this.adapterId,
                });
            }
        }
        super.initialize(config);
        this.googleConfig = { ...cfg, deploymentTarget: target };
        // AC 5: emit WARN if using generative-language (no BAA)
        if (target === 'generative-language') {
            this.emit({
                type: 'llm:auth-failure', // reusing audit channel -- closest available type
                severity: 'WARN',
                adapterId: this.adapterId,
                providerId: this.providerId,
                detail: 'GoogleAdapter initialized with deploymentTarget=generative-language. ' +
                    'The Google Generative Language API (AI Studio) endpoint does not carry a HIPAA BAA. ' +
                    'BAA coverage is available only via Vertex AI. Do not use this target for PHI workloads.',
                timestamp: new Date().toISOString(),
                data: { deploymentTarget: target },
            });
        }
    }
    // ---------------------------------------------------------------------------
    // URL builder
    // ---------------------------------------------------------------------------
    buildUrl(modelId, suffix) {
        const cfg = this.googleConfig;
        const target = cfg.deploymentTarget ?? 'vertex-ai';
        if (cfg.baseUrl) {
            // Test / proxy override path
            const base = cfg.baseUrl.replace(/\/$/, '');
            return `${base}/${TESTED_API_VERSION}/models/${modelId}:${suffix}`;
        }
        if (target === 'vertex-ai') {
            // https://<region>-aiplatform.googleapis.com/v1/projects/<project>/locations/<region>/publishers/google/models/<model>:<action>
            // Note: Vertex AI uses v1, not v1beta, for stable endpoints.
            // We default to v1beta REST surface constant but override here for Vertex AI path.
            const region = cfg.region;
            const project = cfg.projectId;
            return (`https://${region}-aiplatform.googleapis.com/v1/projects/${project}` +
                `/locations/${region}/publishers/google/models/${modelId}:${suffix}`);
        }
        // generative-language (Google AI Studio)
        return `https://generativelanguage.googleapis.com/${TESTED_API_VERSION}/models/${modelId}:${suffix}`;
    }
    // ---------------------------------------------------------------------------
    // Auth headers
    // ---------------------------------------------------------------------------
    buildAuthHeaders() {
        const cfg = this.googleConfig;
        const target = cfg.deploymentTarget ?? 'vertex-ai';
        const headers = {
            'Content-Type': 'application/json',
        };
        if (target === 'vertex-ai') {
            // Vertex AI uses OAuth2 bearer tokens
            headers['Authorization'] = `Bearer ${cfg.apiKey}`;
        }
        else {
            // Generative Language uses API key header
            headers['x-goog-api-key'] = cfg.apiKey;
        }
        return headers;
    }
    // ---------------------------------------------------------------------------
    // Message conversion: LlmMessage[] -> GoogleContent[]
    // ---------------------------------------------------------------------------
    /**
     * Convert one LlmContentPart (vision-aware) to a GooglePart. Supports
     * three URL shapes for image_url:
     *   - data:<mime>;base64,<bytes>  → inlineData { mimeType, data }
     *   - gs://bucket/path            → fileData   { mimeType, fileUri }
     *   - https://...                 → fileData   { mimeType, fileUri }
     *                                   (Vertex AI fetches it; AI Studio
     *                                   does not — adapter passes it
     *                                   through and lets the provider
     *                                   reject if unsupported.)
     *
     * The MIME type is taken from the part's `mimeType` field if present,
     * otherwise inferred from the URL/data-URI prefix, otherwise defaults
     * to 'image/jpeg' (paligemma's training default). Inferred MIME is
     * advisory; provider may reject if mismatched.
     *
     * F-NEW-GEMMA-VISION 2026-05-09.
     */
    convertImagePart(part) {
        const url = part.image_url.url;
        if (typeof url !== 'string' || url.length === 0) {
            throw new LlmProviderError({
                message: 'GoogleAdapter: image_url.url is required and must be a non-empty string.',
                code: 'INVALID_REQUEST',
                providerId: this.providerId,
                adapterId: this.adapterId,
            });
        }
        // data: URI → inlineData (base64)
        if (url.startsWith('data:')) {
            // data:<mime>;base64,<payload>
            const match = /^data:([^;,]+);base64,(.+)$/i.exec(url);
            if (!match) {
                throw new LlmProviderError({
                    message: 'GoogleAdapter: image_url.url data: URI must be base64-encoded ' +
                        '(format: data:<mime>;base64,<payload>).',
                    code: 'INVALID_REQUEST',
                    providerId: this.providerId,
                    adapterId: this.adapterId,
                });
            }
            const inferredMime = match[1];
            const payload = match[2];
            return {
                inlineData: {
                    mimeType: part.image_url.mimeType ?? inferredMime ?? 'image/jpeg',
                    data: payload,
                },
            };
        }
        // gs:// → fileData (Vertex AI Cloud Storage reference)
        // https:// → fileData (Vertex AI may fetch; AI Studio rejects)
        if (url.startsWith('gs://') || url.startsWith('http://') || url.startsWith('https://')) {
            const inferredMime = this.inferMimeFromUrl(url);
            return {
                fileData: {
                    mimeType: part.image_url.mimeType ?? inferredMime ?? 'image/jpeg',
                    fileUri: url,
                },
            };
        }
        throw new LlmProviderError({
            message: `GoogleAdapter: image_url.url scheme not supported: '${url.slice(0, 40)}'. ` +
                'Use data:<mime>;base64,<payload>, gs://<bucket>/<path>, or https://<host>/<path>.',
            code: 'INVALID_REQUEST',
            providerId: this.providerId,
            adapterId: this.adapterId,
        });
    }
    inferMimeFromUrl(url) {
        const lower = url.toLowerCase();
        if (lower.endsWith('.png'))
            return 'image/png';
        if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
            return 'image/jpeg';
        if (lower.endsWith('.webp'))
            return 'image/webp';
        if (lower.endsWith('.heic'))
            return 'image/heic';
        if (lower.endsWith('.heif'))
            return 'image/heif';
        if (lower.endsWith('.gif'))
            return 'image/gif';
        return undefined;
    }
    /**
     * Convert one LlmMessage to its part list. Plain-string content is
     * wrapped to `[{ text }]`. Array content (multimodal) is mapped
     * part-by-part. Rejects array content for non-multimodal models with
     * UNSUPPORTED_MULTIMODAL.
     *
     * F-NEW-GEMMA-VISION 2026-05-09.
     */
    convertMessageToParts(message, modelId) {
        if (typeof message.content === 'string') {
            return [{ text: message.content }];
        }
        // Array content → vision shape. Reject if model is not multimodal.
        // Gemini 1.5+ is multimodal; paligemma-* is multimodal; everything
        // else (text-only Gemma 2, Gemma 1, gemini-pro-vision-deprecated)
        // gets rejected.
        if (!this.modelSupportsMultimodal(modelId)) {
            throw new LlmProviderError({
                message: `GoogleAdapter: model '${modelId}' does not support multimodal ` +
                    `(image) content. Use a vision-capable model (paligemma-*, ` +
                    `gemini-1.5-*, gemini-2.0-*, gemini-2.5-*) or pass a plain-string ` +
                    `message instead. Silently dropping image parts is a governance ` +
                    `violation (F-NEW-GEMMA-VISION).`,
                code: 'UNSUPPORTED_MULTIMODAL',
                providerId: this.providerId,
                adapterId: this.adapterId,
            });
        }
        const parts = [];
        for (const part of message.content) {
            if (part.type === 'text') {
                parts.push({ text: part.text });
            }
            else if (part.type === 'image_url') {
                parts.push(this.convertImagePart(part));
            }
            else {
                // Unknown part type — fail-closed rather than silently drop.
                const unknownType = part.type;
                throw new LlmProviderError({
                    message: `GoogleAdapter: unsupported content part type '${String(unknownType)}'.`,
                    code: 'INVALID_REQUEST',
                    providerId: this.providerId,
                    adapterId: this.adapterId,
                });
            }
        }
        if (parts.length === 0) {
            throw new LlmProviderError({
                message: 'GoogleAdapter: multimodal message contains no parts after conversion.',
                code: 'INVALID_REQUEST',
                providerId: this.providerId,
                adapterId: this.adapterId,
            });
        }
        return parts;
    }
    /**
     * Returns true if the requested model supports image content.
     * F-NEW-GEMMA-VISION 2026-05-09.
     */
    modelSupportsMultimodal(modelId) {
        if (isGemmaMultimodal(modelId))
            return true;
        // Gemini 1.5+ is multimodal; gemini-1.0-pro and gemini-pro are text-only.
        const lower = modelId.toLowerCase();
        if (lower.startsWith('gemini-1.5'))
            return true;
        if (lower.startsWith('gemini-2.'))
            return true;
        if (lower.startsWith('gemini-pro-vision'))
            return true; // deprecated but multimodal
        return false;
    }
    /**
     * Extract every image-URL string referenced by this request. Used by
     * the pre-call governance hook so pack classifiers see image URIs as
     * governance-relevant data alongside text. Returns the empty array
     * for text-only requests.
     *
     * Public so framework adapters and the runtime governance check can
     * read it without needing access to the private parts.
     *
     * F-NEW-GEMMA-VISION 2026-05-09.
     */
    static extractImageUrls(request) {
        const urls = [];
        for (const m of request.messages) {
            if (Array.isArray(m.content)) {
                for (const part of m.content) {
                    if (part && part.type === 'image_url') {
                        const u = part.image_url?.url;
                        if (typeof u === 'string' && u.length > 0)
                            urls.push(u);
                    }
                }
            }
        }
        return urls;
    }
    convertMessages(request) {
        const systemMessages = request.messages.filter((m) => m.role === 'system');
        const nonSystemMessages = request.messages.filter((m) => m.role !== 'system');
        // Prepend systemPrompt or system messages into systemInstruction.
        // System messages with array content (multimodal) are flattened to
        // their text parts only — Google's systemInstruction does not
        // accept image input. Image parts on a system message are rejected.
        const systemTextParts = [];
        if (request.systemPrompt)
            systemTextParts.push(request.systemPrompt);
        for (const m of systemMessages) {
            if (typeof m.content === 'string') {
                systemTextParts.push(m.content);
            }
            else {
                const hasImage = m.content.some((p) => p && p.type === 'image_url');
                if (hasImage) {
                    throw new LlmProviderError({
                        message: 'GoogleAdapter: image_url parts are not permitted on system ' +
                            'messages — Google systemInstruction is text-only.',
                        code: 'INVALID_REQUEST',
                        providerId: this.providerId,
                        adapterId: this.adapterId,
                    });
                }
                for (const p of m.content) {
                    if (p.type === 'text')
                        systemTextParts.push(p.text);
                }
            }
        }
        const systemText = systemTextParts.filter(Boolean).join('\n\n');
        const systemInstruction = systemText
            ? { parts: [{ text: systemText }] }
            : undefined;
        // Map user/assistant/tool messages — multimodal-aware.
        const contents = nonSystemMessages.map((m) => {
            const role = m.role === 'assistant' ? 'model' : 'user';
            return {
                role,
                parts: this.convertMessageToParts(m, request.modelId),
            };
        });
        return { systemInstruction, contents };
    }
    // ---------------------------------------------------------------------------
    // Tool definitions: LlmToolDefinition[] -> GoogleTool[]
    // ---------------------------------------------------------------------------
    buildGoogleTools(request) {
        if (!request.tools || request.tools.length === 0)
            return undefined;
        const declarations = normalizeToolUseRequest(request.tools, 'google');
        return [{ functionDeclarations: declarations }];
    }
    // ---------------------------------------------------------------------------
    // chat (AC 4)
    // ---------------------------------------------------------------------------
    async chat(request) {
        this.ensureInitialized();
        this.validateModelId(request.modelId);
        this.emitMultimodalAuditIfPresent(request, 'chat');
        return this.withRetry(async () => this.chatOnce(request));
    }
    async chatOnce(request) {
        const { systemInstruction, contents } = this.convertMessages(request);
        const body = {
            contents,
            generationConfig: {
                maxOutputTokens: request.maxTokens,
                temperature: request.temperature,
            },
        };
        if (systemInstruction) {
            body['systemInstruction'] = systemInstruction;
        }
        const googleTools = this.buildGoogleTools(request);
        if (googleTools) {
            body['tools'] = googleTools;
        }
        const url = this.buildUrl(request.modelId, 'generateContent');
        const rawBody = JSON.stringify(body);
        const headers = this.buildAuthHeaders();
        const timeoutMs = this.googleConfig?.timeoutMs ?? 30000;
        const response = await httpRequest(url, 'POST', headers, rawBody, timeoutMs);
        this.handleHttpErrors(response, request.modelId);
        let parsed;
        try {
            parsed = JSON.parse(response.body);
        }
        catch {
            throw new LlmProviderError({
                message: 'GoogleAdapter: failed to parse response as JSON',
                code: 'UNEXPECTED_RESPONSE_SHAPE',
                providerId: this.providerId,
                adapterId: this.adapterId,
                raw: response.body.slice(0, 200),
            });
        }
        detectUnexpectedShape(parsed, ['candidates'], this.providerId, this.adapterId);
        // Check promptFeedback for prompt-level blocks
        if (parsed.promptFeedback?.blockReason) {
            const safetySignals = this.mapSafetyRatings(parsed.promptFeedback.safetyRatings ?? []);
            this.emitSafetyEvent(safetySignals, true);
            throw new LlmContentFilterError({
                message: `GoogleAdapter: prompt blocked. Reason: ${parsed.promptFeedback.blockReason}`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                filterCategory: parsed.promptFeedback.blockReason,
            });
        }
        const candidate = parsed.candidates?.[0];
        const safetySignals = this.mapSafetyRatings(candidate?.safetyRatings ?? []);
        const isBlocked = safetySignals.some((s) => s.blocked);
        if (isBlocked) {
            this.emitSafetyEvent(safetySignals, true);
            const blockedCategory = safetySignals.find((s) => s.blocked)?.category;
            throw new LlmContentFilterError({
                message: `GoogleAdapter: response blocked by safety filter. Category: ${blockedCategory ?? 'unknown'}`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                filterCategory: blockedCategory,
            });
        }
        if (safetySignals.length > 0) {
            this.emitSafetyEvent(safetySignals, false);
        }
        // Extract text content
        const textParts = (candidate?.content?.parts ?? [])
            .filter((p) => typeof p.text === 'string')
            .map((p) => p.text);
        // Extract function calls
        const funcCallParts = (candidate?.content?.parts ?? []).filter((p) => p.functionCall != null);
        const toolCalls = funcCallParts.length > 0
            ? normalizeToolUseResponse(funcCallParts.map((p) => ({
                id: `tool_${Date.now()}_${p.functionCall.name}`,
                name: p.functionCall.name,
                args: p.functionCall.args,
            })), 'google')
            : undefined;
        const usage = parsed.usageMetadata
            ? {
                inputTokens: parsed.usageMetadata.promptTokenCount ?? 0,
                outputTokens: parsed.usageMetadata.candidatesTokenCount ?? 0,
                totalTokens: parsed.usageMetadata.totalTokenCount ?? 0,
            }
            : undefined;
        return {
            modelId: request.modelId,
            content: textParts.join(''),
            finishReason: this.mapFinishReason(candidate?.finishReason),
            toolCalls,
            usage,
            _raw: parsed,
        };
    }
    // ---------------------------------------------------------------------------
    // chatStream (AC 4 -- streaming via streamGenerateContent)
    // ---------------------------------------------------------------------------
    async *chatStream(request) {
        this.ensureInitialized();
        this.validateModelId(request.modelId);
        this.emitMultimodalAuditIfPresent(request, 'chatStream');
        const { systemInstruction, contents } = this.convertMessages(request);
        const body = {
            contents,
            generationConfig: {
                maxOutputTokens: request.maxTokens,
                temperature: request.temperature,
            },
        };
        if (systemInstruction) {
            body['systemInstruction'] = systemInstruction;
        }
        const googleTools = this.buildGoogleTools(request);
        if (googleTools) {
            body['tools'] = googleTools;
        }
        const url = this.buildUrl(request.modelId, 'streamGenerateContent');
        const rawBody = JSON.stringify(body);
        const headers = this.buildAuthHeaders();
        const timeoutMs = this.googleConfig?.timeoutMs ?? 60000;
        const response = await httpRequest(url, 'POST', headers, rawBody, timeoutMs);
        this.handleHttpErrors(response, request.modelId);
        // Google streamGenerateContent returns newline-delimited JSON (NDJSON)
        // OR Server-Sent Events (SSE) depending on the alt=sse query param.
        // Default (no alt param) is NDJSON array format wrapped in [] with comma separators.
        // We parse each line that begins with '{' as a standalone JSON object.
        const lines = response.body.split('\n');
        for (const line of lines) {
            const trimmed = line.trim();
            // Skip SSE data prefix if present (alt=sse mode)
            const jsonStr = trimmed.startsWith('data:')
                ? trimmed.slice(5).trim()
                : trimmed;
            if (!jsonStr || jsonStr === '[' || jsonStr === ']' || jsonStr === ',')
                continue;
            // Strip trailing comma from NDJSON array format
            const cleaned = jsonStr.replace(/,$/, '');
            if (!cleaned || cleaned === '[DONE]')
                continue;
            let chunk;
            try {
                chunk = JSON.parse(cleaned);
            }
            catch {
                continue;
            }
            const candidate = chunk.candidates?.[0];
            if (!candidate)
                continue;
            // Safety check on streaming chunks
            const safetySignals = this.mapSafetyRatings(candidate.safetyRatings ?? []);
            const isBlocked = safetySignals.some((s) => s.blocked);
            if (isBlocked) {
                this.emitSafetyEvent(safetySignals, true);
                const blockedCategory = safetySignals.find((s) => s.blocked)?.category;
                throw new LlmContentFilterError({
                    message: `GoogleAdapter: streaming response blocked by safety filter. Category: ${blockedCategory ?? 'unknown'}`,
                    providerId: this.providerId,
                    adapterId: this.adapterId,
                    filterCategory: blockedCategory,
                });
            }
            const textParts = (candidate.content?.parts ?? [])
                .filter((p) => typeof p.text === 'string')
                .map((p) => p.text);
            const delta = textParts.join('');
            const finishReason = candidate.finishReason
                ? this.mapFinishReason(candidate.finishReason)
                : undefined;
            // Usage only on final chunk
            const usage = chunk.usageMetadata
                ? {
                    inputTokens: chunk.usageMetadata.promptTokenCount ?? 0,
                    outputTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
                    totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,
                }
                : undefined;
            yield { delta, finishReason, usage };
        }
    }
    // ---------------------------------------------------------------------------
    // invokeToolUse (AC 4 -- function calling)
    // ---------------------------------------------------------------------------
    async invokeToolUse(request) {
        this.ensureInitialized();
        this.validateModelId(request.modelId);
        this.emitMultimodalAuditIfPresent(request, 'invokeToolUse');
        return this.withRetry(async () => this.invokeToolUseOnce(request));
    }
    async invokeToolUseOnce(request) {
        if (!request.tools || request.tools.length === 0) {
            throw new LlmProviderError({
                message: 'GoogleAdapter.invokeToolUse: at least one tool definition is required.',
                code: 'TOOL_USE_FAILURE',
                providerId: this.providerId,
                adapterId: this.adapterId,
            });
        }
        const { systemInstruction, contents } = this.convertMessages(request);
        const googleTools = this.buildGoogleTools(request);
        const body = {
            contents,
            tools: googleTools,
            toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
            generationConfig: {
                maxOutputTokens: request.maxTokens,
                temperature: request.temperature,
            },
        };
        if (systemInstruction) {
            body['systemInstruction'] = systemInstruction;
        }
        const url = this.buildUrl(request.modelId, 'generateContent');
        const rawBody = JSON.stringify(body);
        const headers = this.buildAuthHeaders();
        const timeoutMs = this.googleConfig?.timeoutMs ?? 30000;
        const response = await httpRequest(url, 'POST', headers, rawBody, timeoutMs);
        this.handleHttpErrors(response, request.modelId);
        let parsed;
        try {
            parsed = JSON.parse(response.body);
        }
        catch {
            throw new LlmProviderError({
                message: 'GoogleAdapter: failed to parse tool-use response as JSON',
                code: 'UNEXPECTED_RESPONSE_SHAPE',
                providerId: this.providerId,
                adapterId: this.adapterId,
                raw: response.body.slice(0, 200),
            });
        }
        detectUnexpectedShape(parsed, ['candidates'], this.providerId, this.adapterId);
        const candidate = parsed.candidates?.[0];
        const safetySignals = this.mapSafetyRatings(candidate?.safetyRatings ?? []);
        if (safetySignals.some((s) => s.blocked)) {
            this.emitSafetyEvent(safetySignals, true);
            const blockedCategory = safetySignals.find((s) => s.blocked)?.category;
            throw new LlmContentFilterError({
                message: `GoogleAdapter: tool-use response blocked by safety filter. Category: ${blockedCategory ?? 'unknown'}`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                filterCategory: blockedCategory,
            });
        }
        const funcCallParts = (candidate?.content?.parts ?? []).filter((p) => p.functionCall != null);
        const rawToolCalls = funcCallParts.map((p) => ({
            id: `tool_${Date.now()}_${p.functionCall.name}`,
            name: p.functionCall.name,
            args: p.functionCall.args,
        }));
        const toolCalls = normalizeToolUseResponse(rawToolCalls, 'google');
        const textParts = (candidate?.content?.parts ?? [])
            .filter((p) => typeof p.text === 'string')
            .map((p) => p.text);
        const usage = parsed.usageMetadata
            ? {
                inputTokens: parsed.usageMetadata.promptTokenCount ?? 0,
                outputTokens: parsed.usageMetadata.candidatesTokenCount ?? 0,
                totalTokens: parsed.usageMetadata.totalTokenCount ?? 0,
            }
            : undefined;
        return {
            modelId: request.modelId,
            toolCalls,
            content: textParts.join('') || undefined,
            finishReason: this.mapFinishReason(candidate?.finishReason),
            usage,
            _raw: parsed,
        };
    }
    // ---------------------------------------------------------------------------
    // enumerateModels
    // ---------------------------------------------------------------------------
    async enumerateModels() {
        this.ensureInitialized();
        // Google does not expose a universally accessible model enumeration endpoint
        // that works across both Vertex AI and Generative Language targets without
        // additional scoped permissions. Return the configured default model + the
        // known Gemma family (F-NEW-GEMMA-ADAPTER 2026-05-08).
        const cfg = this.googleConfig;
        const target = cfg.deploymentTarget ?? 'vertex-ai';
        const defaultModelId = cfg.defaultModelId ?? 'gemini-1.5-pro';
        const defaultMeta = getGoogleModelMetadata(defaultModelId, target);
        const descriptors = [
            {
                modelId: defaultModelId,
                family: defaultMeta.family,
                displayName: defaultMeta.displayName,
                deprecated: false,
            },
        ];
        // Gemma family. Open-weight; BAA inheritance still depends on
        // deployment target. Excluded if defaultModelId is itself a Gemma
        // ID (avoid duplicate entry).
        for (const gemmaId of KNOWN_GEMMA_MODEL_IDS) {
            if (gemmaId === defaultModelId)
                continue;
            const meta = getGoogleModelMetadata(gemmaId, target);
            descriptors.push({
                modelId: gemmaId,
                family: meta.family,
                displayName: meta.displayName,
                deprecated: false,
            });
        }
        return descriptors;
    }
    // ---------------------------------------------------------------------------
    // translateError (AC 22-24)
    // ---------------------------------------------------------------------------
    translateError(raw) {
        if (raw instanceof LlmProviderError)
            return raw;
        const isObj = (v) => typeof v === 'object' && v !== null;
        let httpStatus;
        let errorCode;
        let errorMessage;
        let retryAfterMs;
        if (isObj(raw)) {
            httpStatus = typeof raw['status'] === 'number' ? raw['status'] : undefined;
            retryAfterMs = typeof raw['retryAfterMs'] === 'number' ? raw['retryAfterMs'] : undefined;
            const inner = isObj(raw['error']) ? raw['error'] : null;
            if (inner) {
                errorCode = typeof inner['code'] === 'string' ? inner['code'] : undefined;
                errorMessage = typeof inner['message'] === 'string' ? inner['message'] : undefined;
                // Google error format: { error: { code: 429, message: '...', status: 'RESOURCE_EXHAUSTED' } }
                if (typeof inner['status'] === 'string') {
                    errorCode = errorCode ?? inner['status'];
                }
            }
        }
        else if (raw instanceof Error) {
            errorMessage = raw.message;
        }
        else {
            errorMessage = String(raw);
        }
        const rawStr = isObj(raw) ? JSON.stringify(raw).slice(0, 500) : String(raw).slice(0, 500);
        if (httpStatus === 401 || httpStatus === 403 || errorCode === 'UNAUTHENTICATED' || errorCode === 'PERMISSION_DENIED') {
            return new LlmAuthError({
                message: `Google auth failure: ${errorMessage ?? 'unauthorized'}`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                httpStatus,
                raw: rawStr,
            });
        }
        if (httpStatus === 429 || errorCode === 'RESOURCE_EXHAUSTED') {
            return new LlmRateLimitError({
                message: `Google rate limit: ${errorMessage ?? 'quota exceeded'}`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                httpStatus,
                retryAfterMs: retryAfterMs ?? 5000,
                raw: rawStr,
            });
        }
        if (httpStatus === 404 || errorCode === 'NOT_FOUND') {
            const requestedModelId = this.googleConfig?.defaultModelId ?? 'unknown';
            return new LlmModelNotFoundError({
                message: `Google model not found: ${errorMessage ?? errorCode}`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                httpStatus,
                requestedModelId,
                raw: rawStr,
            });
        }
        return new LlmProviderError({
            message: errorMessage ?? `Google error (HTTP ${httpStatus ?? 'unknown'})`,
            code: errorCode ?? 'PROVIDER_ERROR',
            providerId: this.providerId,
            adapterId: this.adapterId,
            httpStatus,
            raw: rawStr,
        });
    }
    // ---------------------------------------------------------------------------
    // getCapabilities
    // ---------------------------------------------------------------------------
    getCapabilities() {
        return {
            chat: true,
            streaming: true,
            toolUse: true,
            modelEnumeration: false,
            baa: this.claimsBaa(),
            baaScope: 'vertex-ai-only',
            // F-NEW-GEMMA-VISION 2026-05-09: GoogleAdapter supports multimodal
            // via paligemma-* and gemini-1.5+/2.x. The per-call check
            // (modelSupportsMultimodal) still gates by modelId.
            multimodal: true,
        };
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    validateModelId(modelId) {
        if (!modelId || typeof modelId !== 'string') {
            throw new LlmModelNotFoundError({
                message: `GoogleAdapter: invalid modelId '${modelId}'. Provide a non-empty string.`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                requestedModelId: String(modelId ?? ''),
            });
        }
    }
    handleHttpErrors(response, requestedModelId) {
        if (response.statusCode >= 200 && response.statusCode < 300)
            return;
        const retryAfterHeader = getHeaderString(response.headers, 'retry-after');
        const retryAfterMs = parseRetryAfterMs(retryAfterHeader);
        let errorBody = null;
        try {
            errorBody = JSON.parse(response.body);
        }
        catch {
            // body may not be JSON
        }
        // Google error body: { error: { code: <number>, message: '...', status: '<STATUS_STRING>' } }
        const isObj = (v) => typeof v === 'object' && v !== null;
        const errorInner = isObj(errorBody?.['error']) ? errorBody['error'] : null;
        const errorMessage = typeof errorInner?.['message'] === 'string' ? errorInner['message'] : undefined;
        const errorStatus = typeof errorInner?.['status'] === 'string' ? errorInner['status'] : undefined;
        const raw = response.body.slice(0, 500);
        if (response.statusCode === 401 || response.statusCode === 403 || errorStatus === 'UNAUTHENTICATED' || errorStatus === 'PERMISSION_DENIED') {
            throw new LlmAuthError({
                message: `Google authentication failed: ${errorMessage ?? 'unauthorized'}`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                httpStatus: response.statusCode,
                raw,
            });
        }
        if (response.statusCode === 429 || errorStatus === 'RESOURCE_EXHAUSTED') {
            this.emit({
                type: 'llm:rate-limit',
                severity: 'WARN',
                adapterId: this.adapterId,
                providerId: this.providerId,
                detail: `Rate limited by Google. Retry-After: ${retryAfterMs}ms`,
                timestamp: new Date().toISOString(),
                data: { retryAfterMs },
            });
            throw new LlmRateLimitError({
                message: `Google rate limit exceeded. Retry after ${retryAfterMs}ms.`,
                providerId: this.providerId,
                adapterId: this.adapterId,
                httpStatus: response.statusCode,
                retryAfterMs: retryAfterMs > 0 ? retryAfterMs : 5000,
                raw,
            });
        }
        if (response.statusCode === 404 || errorStatus === 'NOT_FOUND') {
            throw new LlmModelNotFoundError({
                message: `Google model not found: ${requestedModelId}. ${errorMessage ?? ''}`.trim(),
                providerId: this.providerId,
                adapterId: this.adapterId,
                httpStatus: 404,
                requestedModelId,
                raw,
            });
        }
        throw new LlmProviderError({
            message: errorMessage ?? `Google error (HTTP ${response.statusCode})`,
            code: errorStatus ?? 'PROVIDER_ERROR',
            providerId: this.providerId,
            adapterId: this.adapterId,
            httpStatus: response.statusCode,
            raw,
        });
    }
    /**
     * Map Google HarmCategory safety ratings to normalized LlmSafetySignal[].
     * AC 4: safety signal pass-through.
     */
    mapSafetyRatings(ratings) {
        return ratings.map((r) => ({
            category: r.category,
            blocked: r.blocked === true || r.probability === 'HIGH',
            probability: this.normalizeProbability(r.probability),
            severity: r.probability,
        }));
    }
    normalizeProbability(prob) {
        switch (prob) {
            case 'NEGLIGIBLE': return 'NEGLIGIBLE';
            case 'LOW': return 'LOW';
            case 'MEDIUM': return 'MEDIUM';
            case 'HIGH': return 'HIGH';
            default: return undefined;
        }
    }
    /**
     * Emit a `llm:multimodal-input` audit event when a request contains
     * image_url parts. Fires BEFORE the wire request goes out, so
     * downstream pack classifiers (e.g. PHI image leak detector, public
     * Cloud Storage exposure check) see the image URIs as governance-
     * relevant data alongside text. Suppressed for text-only requests.
     *
     * F-NEW-GEMMA-VISION 2026-05-09. Locked Invariant 10 (push-receive
     * only) is preserved: we DO NOT fetch the image — we only emit the
     * URIs through the audit channel.
     */
    emitMultimodalAuditIfPresent(request, callPath) {
        const imageUrls = GoogleAdapter.extractImageUrls(request);
        if (imageUrls.length === 0)
            return;
        // Truncate each URL to 256 chars in the audit data so a base64
        // data: URI doesn't dominate the audit log.
        const truncated = imageUrls.map((u) => u.length > 256 ? u.slice(0, 253) + '...' : u);
        // Classify URI scheme for downstream policy (a gs:// or https://
        // URL might be public — pack classifier decides).
        const schemes = imageUrls.map((u) => {
            if (u.startsWith('data:'))
                return 'data';
            if (u.startsWith('gs://'))
                return 'gs';
            if (u.startsWith('https://'))
                return 'https';
            if (u.startsWith('http://'))
                return 'http';
            return 'other';
        });
        this.emit({
            type: 'llm:multimodal-input',
            severity: 'INFO',
            adapterId: this.adapterId,
            providerId: this.providerId,
            detail: `GoogleAdapter ${callPath} received multimodal input: ` +
                `${imageUrls.length} image part(s) bound for model '${request.modelId}'. ` +
                `Pre-call check sees image URIs as governance-relevant data ` +
                `(F-NEW-GEMMA-VISION).`,
            timestamp: new Date().toISOString(),
            data: {
                callPath,
                modelId: request.modelId,
                imageCount: imageUrls.length,
                imageUrls: truncated,
                imageSchemes: schemes,
                modelIsMultimodal: this.modelSupportsMultimodal(request.modelId),
            },
        });
    }
    /**
     * Emit audit event for safety signals (AC 4 -- always emit when signals present).
     */
    emitSafetyEvent(signals, blocked) {
        const categories = signals.map((s) => s.category).join(', ');
        const blockedCategories = signals.filter((s) => s.blocked).map((s) => s.category);
        this.emit({
            type: 'llm:content-filter-triggered',
            severity: blocked ? 'WARN' : 'INFO',
            adapterId: this.adapterId,
            providerId: this.providerId,
            detail: blocked
                ? `Google safety filter blocked response. Categories: ${blockedCategories.join(', ')}`
                : `Google safety ratings present (no block). Categories: ${categories}`,
            timestamp: new Date().toISOString(),
            data: {
                blocked,
                blockedCategories,
                signals: JSON.stringify(signals).slice(0, 500),
            },
        });
    }
    mapFinishReason(reason) {
        switch (reason) {
            case 'STOP': return 'stop';
            case 'MAX_TOKENS': return 'length';
            case 'SAFETY': return 'content_filter';
            case 'RECITATION': return 'content_filter';
            case 'TOOL_CALLS':
            case 'FUNCTION_CALL': return 'tool_calls';
            default: return 'unknown';
        }
    }
}
//# sourceMappingURL=google.js.map