import { promises as fs } from "node:fs";
import { MAX_ERROR_DETAIL_CHARS, TRANSCRIPTION_TIMEOUT_MS } from "./constants.js";
import { resolveGeminiTranscriptionModel } from "./provider-setup.js";
import { ensureWhisperFilenameExtension, toArrayBuffer, wrapError } from "./utils.js";
const GEMINI_INLINE_UPLOAD_BYTES = 20 * 1024 * 1024;
const DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com";
const TRANSCRIPTION_PROMPT = "Transcribe this audio or video into plain text. Return only the transcript text, preserving the spoken language. No timestamps, speaker labels, summaries, or markdown.";
export async function transcribeWithGemini(bytes, mediaType, filename, apiKey, options) {
    if (bytes.byteLength <= GEMINI_INLINE_UPLOAD_BYTES) {
        return await generateInlineTranscript({
            bytes,
            mediaType,
            apiKey,
            env: options?.env,
            model: options?.model ?? null,
        });
    }
    const upload = await uploadGeminiBytes({
        bytes,
        mediaType,
        filename,
        apiKey,
        env: options?.env,
    });
    try {
        return await generateFileTranscript({
            file: upload,
            apiKey,
            env: options?.env,
            model: options?.model ?? null,
            mediaType,
        });
    }
    finally {
        await deleteGeminiFile(upload, apiKey, options?.env).catch(() => { });
    }
}
export async function transcribeFileWithGemini({ filePath, mediaType, filename, apiKey, env, model, }) {
    const stat = await fs.stat(filePath);
    if (stat.size <= GEMINI_INLINE_UPLOAD_BYTES) {
        const bytes = new Uint8Array(await fs.readFile(filePath));
        return await generateInlineTranscript({
            bytes,
            mediaType,
            apiKey,
            env,
            model,
        });
    }
    const upload = await uploadGeminiFile({
        filePath,
        mediaType,
        filename,
        apiKey,
        env,
    });
    try {
        return await generateFileTranscript({
            file: upload,
            apiKey,
            env,
            model,
            mediaType,
        });
    }
    finally {
        await deleteGeminiFile(upload, apiKey, env).catch(() => { });
    }
}
async function generateInlineTranscript({ bytes, mediaType, apiKey, env, model, }) {
    const response = await geminiJsonRequest({
        path: `v1beta/models/${resolveModelId(model, env)}:generateContent`,
        apiKey,
        env,
        body: {
            generationConfig: { temperature: 0 },
            contents: [
                {
                    parts: [
                        { text: TRANSCRIPTION_PROMPT },
                        {
                            inline_data: {
                                mime_type: mediaType,
                                data: Buffer.from(toArrayBuffer(bytes)).toString("base64"),
                            },
                        },
                    ],
                },
            ],
        },
    });
    return extractGeminiTranscript(response, resolveModelId(model, env));
}
async function generateFileTranscript({ file, apiKey, env, model, mediaType, }) {
    const ready = await waitForGeminiFileActive(file, apiKey, env);
    const fileUri = typeof ready.uri === "string" ? ready.uri.trim() : "";
    if (!fileUri) {
        throw new Error("Gemini Files API did not return a file uri");
    }
    const response = await geminiJsonRequest({
        path: `v1beta/models/${resolveModelId(model, env)}:generateContent`,
        apiKey,
        env,
        body: {
            generationConfig: { temperature: 0 },
            contents: [
                {
                    parts: [
                        { text: TRANSCRIPTION_PROMPT },
                        {
                            file_data: {
                                mime_type: resolveGeminiMimeType(ready, mediaType),
                                file_uri: fileUri,
                            },
                        },
                    ],
                },
            ],
        },
    });
    return extractGeminiTranscript(response, resolveModelId(model, env));
}
async function uploadGeminiFile({ filePath, mediaType, filename, apiKey, env, }) {
    const bytes = await fs.readFile(filePath);
    return await uploadGeminiBytes({
        bytes: new Uint8Array(bytes),
        mediaType,
        filename,
        apiKey,
        env,
    });
}
async function uploadGeminiBytes({ bytes, mediaType, filename, apiKey, env, }) {
    const displayName = ensureWhisperFilenameExtension(filename?.trim() || "media", mediaType);
    const startUrl = new URL(`${resolveGeminiBaseUrl(env)}/upload/v1beta/files`);
    const start = await globalThis.fetch(startUrl, {
        method: "POST",
        headers: {
            "content-type": "application/json",
            "x-goog-api-key": apiKey,
            "x-goog-upload-command": "start",
            "x-goog-upload-protocol": "resumable",
            "x-goog-upload-header-content-length": String(bytes.byteLength),
            "x-goog-upload-header-content-type": mediaType,
        },
        body: JSON.stringify({
            file: {
                display_name: displayName,
            },
        }),
        signal: AbortSignal.timeout(TRANSCRIPTION_TIMEOUT_MS),
    });
    if (!start.ok) {
        const detail = await readErrorDetail(start);
        const suffix = detail ? `: ${detail}` : "";
        throw new Error(`Gemini file upload start failed (${start.status})${suffix}`);
    }
    const uploadUrl = start.headers.get("x-goog-upload-url")?.trim() || "";
    if (!uploadUrl) {
        throw new Error("Gemini file upload start response did not include x-goog-upload-url");
    }
    const upload = await globalThis.fetch(uploadUrl, {
        method: "POST",
        headers: {
            "content-length": String(bytes.byteLength),
            "x-goog-upload-command": "upload, finalize",
            "x-goog-upload-offset": "0",
        },
        body: new Blob([toArrayBuffer(bytes)], { type: mediaType }),
        signal: AbortSignal.timeout(TRANSCRIPTION_TIMEOUT_MS),
    });
    if (!upload.ok) {
        const detail = await readErrorDetail(upload);
        const suffix = detail ? `: ${detail}` : "";
        throw new Error(`Gemini file upload failed (${upload.status})${suffix}`);
    }
    const payload = (await upload.json());
    if (!payload.file) {
        throw new Error("Gemini file upload returned no file metadata");
    }
    return payload.file;
}
async function waitForGeminiFileActive(file, apiKey, env) {
    const name = typeof file.name === "string" ? file.name.trim() : "";
    if (!name)
        return file;
    let current = file;
    const deadline = Date.now() + 60_000;
    while (Date.now() < deadline) {
        const state = normalizeFileState(current.state);
        if (!state || state === "ACTIVE")
            return current;
        if (state === "FAILED") {
            throw new Error(`Gemini file processing failed for ${name}`);
        }
        await new Promise((resolve) => setTimeout(resolve, 1_000));
        current = await getGeminiFile(name, apiKey, env);
    }
    throw new Error(`Gemini file processing timed out for ${name}`);
}
async function getGeminiFile(name, apiKey, env) {
    const response = await geminiJsonRequest({
        path: `v1beta/${name}`,
        apiKey,
        env,
        method: "GET",
    });
    if (response && typeof response === "object" && "file" in response) {
        const payload = response;
        if (payload.file)
            return payload.file;
    }
    return response;
}
async function deleteGeminiFile(file, apiKey, env) {
    const name = typeof file.name === "string" ? file.name.trim() : "";
    if (!name)
        return;
    const url = new URL(`${resolveGeminiBaseUrl(env)}/v1beta/${name}`);
    const response = await globalThis.fetch(url, {
        method: "DELETE",
        headers: { "x-goog-api-key": apiKey },
        signal: AbortSignal.timeout(TRANSCRIPTION_TIMEOUT_MS),
    });
    if (!response.ok && response.status !== 404) {
        throw new Error(`Gemini file delete failed (${response.status})`);
    }
}
async function geminiJsonRequest({ path, apiKey, env, method = "POST", body, }) {
    const url = new URL(`${resolveGeminiBaseUrl(env)}/${path.replace(/^\/+/, "")}`);
    const response = await globalThis.fetch(url, {
        method,
        headers: {
            "content-type": "application/json",
            "x-goog-api-key": apiKey,
        },
        ...(body === undefined ? {} : { body: JSON.stringify(body) }),
        signal: AbortSignal.timeout(TRANSCRIPTION_TIMEOUT_MS),
    });
    const text = await response.text();
    if (!response.ok) {
        const suffix = text.trim() ? `: ${truncate(text)}` : "";
        throw new Error(`Gemini request failed (${response.status})${suffix}`);
    }
    if (!text.trim())
        return {};
    try {
        return JSON.parse(text);
    }
    catch (error) {
        throw wrapError("Gemini returned invalid JSON", error);
    }
}
function extractGeminiTranscript(payload, modelId) {
    const candidates = Array.isArray(payload?.candidates)
        ? payload.candidates
        : [];
    const text = candidates
        .flatMap((candidate) => {
        const content = candidate.content;
        if (!content || typeof content !== "object")
            return [];
        const parts = content.parts;
        return Array.isArray(parts) ? parts : [];
    })
        .map((part) => {
        if (!part || typeof part !== "object")
            return "";
        const textValue = part.text;
        return typeof textValue === "string" ? textValue : "";
    })
        .join("")
        .trim();
    if (text)
        return text;
    const finishReason = candidates
        .map((candidate) => {
        const value = candidate.finishReason;
        return typeof value === "string" ? value.trim() : "";
    })
        .find(Boolean);
    if (finishReason && finishReason !== "STOP") {
        throw new Error(`Gemini transcription stopped with ${finishReason} (model ${modelId})`);
    }
    return null;
}
function resolveGeminiBaseUrl(env) {
    const source = env ?? process.env;
    return (source.GOOGLE_BASE_URL?.trim() ||
        source.GEMINI_BASE_URL?.trim() ||
        DEFAULT_GEMINI_BASE_URL).replace(/\/+$/, "");
}
function resolveModelId(model, env) {
    const explicit = model?.trim();
    if (explicit)
        return explicit;
    return resolveGeminiTranscriptionModel(env);
}
function normalizeFileState(state) {
    const normalized = state?.trim().toUpperCase() || "";
    return normalized.length > 0 ? normalized : null;
}
function resolveGeminiMimeType(file, fallback) {
    const fromFile = (typeof file.mimeType === "string" ? file.mimeType : null) ??
        (typeof file.mime_type === "string" ? file.mime_type : null);
    return fromFile?.trim() || fallback;
}
function truncate(value) {
    const trimmed = value.trim();
    return trimmed.length > MAX_ERROR_DETAIL_CHARS
        ? `${trimmed.slice(0, MAX_ERROR_DETAIL_CHARS)}…`
        : trimmed;
}
async function readErrorDetail(response) {
    try {
        const text = await response.text();
        const trimmed = text.trim();
        return trimmed.length > 0 ? truncate(trimmed) : null;
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=gemini.js.map