import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { z } from "zod";
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { GoogleGenAI, FileState, createUserContent, createPartFromUri, createPartFromBase64, } from "@google/genai";
import { getAgentState } from "../agent/state.js";
import { resolveGeminiApiKey } from "../gemini-credentials.js";
import { getModelsForCapability, } from "../gemini-models.js";
import { logger } from "../logger.js";
// ── Constants ────────────────────────────────────────────────────────
/** Directory for saving generated files. */
const GEMINI_TEMP_DIR = path.join(os.tmpdir(), "visionclaw-gemini");
/** Extension → MIME type mapping for input files. */
const MIME_MAP = {
    // Images
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".webp": "image/webp",
    ".gif": "image/gif",
    // Audio
    ".mp3": "audio/mpeg",
    ".wav": "audio/wav",
    ".ogg": "audio/ogg",
    ".flac": "audio/flac",
    ".aac": "audio/aac",
    // Video
    ".mp4": "video/mp4",
    ".mov": "video/quicktime",
    ".avi": "video/x-msvideo",
    ".mkv": "video/x-matroska",
    ".webm": "video/webm",
    ".flv": "video/x-flv",
    ".mpeg": "video/mpeg",
    ".mpg": "video/mpeg",
    ".wmv": "video/x-ms-wmv",
    ".3gpp": "video/3gpp",
    // Documents
    ".pdf": "application/pdf",
    ".txt": "text/plain",
    ".csv": "text/csv",
    ".json": "application/json",
};
/** File types that must be uploaded via the Files API (too large for inline). */
const FILE_API_MIMES = new Set([
    "video/mp4",
    "video/quicktime",
    "video/x-msvideo",
    "video/x-matroska",
    "video/webm",
    "video/x-flv",
    "video/mpeg",
    "video/x-ms-wmv",
    "video/3gpp",
]);
// ── Fallback model IDs (used when model discovery is unavailable) ────
const FALLBACK_IMAGE_MODEL = "gemini-3.1-flash-image-preview";
// Plain text / multimodal fallback. Previously accidentally pointed at the
// image-preview model, which works for text but is the wrong pick when
// the agent just wants to analyse a document or answer a question.
const FALLBACK_TEXT_MODEL = "gemini-3-flash-preview";
const FALLBACK_MUSIC_CLIP_MODEL = "lyria-3-clip-preview";
const FALLBACK_MUSIC_PRO_MODEL = "lyria-3-pro-preview";
const FALLBACK_VIDEO_MODEL = "veo-3.1-generate-preview";
const FALLBACK_TTS_MODEL = "gemini-3.1-flash-tts-preview";
// ── Helpers ──────────────────────────────────────────────────────────
function ensureTempDir() {
    fs.mkdirSync(GEMINI_TEMP_DIR, { recursive: true });
}
function saveToTemp(base64Data, ext) {
    ensureTempDir();
    const filename = `gemini-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
    const filePath = path.join(GEMINI_TEMP_DIR, filename);
    fs.writeFileSync(filePath, Buffer.from(base64Data, "base64"));
    return filePath;
}
function extForMime(mimeType) {
    if (mimeType.includes("wav"))
        return "wav";
    if (mimeType.includes("mp3") || mimeType.includes("mpeg"))
        return "mp3";
    if (mimeType.includes("ogg"))
        return "ogg";
    if (mimeType.includes("png"))
        return "png";
    if (mimeType.includes("webp"))
        return "webp";
    if (mimeType.includes("gif"))
        return "gif";
    return "jpg";
}
function mimeFromPath(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    return MIME_MAP[ext] ?? "application/octet-stream";
}
/**
 * Poll until a file uploaded to the Gemini File API reaches ACTIVE state.
 */
async function waitForFileActive(ai, fileName, timeoutMs = 120_000) {
    const start = Date.now();
    const pollIntervalMs = 3_000;
    while (Date.now() - start < timeoutMs) {
        const file = await ai.files.get({ name: fileName });
        if (file.state === FileState.ACTIVE)
            return;
        if (file.state === FileState.FAILED) {
            throw new Error(`File processing failed: ${fileName}`);
        }
        await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
    }
    throw new Error(`Timed out waiting for file to become active: ${fileName}`);
}
/**
 * Build a WAV header for raw PCM data.
 * Gemini TTS outputs PCM: 24kHz, 16-bit, mono.
 */
function buildWavHeader(pcmByteLength, sampleRate = 24000, bitsPerSample = 16, channels = 1) {
    const byteRate = sampleRate * channels * (bitsPerSample / 8);
    const blockAlign = channels * (bitsPerSample / 8);
    const header = Buffer.alloc(44);
    header.write("RIFF", 0);
    header.writeUInt32LE(36 + pcmByteLength, 4);
    header.write("WAVE", 8);
    header.write("fmt ", 12);
    header.writeUInt32LE(16, 16); // subchunk1 size
    header.writeUInt16LE(1, 20); // PCM format
    header.writeUInt16LE(channels, 22);
    header.writeUInt32LE(sampleRate, 24);
    header.writeUInt32LE(byteRate, 28);
    header.writeUInt16LE(blockAlign, 32);
    header.writeUInt16LE(bitsPerSample, 34);
    header.write("data", 36);
    header.writeUInt32LE(pcmByteLength, 40);
    return header;
}
// ── Auto-model selection ─────────────────────────────────────────────
async function selectModel(apiKey, outputModality, _hasInputFiles, musicMode) {
    // Try dynamic discovery first
    try {
        if (outputModality === "speech") {
            const models = await getModelsForCapability(apiKey, "tts");
            if (models.length > 0)
                return models[0].id;
        }
        if (outputModality === "audio") {
            const capability = "music";
            const models = await getModelsForCapability(apiKey, capability);
            if (models.length > 0) {
                // If pro mode, prefer pro model; otherwise prefer clip model
                if (musicMode === "pro") {
                    const pro = models.find((m) => m.id.includes("pro"));
                    if (pro)
                        return pro.id;
                }
                const clip = models.find((m) => m.id.includes("clip"));
                if (clip)
                    return clip.id;
                return models[0].id;
            }
        }
        if (outputModality === "video") {
            const models = await getModelsForCapability(apiKey, "video_generation");
            if (models.length > 0)
                return models[0].id;
        }
        if (outputModality === "image") {
            const models = await getModelsForCapability(apiKey, "image_generation");
            if (models.length > 0)
                return models[0].id;
        }
        if (outputModality === "text") {
            // For text output with files, prefer a capable multimodal model
            const models = await getModelsForCapability(apiKey, "text");
            if (models.length > 0) {
                // Prefer flash for speed, pro for quality with large files
                const flash = models.find((m) => m.id.includes("flash"));
                return flash?.id ?? models[0].id;
            }
        }
    }
    catch {
        // Fall through to hardcoded defaults
    }
    // Fallback to hardcoded defaults
    if (outputModality === "speech")
        return FALLBACK_TTS_MODEL;
    if (outputModality === "audio") {
        return musicMode === "pro" ? FALLBACK_MUSIC_PRO_MODEL : FALLBACK_MUSIC_CLIP_MODEL;
    }
    if (outputModality === "video")
        return FALLBACK_VIDEO_MODEL;
    if (outputModality === "image")
        return FALLBACK_IMAGE_MODEL;
    return FALLBACK_TEXT_MODEL;
}
// ── Tool definition ──────────────────────────────────────────────────
export const geminiTool = tool("gemini", `Unified Gemini AI tool for generation and understanding tasks.
Leverages Google's Gemini models for capabilities Claude does not have natively:
image generation/editing, video generation, music generation, text-to-speech, video/document understanding, and more.

Parameters:
- prompt (required): What you want - generation instructions, questions about files, editing instructions, etc.
- input_files (optional): Array of absolute file paths to include as input (images, videos, audio, documents).
  Files are auto-detected by extension and sent to the model appropriately.
- output_modality (optional): What kind of output you want - "text" (default), "image", "audio", "video", or "speech".
  Set to "image" for image generation/editing, "audio" for music generation, "video" for video generation, "speech" for text-to-speech.
- model (optional): Specific model ID to use. If omitted, the best model is auto-selected based on the task.
- music_mode (optional): For audio output - "clip" (30s, default) or "pro" (full song ~2min).
- aspect_ratio (optional): For video output - "16:9" (landscape, default) or "9:16" (portrait).
- voice_name (optional): For speech output - preset voice name (e.g. "Kore", "Puck", "Charon", "Zephyr"). 30 voices available.
- multi_speaker (optional): For speech output - array of {speaker, voice} objects for multi-speaker TTS.
  The prompt should use speaker names to assign dialogue. Max 2 speakers.
  Example: [{"speaker": "Alice", "voice": "Kore"}, {"speaker": "Bob", "voice": "Puck"}]

Examples:
- Generate image: prompt="A cat on the moon", output_modality="image"
- Edit image: prompt="Make the sky purple", input_files=["/path/to/photo.jpg"], output_modality="image"
- Understand video: prompt="Summarize this video", input_files=["/path/to/video.mp4"]
- Generate video: prompt="A drone flying over a mountain", output_modality="video"
- Generate portrait video: prompt="A person dancing", output_modality="video", aspect_ratio="9:16"
- Generate music: prompt="Upbeat jazz piano", output_modality="audio"
- Image-to-music: prompt="Create music matching this scene", input_files=["/path/to/photo.jpg"], output_modality="audio"
- Text-to-speech: prompt="Hello, welcome to VisionClaw!", output_modality="speech", voice_name="Kore"
- Multi-speaker TTS: prompt="Alice: Hi! Bob: Hello!", output_modality="speech", multi_speaker=[{"speaker":"Alice","voice":"Kore"},{"speaker":"Bob","voice":"Puck"}]
- Analyze document: prompt="Summarize this PDF", input_files=["/path/to/doc.pdf"]

Generated files are saved to disk and the file path is returned so you can send them via notify_user.`, {
    prompt: z
        .string()
        .describe("Text prompt: generation instructions, editing instructions, or questions about input files."),
    input_files: z
        .array(z.string())
        .optional()
        .describe("Array of absolute file paths to include as input (images, videos, audio, documents). Auto-detected by extension."),
    output_modality: z
        .enum(["text", "image", "audio", "video", "speech"])
        .optional()
        .default("text")
        .describe("Desired output type. 'text' (default) for analysis/understanding, 'image' for image generation/editing, 'audio' for music generation, 'video' for video generation, 'speech' for text-to-speech."),
    model: z
        .string()
        .optional()
        .describe("Specific Gemini model ID to use (e.g. 'gemini-3.1-flash-image-preview', 'lyria-3-clip-preview', 'veo-3.1-generate-preview'). If omitted, auto-selects the best model."),
    music_mode: z
        .enum(["clip", "pro"])
        .optional()
        .describe("Music generation mode (for output_modality='audio'). 'clip' = 30-second clips (default). 'pro' = full-length songs ~2 minutes."),
    aspect_ratio: z
        .enum(["16:9", "9:16"])
        .optional()
        .describe("Aspect ratio for video generation (for output_modality='video'). '16:9' = landscape (default). '9:16' = portrait."),
    voice_name: z
        .string()
        .optional()
        .describe("Voice name for TTS (output_modality='speech'). Preset voices: Zephyr, Puck, Charon, Kore, Fenrir, Leda, Orus, Aoede, and more. Defaults to Kore."),
    multi_speaker: z
        .array(z.object({
        speaker: z.string().describe("Speaker name as used in the prompt text."),
        voice: z.string().describe("Preset voice name for this speaker."),
    }))
        .optional()
        .describe("Multi-speaker TTS config (output_modality='speech'). Array of {speaker, voice} objects. Exactly 2 speakers required. Mutually exclusive with voice_name."),
}, async (args) => {
    try {
        const state = getAgentState();
        const apiKey = resolveGeminiApiKey(state.config);
        if (!apiKey) {
            return {
                content: [
                    { type: "text", text: "Gemini API key is not configured. It should be provided via the onboarding service heartbeat or set manually via `visionclaw reconfigure`." },
                ],
                isError: true,
            };
        }
        const ai = new GoogleGenAI({ apiKey });
        const outputModality = args.output_modality;
        const hasInputFiles = (args.input_files?.length ?? 0) > 0;
        // -- Resolve model --------------------------------------------------
        const modelId = args.model ?? await selectModel(apiKey, outputModality, hasInputFiles, args.music_mode);
        logger.system(`[gemini] model=${modelId} output=${outputModality} files=${args.input_files?.length ?? 0} prompt="${args.prompt.slice(0, 60)}..."`);
        // -- Video generation (separate API: generateVideos + polling) ------
        if (outputModality === "video") {
            // Validate input files exist
            if (args.input_files) {
                for (const filePath of args.input_files) {
                    if (!fs.existsSync(filePath)) {
                        return {
                            content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
                            isError: true,
                        };
                    }
                }
            }
            const videoConfig = {};
            if (args.aspect_ratio) {
                videoConfig.aspectRatio = args.aspect_ratio;
            }
            // Veo supports up to 3 reference images for guided generation (image/* only)
            const imageFiles = args.input_files
                ?.filter((f) => mimeFromPath(f).startsWith("image/"));
            if (imageFiles && imageFiles.length > 0) {
                videoConfig.referenceImages = imageFiles.slice(0, 3).map((f) => ({
                    image: {
                        imageBytes: fs.readFileSync(f).toString("base64"),
                        mimeType: mimeFromPath(f),
                    },
                    referenceType: "asset",
                }));
            }
            let operation = await ai.models.generateVideos({
                model: modelId,
                prompt: args.prompt,
                ...(Object.keys(videoConfig).length > 0 ? { config: videoConfig } : {}),
            });
            // Poll until the video is ready (Veo typically takes 1-3 minutes)
            const videoPollStart = Date.now();
            const videoTimeoutMs = 300_000; // 5 minutes
            while (!operation.done) {
                if (Date.now() - videoPollStart > videoTimeoutMs) {
                    return {
                        content: [{ type: "text", text: "Video generation timed out after 5 minutes." }],
                        isError: true,
                    };
                }
                logger.system("[gemini] Video generation in progress...");
                await new Promise((resolve) => setTimeout(resolve, 10_000));
                operation = await ai.operations.getVideosOperation({ operation });
            }
            // Download the generated video
            const generatedVideos = operation.response?.generatedVideos;
            if (!generatedVideos || generatedVideos.length === 0) {
                return {
                    content: [{ type: "text", text: "Video generation completed but returned no videos. The request may have been filtered by safety settings." }],
                    isError: true,
                };
            }
            const video = generatedVideos[0].video;
            if (!video) {
                return {
                    content: [{ type: "text", text: "Video generation completed but the video object is missing." }],
                    isError: true,
                };
            }
            ensureTempDir();
            const videoFilename = `gemini-video-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mp4`;
            const videoPath = path.join(GEMINI_TEMP_DIR, videoFilename);
            await ai.files.download({ file: video, downloadPath: videoPath });
            logger.system(`[gemini] Video saved to: ${videoPath}`);
            return {
                content: [{ type: "text", text: `Video generated and saved to: ${videoPath}` }],
            };
        }
        // -- Text-to-speech (TTS) generation ----------------------------------
        if (outputModality === "speech") {
            const speechConfig = {};
            if (args.multi_speaker && args.multi_speaker.length > 0) {
                // Multi-speaker TTS
                speechConfig.multiSpeakerVoiceConfig = {
                    speakerVoiceConfigs: args.multi_speaker.map((s) => ({
                        speaker: s.speaker,
                        voiceConfig: {
                            prebuiltVoiceConfig: { voiceName: s.voice },
                        },
                    })),
                };
            }
            else {
                // Single-speaker TTS
                const voiceName = args.voice_name ?? "Kore";
                speechConfig.voiceConfig = {
                    prebuiltVoiceConfig: { voiceName },
                };
            }
            const ttsResponse = await ai.models.generateContent({
                model: modelId,
                contents: createUserContent([{ text: args.prompt }]),
                config: {
                    responseModalities: ["AUDIO"],
                    speechConfig,
                },
            });
            const ttsParts = ttsResponse.candidates?.[0]?.content?.parts;
            if (!ttsParts || ttsParts.length === 0) {
                return {
                    content: [{ type: "text", text: "Gemini TTS returned no content. The request may have been filtered by safety settings." }],
                    isError: true,
                };
            }
            const audioPart = ttsParts.find((p) => p.inlineData?.data);
            if (!audioPart?.inlineData?.data) {
                return {
                    content: [{ type: "text", text: "Gemini TTS returned no audio data." }],
                    isError: true,
                };
            }
            // Gemini TTS returns raw PCM (24kHz, 16-bit, mono) — wrap in WAV header
            const pcmData = Buffer.from(audioPart.inlineData.data, "base64");
            const wavHeader = buildWavHeader(pcmData.byteLength);
            const wavBuffer = Buffer.concat([wavHeader, pcmData]);
            ensureTempDir();
            const ttsFilename = `gemini-tts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.wav`;
            const ttsPath = path.join(GEMINI_TEMP_DIR, ttsFilename);
            fs.writeFileSync(ttsPath, wavBuffer);
            logger.system(`[gemini] TTS audio saved to: ${ttsPath} (${(wavBuffer.byteLength / 1024).toFixed(1)} KB)`);
            return {
                content: [{ type: "text", text: `Speech audio generated and saved to: ${ttsPath}` }],
            };
        }
        // -- Build content parts (for generateContent path) -----------------
        const contentParts = [];
        // Process input files
        if (args.input_files) {
            for (const filePath of args.input_files) {
                if (!fs.existsSync(filePath)) {
                    return {
                        content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
                        isError: true,
                    };
                }
                const mime = mimeFromPath(filePath);
                if (FILE_API_MIMES.has(mime)) {
                    // Large files (videos) -> upload via Files API
                    logger.system(`[gemini] Uploading file via Files API: ${filePath}`);
                    const uploaded = await ai.files.upload({ file: filePath, config: { mimeType: mime } });
                    if (!uploaded.name) {
                        return {
                            content: [{ type: "text", text: `Error: File upload returned no name for ${filePath}` }],
                            isError: true,
                        };
                    }
                    await waitForFileActive(ai, uploaded.name);
                    contentParts.push(createPartFromUri(uploaded.uri ?? "", uploaded.mimeType ?? mime));
                }
                else {
                    // Small files (images, audio, documents) -> inline base64
                    const buf = fs.readFileSync(filePath);
                    contentParts.push(createPartFromBase64(buf.toString("base64"), mime));
                }
            }
        }
        // Add the text prompt
        contentParts.push({ text: args.prompt });
        // -- Build generation config ----------------------------------------
        const config = {};
        if (outputModality === "image") {
            config.responseModalities = ["IMAGE", "TEXT"];
        }
        else if (outputModality === "audio") {
            config.responseModalities = ["AUDIO"];
        }
        // -- Call Gemini (generateContent) -----------------------------------
        const response = await ai.models.generateContent({
            model: modelId,
            contents: createUserContent(contentParts),
            ...(Object.keys(config).length > 0 ? { config } : {}),
        });
        // -- Process response ------------------------------------------------
        const parts = response.candidates?.[0]?.content?.parts;
        if (!parts || parts.length === 0) {
            return {
                content: [
                    { type: "text", text: "Gemini returned no content. The request may have been filtered by safety settings." },
                ],
                isError: true,
            };
        }
        const resultContent = [];
        let savedPath;
        for (const part of parts) {
            if (part.text) {
                resultContent.push({ type: "text", text: part.text });
            }
            else if (part.inlineData) {
                const { data, mimeType } = part.inlineData;
                if (data) {
                    const mime = mimeType ?? (outputModality === "audio" ? "audio/mpeg" : "image/png");
                    savedPath = saveToTemp(data, extForMime(mime));
                    if (mime.startsWith("image/")) {
                        // Return image data so the agent can see it
                        resultContent.push({ type: "image", data, mimeType: mime });
                    }
                    resultContent.push({
                        type: "text",
                        text: `File saved to: ${savedPath}`,
                    });
                }
            }
        }
        return { content: resultContent };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        logger.err(`[gemini] Operation failed: ${message}`);
        return {
            content: [{ type: "text", text: `Gemini operation failed: ${message}` }],
            isError: true,
        };
    }
});
//# sourceMappingURL=gemini.js.map