/**
 * Spectral Vision Extension
 *
 * Automatically switches to a vision-capable model for image processing
 * when images are attached. This allows non-vision models to "see" images
 * and saves main agent context by using a potentially smaller/cheaper
 * vision model for image understanding.
 *
 * Logic:
 *   - If the main model supports vision natively, images pass through
 *     directly — no interception, no text descriptions.
 *   - When the main model does NOT support vision, falls back to the
 *     admin-configured default vision model (isVisionDefault flag from
 *     backend via SettingsManager) for image descriptions.
 *   - If no admin default is configured, falls back to the best
 *     available vision-capable model by provider priority.
 *
 * Hooks into the `context` event to intercept ALL images before they reach
 * the LLM (covers user-attached images and tool-result images alike).
 */
import { streamSimple } from "../sdk/ai/index.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Check if the SettingsManager has an admin-configured default vision model
 * with working auth. Returns the model if found and auth-ready, or undefined.
 */
function getAdminVisionModel(ctx) {
    const settings = ctx.settingsManager;
    if (!settings)
        return undefined;
    const defaultVisionProvider = settings.getDefaultVisionProvider();
    const defaultVisionModel = settings.getDefaultVisionModel();
    if (!defaultVisionProvider || !defaultVisionModel)
        return undefined;
    const allModels = ctx.modelRegistry.getAll();
    const match = allModels.find((m) => m.provider === defaultVisionProvider && m.id === defaultVisionModel);
    if (!match || !match.input.includes("image"))
        return undefined;
    if (!ctx.modelRegistry.hasConfiguredAuth(match))
        return undefined;
    return match;
}
/**
 * Resolve the best vision model to use for image descriptions.
 *
 * Priority:
 *   1. Current main model if it supports images — passthrough, no interception
 *   2. Admin-configured default vision model (isVisionDefault from backend)
 *   3. Best available vision model by provider priority
 *   4. First available vision model
 */
function resolveVisionModel(ctx) {
    // 1. If the main model supports vision natively, skip interception entirely
    //    and let the main model process images directly.
    const currentModel = ctx.model;
    if (currentModel?.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(currentModel)) {
        process.stderr.write(`[spectral-vision] Main model supports vision — passing images through natively (${currentModel.provider}/${currentModel.id})\n`);
        return undefined;
    }
    // 2. Admin-configured default vision model (fallback for non-vision main models)
    const adminModel = getAdminVisionModel(ctx);
    if (adminModel) {
        process.stderr.write(`[spectral-vision] Using admin-configured default: ${adminModel.provider}/${adminModel.id}\n`);
        return adminModel;
    }
    // 3. Fall back to best available vision model by provider priority
    const allModels = ctx.modelRegistry.getAll();
    const visionModels = allModels.filter((m) => m.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(m));
    if (visionModels.length === 0)
        return undefined;
    const providerPriority = ["anthropic", "openai", "google", "openrouter"];
    for (const provider of providerPriority) {
        const match = visionModels.find((m) => m.provider === provider);
        if (match) {
            process.stderr.write(`[spectral-vision] Using fallback vision model: ${match.provider}/${match.id}\n`);
            return match;
        }
    }
    // 4. First available vision model
    const fallback = visionModels[0];
    process.stderr.write(`[spectral-vision] Using first available vision model: ${fallback.provider}/${fallback.id}\n`);
    return fallback;
}
/** Check if any content block in an array is an image */
function hasImageContent(content) {
    return Array.isArray(content) && content.some((c) => c?.type === "image");
}
/** Count images in a message */
function countImages(msg) {
    if (!Array.isArray(msg.content))
        return 0;
    return msg.content.filter((c) => c.type === "image").length;
}
// ---------------------------------------------------------------------------
// Core: call vision model to describe images
// ---------------------------------------------------------------------------
async function describeImages(visionModel, content, contextText, ctx) {
    const userContent = [
        {
            type: "text",
            text: `Please describe the following image(s). ` +
                (contextText ? `Context: ${contextText}. ` : "") +
                "Focus on what is visually visible — text content, UI elements, diagrams, " +
                "code structure, layout, colors, etc. Be concise but thorough. " +
                "If multiple images are provided, describe each one separately with a heading.",
        },
    ];
    const textPrefixBlocks = [];
    let imageCount = 0;
    for (const block of content) {
        if (block.type === "image") {
            userContent.push(block);
            imageCount++;
        }
        else if (block.type === "text") {
            textPrefixBlocks.push(block);
        }
    }
    if (imageCount === 0)
        return content;
    const auth = await ctx.modelRegistry.getApiKeyAndHeaders(visionModel);
    if (!auth.ok) {
        process.stderr.write(`[spectral-vision] No API key for vision model ${visionModel.provider}/${visionModel.id}\n`);
        return content;
    }
    try {
        const result = await streamSimple(visionModel, {
            systemPrompt: "You are an image description assistant. Describe images accurately and concisely.",
            messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
            tools: [],
        }, {
            apiKey: auth.apiKey,
            headers: auth.headers,
        });
        let description = "";
        for await (const event of result) {
            if (event.type === "text_delta") {
                description += event.delta;
            }
        }
        const textNote = description.trim()
            ? `[Image description from ${visionModel.provider}/${visionModel.id} (${imageCount} image(s)):\n${description}\n]`
            : `[${imageCount} image(s) — vision model returned empty description]`;
        // Notify the frontend that the vision extension analyzed the image(s)
        try {
            ctx.ui.notify(`[spectral-vision] Analyzed ${imageCount} image(s) with ${visionModel.provider}/${visionModel.id}`, "info");
        }
        catch {
            // UI context may not be available in headless modes
        }
        return [
            ...textPrefixBlocks,
            { type: "text", text: textNote },
        ];
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stderr.write(`[spectral-vision] Vision call failed: ${msg}\n`);
        // Notify frontend about the failure
        try {
            ctx.ui.notify(`[spectral-vision] Failed to analyze ${imageCount} image(s): ${msg}`, "warning");
        }
        catch {
            // UI context may not be available in headless modes
        }
        return [
            ...textPrefixBlocks,
            {
                type: "text",
                text: `[${imageCount} image(s) omitted — vision fallback failed: ${msg}]`,
            },
        ];
    }
}
// ---------------------------------------------------------------------------
// Extension entry point
// ---------------------------------------------------------------------------
export default function spectralVisionExtension(ext) {
    let visionModel;
    ext.on("session_start", (_event, ctx) => {
        visionModel = resolveVisionModel(ctx);
        if (visionModel) {
            process.stderr.write(`[spectral-vision] Ready — using ${visionModel.provider}/${visionModel.id} for image descriptions.\n`);
        }
        else if (ctx.model?.input.includes("image")) {
            process.stderr.write(`[spectral-vision] Ready — main model ${ctx.model.provider}/${ctx.model.id} handles vision natively, passthrough enabled.\n`);
        }
        else {
            process.stderr.write("[spectral-vision] No vision model with auth configured. Image description disabled.\n");
        }
    });
    ext.on("context", async (event, ctx) => {
        const messages = event.messages;
        // Check if any message contains images
        let totalImages = 0;
        for (const msg of messages) {
            totalImages += countImages(msg);
        }
        if (totalImages === 0)
            return;
        // Re-resolve vision model each time images are encountered.
        // If the main model supports vision natively, resolveVisionModel
        // returns undefined and we pass images through without interception.
        visionModel = resolveVisionModel(ctx);
        if (!visionModel) {
            // Check if we're in passthrough mode (main model has native vision)
            if (ctx.model?.input.includes("image")) {
                process.stderr.write(`[spectral-vision] Passthrough — ${totalImages} image(s) flowing to main model natively\n`);
            }
            else {
                process.stderr.write("[spectral-vision] No vision model available, images will be omitted\n");
            }
            return;
        }
        process.stderr.write(`[spectral-vision] Describing ${totalImages} image(s) with ${visionModel.provider}/${visionModel.id}...\n`);
        // Process each message — intercept both user messages and tool results
        // that contain images (e.g. read tool returning a screenshot).
        const processed = await Promise.all(messages.map(async (msg) => {
            if ((msg.role !== "user" && msg.role !== "toolResult") || !Array.isArray(msg.content) || !hasImageContent(msg.content)) {
                return msg;
            }
            // Extract context from preceding text blocks
            const textBlocks = msg.content
                .filter((c) => c.type === "text")
                .map((c) => c.text)
                .join(" ");
            const contextText = textBlocks.substring(0, 500); // limit context
            const described = await describeImages(visionModel, msg.content, contextText, ctx);
            return { ...msg, content: described };
        }));
        return { messages: processed };
    });
}
