import { fetchWithTimeout } from "./http.js";
import { getGeminiThinkingConfig } from "./reasoning.js";
import { attachTokenCost } from "./tokenAccounting.js";
export const defaultGeminiModel = "gemini-2.5-flash";
export const defaultGeminiBaseUrl = "https://generativelanguage.googleapis.com/v1beta";
export class GeminiClient {
    apiKey;
    baseUrl;
    runtimeOptions;
    constructor(apiKey = readGeminiApiKey(), baseUrl = process.env.PATCHPILOT_GEMINI_BASE_URL ?? defaultGeminiBaseUrl, runtimeOptions = readGeminiRuntimeOptions()) {
        this.apiKey = apiKey;
        this.baseUrl = baseUrl.replace(/\/$/, "");
        this.runtimeOptions = runtimeOptions;
    }
    async chat(options) {
        this.assertConfigured();
        const startedAt = Date.now();
        const response = await this.fetchGemini(`${modelPath(options.model)}:generateContent`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-goog-api-key": this.apiKey
            },
            body: JSON.stringify(toGenerateContentRequest(options.model, options.messages, options.formatJson, this.runtimeOptions, options.reasoningEffort)),
            signal: options.signal
        });
        const durationMs = Date.now() - startedAt;
        const payload = (await readJsonSafely(response));
        if (!response.ok || payload.error) {
            const reason = payload.error?.message ? ` ${payload.error.message}` : "";
            throw new Error(`Gemini chat failed for model "${options.model}": HTTP ${response.status}.${reason}`);
        }
        const finishReason = payload.candidates?.[0]?.finishReason;
        if (isTruncatedFinishReason(finishReason)) {
            throw new Error(`Gemini response for model "${options.model}" was truncated by maxOutputTokens (${this.runtimeOptions.maxOutputTokens}).`);
        }
        const content = payload.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("").trim() ?? "";
        if (!content) {
            const blockReason = payload.promptFeedback?.blockReason;
            const reason = blockReason ? ` Prompt blocked: ${blockReason}.` : finishReason ? ` Finish reason: ${finishReason}.` : "";
            throw new Error(`Gemini returned an empty response.${reason}`);
        }
        return {
            content,
            telemetry: toTelemetry(payload, durationMs, options.model)
        };
    }
    async listModels() {
        this.assertConfigured();
        const models = [];
        let pageToken = "";
        do {
            const response = await this.fetchGemini(`models?pageSize=1000${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : ""}`, {
                headers: {
                    "x-goog-api-key": this.apiKey
                }
            });
            const payload = (await readJsonSafely(response));
            if (!response.ok || payload.error) {
                const reason = payload.error?.message ? ` ${payload.error.message}` : "";
                throw new Error(`Gemini models failed with HTTP ${response.status}.${reason}`);
            }
            models.push(...(payload.models
                ?.filter((model) => model.supportedGenerationMethods?.includes("generateContent"))
                .map((model) => stripModelPrefix(model.name))
                .filter(isLikelyGeminiAgentModel) ?? []));
            pageToken = payload.nextPageToken ?? "";
        } while (pageToken);
        return [...new Set(models)].sort();
    }
    async fetchGemini(path, init) {
        try {
            return await fetchWithTimeout(`${this.baseUrl}/${path}`, init, {
                timeoutMs: init?.method === "POST" ? 90_000 : 8000,
                retries: init?.method === "POST" ? 2 : 1,
                label: `Gemini ${path}`
            });
        }
        catch (error) {
            const suffix = error instanceof Error ? ` ${error.message}` : "";
            throw new Error(`Cannot reach Gemini API at ${this.baseUrl}.${suffix}`);
        }
    }
    assertConfigured() {
        if (!this.apiKey) {
            throw new Error("Gemini API key missing. Set GEMINI_API_KEY in .env or your shell.");
        }
    }
}
export function readGeminiApiKey(env = process.env) {
    return env.GOOGLE_API_KEY?.trim() || env.GEMINI_API_KEY?.trim() || "";
}
export function readGeminiRuntimeOptions(env = process.env) {
    return {
        maxOutputTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT, 8192),
        temperature: readTemperature(env.PATCHPILOT_TEMPERATURE, 0.1)
    };
}
function isTruncatedFinishReason(value) {
    return typeof value === "string" && /length|max_?tokens/i.test(value);
}
function toGenerateContentRequest(model, messages, formatJson, runtimeOptions, reasoningEffort) {
    const systemText = messages
        .filter((message) => message.role === "system")
        .map((message) => message.content)
        .join("\n\n")
        .trim();
    const contents = messages.filter((message) => message.role !== "system").map(toGeminiContent);
    return {
        systemInstruction: systemText
            ? {
                parts: [
                    {
                        text: systemText
                    }
                ]
            }
            : undefined,
        contents,
        generationConfig: {
            maxOutputTokens: runtimeOptions.maxOutputTokens,
            temperature: runtimeOptions.temperature,
            thinkingConfig: getGeminiThinkingConfig(model, reasoningEffort),
            responseMimeType: formatJson ? "application/json" : undefined
        }
    };
}
function toGeminiContent(message) {
    return {
        role: message.role === "assistant" ? "model" : "user",
        parts: [
            {
                text: message.content
            }
        ]
    };
}
function modelPath(model) {
    const normalizedModel = model.startsWith("models/") ? model : `models/${model}`;
    return normalizedModel.split("/").map(encodeURIComponent).join("/");
}
function stripModelPrefix(model) {
    return model.startsWith("models/") ? model.slice("models/".length) : model;
}
function toTelemetry(payload, durationMs, model) {
    const promptTokens = payload.usageMetadata?.promptTokenCount ?? 0;
    const responseTokens = payload.usageMetadata?.candidatesTokenCount ?? 0;
    const cachedPromptTokens = payload.usageMetadata?.cachedContentTokenCount ?? 0;
    const thoughtsTokens = payload.usageMetadata?.thoughtsTokenCount ?? 0;
    const totalTokens = payload.usageMetadata?.totalTokenCount ?? promptTokens + responseTokens + thoughtsTokens;
    return attachTokenCost({
        promptTokens,
        cachedPromptTokens,
        cacheWriteTokens: 0,
        responseTokens: responseTokens + thoughtsTokens,
        totalTokens,
        evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
        promptDurationMs: 0,
        responseDurationMs: durationMs,
        totalDurationMs: durationMs,
        tokenSource: "provider"
    }, "gemini", model);
}
function isLikelyGeminiAgentModel(model) {
    const normalizedModel = model.toLowerCase();
    if (/(embedding|imagen|veo|tts|aqa|live|bidi|audio|speech)/.test(normalizedModel)) {
        return false;
    }
    return normalizedModel.startsWith("gemini-");
}
async function readJsonSafely(response) {
    try {
        return await response.json();
    }
    catch {
        return {};
    }
}
function readPositiveInteger(value, fallback) {
    const parsedValue = Number.parseInt(value ?? "", 10);
    return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
}
function readTemperature(value, fallback) {
    const parsedValue = Number.parseFloat(value ?? "");
    return Number.isFinite(parsedValue) && parsedValue >= 0 && parsedValue <= 2 ? parsedValue : fallback;
}
//# sourceMappingURL=gemini.js.map