import { readFileSync, writeFileSync } from "fs";
import { GraphAILogger, sleep } from "graphai";
import { GoogleGenAI, PersonGeneration } from "@google/genai";
import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, imageAction, movieFileTarget, videoDurationTarget, unsupportedModelTarget, hasCause, } from "../utils/error_cause.js";
import { getAspectRatio } from "../utils/utils.js";
import { ASPECT_RATIOS } from "../types/const.js";
import { getModelDuration, provider2MovieAgent, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS } from "../types/provider2agent.js";
const pollUntilDone = async (ai, operation) => {
    const response = { operation };
    while (!response.operation.done) {
        await sleep(5000);
        response.operation = await ai.operations.getVideosOperation(response);
    }
    return response;
};
const getVideoFromResponse = (response, iteration) => {
    const iterationInfo = iteration !== undefined ? ` in iteration ${iteration}` : "";
    if (!response.operation.response?.generatedVideos) {
        throw new Error(`No video${iterationInfo}: ${JSON.stringify(response.operation, null, 2)}`, {
            cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
        });
    }
    const video = response.operation.response.generatedVideos[0].video;
    if (!video) {
        throw new Error(`No video${iterationInfo}`, {
            cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
        });
    }
    return video;
};
const loadImageAsBase64 = (imagePath) => {
    const buffer = readFileSync(imagePath);
    return {
        imageBytes: buffer.toString("base64"),
        mimeType: "image/png",
    };
};
const downloadVideo = async (ai, video, movieFile, isVertexAI) => {
    if (isVertexAI) {
        // Vertex AI returns videoBytes directly
        writeFileSync(movieFile, Buffer.from(video.videoBytes, "base64"));
    }
    else {
        // Gemini API requires download via uri
        await ai.files.download({
            file: video,
            downloadPath: movieFile,
        });
        await sleep(5000); // HACK: Without this, the file is not ready yet.
    }
    return { saved: movieFile };
};
const createVeo31Payload = (model, prompt, aspectRatio, source) => ({
    model,
    prompt,
    config: {
        aspectRatio,
        resolution: "720p",
        numberOfVideos: 1,
    },
    ...source,
});
const generateExtendedVideo = async (ai, model, prompt, aspectRatio, imagePath, requestedDuration, movieFile, isVertexAI) => {
    const initialDuration = 8;
    const maxExtensionDuration = 8;
    const extensionsNeeded = Math.ceil((requestedDuration - initialDuration) / maxExtensionDuration);
    GraphAILogger.info(`Veo 3.1 video extension: ${extensionsNeeded} extensions needed for ${requestedDuration}s target`);
    const generateIteration = async (iteration, accumulatedDuration, previousVideo) => {
        const isInitial = iteration === 0;
        const remainingDuration = requestedDuration - accumulatedDuration;
        const extensionDuration = isInitial ? initialDuration : (getModelDuration("google", model, remainingDuration) ?? maxExtensionDuration);
        const getSource = () => {
            if (isInitial)
                return imagePath ? { image: loadImageAsBase64(imagePath) } : undefined;
            return previousVideo?.uri ? { video: { uri: previousVideo.uri } } : undefined;
        };
        const payload = createVeo31Payload(model, prompt, aspectRatio, getSource());
        GraphAILogger.info(isInitial ? "Generating initial 8s video..." : `Extending video: iteration ${iteration}/${extensionsNeeded} (+${extensionDuration}s)...`);
        const operation = await ai.models.generateVideos(payload);
        const response = await pollUntilDone(ai, operation);
        const video = getVideoFromResponse(response, iteration);
        const totalDuration = accumulatedDuration + extensionDuration;
        GraphAILogger.info(`Video ${isInitial ? "generated" : "extended"}: ~${totalDuration}s total`);
        return { video, duration: totalDuration };
    };
    const result = await Array.from({ length: extensionsNeeded + 1 }).reduce(async (prev, _, index) => {
        const { video, duration } = await prev;
        return generateIteration(index, duration, video);
    }, Promise.resolve({ video: undefined, duration: 0 }));
    if (!result.video) {
        throw new Error("Failed to generate extended video", {
            cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
        });
    }
    return downloadVideo(ai, result.video, movieFile, isVertexAI);
};
const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI) => {
    const capabilities = provider2MovieAgent.google.modelParams[model];
    const payload = {
        model,
        prompt,
        config: {
            durationSeconds: capabilities?.supportsDuration === false ? undefined : duration,
            aspectRatio,
            personGeneration: imagePath || !capabilities?.supportsPersonGeneration ? undefined : PersonGeneration.ALLOW_ALL,
        },
        image: imagePath ? loadImageAsBase64(imagePath) : undefined,
    };
    // Validate and apply lastFrame
    if (lastFrameImagePath) {
        if (!capabilities?.supportsLastFrame) {
            GraphAILogger.warn(`movieGenAIAgent: model ${model} does not support lastFrame — ignoring lastFrameImageName`);
        }
        else if (!imagePath) {
            GraphAILogger.warn(`movieGenAIAgent: lastFrame requires a first frame image (imagePrompt or firstFrameImageName) — ignoring lastFrameImageName`);
        }
        else {
            payload.config.lastFrame = loadImageAsBase64(lastFrameImagePath);
        }
    }
    // Validate and apply referenceImages (mutually exclusive with image/lastFrame)
    if (referenceImages && referenceImages.length > 0) {
        if (!capabilities?.supportsReferenceImages) {
            GraphAILogger.warn(`movieGenAIAgent: model ${model} does not support referenceImages — ignoring`);
        }
        else if (imagePath) {
            GraphAILogger.warn(`movieGenAIAgent: referenceImages cannot be combined with first frame image — ignoring referenceImages`);
        }
        else if (lastFrameImagePath) {
            GraphAILogger.warn(`movieGenAIAgent: referenceImages cannot be combined with lastFrame — ignoring referenceImages`);
        }
        else {
            payload.config.referenceImages = referenceImages.map((ref) => ({
                image: loadImageAsBase64(ref.imagePath),
                referenceType: ref.referenceType,
            }));
        }
    }
    const operation = await ai.models.generateVideos(payload);
    const response = await pollUntilDone(ai, operation);
    const video = getVideoFromResponse(response);
    return downloadVideo(ai, video, movieFile, isVertexAI);
};
export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
    const { prompt, imagePath, lastFrameImagePath, referenceImages, movieFile } = namedInputs;
    const aspectRatio = getAspectRatio(params.canvasSize, ASPECT_RATIOS);
    const model = params.model ?? provider2MovieAgent.google.defaultModel;
    const apiKey = config?.apiKey;
    try {
        const requestedDuration = params.duration ?? 8;
        const duration = getModelDuration("google", model, requestedDuration);
        if (duration === undefined) {
            throw new Error(`Duration ${requestedDuration} is not supported for model ${model}.`, {
                cause: agentGenerationError("movieGenAIAgent", imageAction, videoDurationTarget),
            });
        }
        // Check generateAudio compatibility (Google API has no toggle)
        if (params.generateAudio !== undefined) {
            const audio = provider2MovieAgent.google.modelParams[model]?.audio ?? { mode: AUDIO_MODE_NEVER };
            if (audio.mode === AUDIO_MODE_NEVER && params.generateAudio === true) {
                throw new Error(`Model ${model} does not support audio generation`, {
                    cause: agentGenerationError("movieGenAIAgent", imageAction, unsupportedModelTarget),
                });
            }
            else if (audio.mode === AUDIO_MODE_ALWAYS && params.generateAudio === false) {
                GraphAILogger.warn(`movieGenAIAgent: model ${model} always generates audio — ignoring generateAudio=false`);
            }
        }
        const isVertexAI = !!params.vertexai_project;
        const ai = isVertexAI
            ? new GoogleGenAI({
                vertexai: true,
                project: params.vertexai_project,
                location: params.vertexai_location ?? "us-central1",
            })
            : (() => {
                if (!apiKey) {
                    throw new Error("Google GenAI authentication is required. Either set GEMINI_API_KEY (Gemini API) or specify movieParams.vertexai_project (Vertex AI). See docs/vertexai_en.md or docs/vertexai_ja.md.", {
                        cause: apiKeyMissingError("movieGenAIAgent", imageAction, "GEMINI_API_KEY"),
                    });
                }
                return new GoogleGenAI({ apiKey });
            })();
        // Veo 3.1: Video extension mode for videos longer than 8s
        if (model === "veo-3.1-generate-preview" && requestedDuration > 8 && params.canvasSize) {
            return generateExtendedVideo(ai, model, prompt, aspectRatio, imagePath, requestedDuration, movieFile, isVertexAI);
        }
        // Standard mode
        return generateStandardVideo(ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI);
    }
    catch (error) {
        GraphAILogger.info("Failed to generate movie:", error.message);
        if (hasCause(error) && error.cause) {
            throw error;
        }
        throw new Error("Failed to generate movie with Google GenAI", {
            cause: agentGenerationError("movieGenAIAgent", imageAction, movieFileTarget),
        });
    }
};
const movieGenAIAgentInfo = {
    name: "movieGenAIAgent",
    agent: movieGenAIAgent,
    mock: movieGenAIAgent,
    samples: [],
    description: "Google Movie agent",
    category: ["movie"],
    author: "Receptron Team",
    repository: "https://github.com/receptron/mulmocast-cli/",
    license: "MIT",
    environmentVariables: [],
};
export default movieGenAIAgentInfo;
