import fsSync from "node:fs";
import { resolveUserPath } from "../utils.js";
import { createGeminiEmbeddingProvider } from "./embeddings-gemini.js";
import { createOpenAiEmbeddingProvider } from "./embeddings-openai.js";
import { importNodeLlamaCpp } from "./node-llama.js";
const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
function canAutoSelectLocal(options) {
    const modelPath = options.local?.modelPath?.trim();
    if (!modelPath)
        return false;
    if (/^(hf:|https?:)/i.test(modelPath))
        return false;
    const resolved = resolveUserPath(modelPath);
    try {
        return fsSync.statSync(resolved).isFile();
    }
    catch {
        return false;
    }
}
function isMissingApiKeyError(err) {
    const message = formatError(err);
    return message.includes("No API key found for provider");
}
async function createLocalEmbeddingProvider(options) {
    const modelPath = options.local?.modelPath?.trim() || DEFAULT_LOCAL_MODEL;
    const modelCacheDir = options.local?.modelCacheDir?.trim();
    // Lazy-load node-llama-cpp to keep startup light unless local is enabled.
    const { getLlama, resolveModelFile, LlamaLogLevel } = await importNodeLlamaCpp();
    let llama = null;
    let embeddingModel = null;
    let embeddingContext = null;
    const ensureContext = async () => {
        if (!llama) {
            llama = await getLlama({ logLevel: LlamaLogLevel.error });
        }
        if (!embeddingModel) {
            const resolved = await resolveModelFile(modelPath, modelCacheDir || undefined);
            embeddingModel = await llama.loadModel({ modelPath: resolved });
        }
        if (!embeddingContext) {
            embeddingContext = await embeddingModel.createEmbeddingContext();
        }
        return embeddingContext;
    };
    return {
        id: "local",
        model: modelPath,
        embedQuery: async (text) => {
            const ctx = await ensureContext();
            const embedding = await ctx.getEmbeddingFor(text);
            return Array.from(embedding.vector);
        },
        embedBatch: async (texts) => {
            const ctx = await ensureContext();
            const embeddings = await Promise.all(texts.map(async (text) => {
                const embedding = await ctx.getEmbeddingFor(text);
                return Array.from(embedding.vector);
            }));
            return embeddings;
        },
    };
}
export async function createEmbeddingProvider(options) {
    const requestedProvider = options.provider;
    const fallback = options.fallback;
    const createProvider = async (id) => {
        if (id === "local") {
            const provider = await createLocalEmbeddingProvider(options);
            return { provider };
        }
        if (id === "gemini") {
            const { provider, client } = await createGeminiEmbeddingProvider(options);
            return { provider, gemini: client };
        }
        const { provider, client } = await createOpenAiEmbeddingProvider(options);
        return { provider, openAi: client };
    };
    const formatPrimaryError = (err, provider) => provider === "local" ? formatLocalSetupError(err) : formatError(err);
    if (requestedProvider === "auto") {
        const missingKeyErrors = [];
        let localError = null;
        if (canAutoSelectLocal(options)) {
            try {
                const local = await createProvider("local");
                return { ...local, requestedProvider };
            }
            catch (err) {
                localError = formatLocalSetupError(err);
            }
        }
        for (const provider of ["openai", "gemini"]) {
            try {
                const result = await createProvider(provider);
                return { ...result, requestedProvider };
            }
            catch (err) {
                const message = formatPrimaryError(err, provider);
                if (isMissingApiKeyError(err)) {
                    missingKeyErrors.push(message);
                    continue;
                }
                throw new Error(message);
            }
        }
        // All cloud providers failed (missing API keys). Try local as last resort
        // — this allows auto-download of the default HF model via node-llama-cpp.
        if (!localError) {
            try {
                const local = await createProvider("local");
                return { ...local, requestedProvider };
            }
            catch (err) {
                localError = formatLocalSetupError(err);
            }
        }
        const details = [...missingKeyErrors, localError].filter(Boolean);
        if (details.length > 0) {
            throw new Error(details.join("\n\n"));
        }
        throw new Error("No embeddings provider available.");
    }
    try {
        const primary = await createProvider(requestedProvider);
        return { ...primary, requestedProvider };
    }
    catch (primaryErr) {
        const reason = formatPrimaryError(primaryErr, requestedProvider);
        if (fallback && fallback !== "none" && fallback !== requestedProvider) {
            try {
                const fallbackResult = await createProvider(fallback);
                return {
                    ...fallbackResult,
                    requestedProvider,
                    fallbackFrom: requestedProvider,
                    fallbackReason: reason,
                };
            }
            catch (fallbackErr) {
                throw new Error(`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`);
            }
        }
        throw new Error(reason);
    }
}
function formatError(err) {
    if (err instanceof Error)
        return err.message;
    return String(err);
}
function isNodeLlamaCppMissing(err) {
    if (!(err instanceof Error))
        return false;
    const code = err.code;
    if (code === "ERR_MODULE_NOT_FOUND") {
        return err.message.includes("node-llama-cpp");
    }
    return false;
}
function formatLocalSetupError(err) {
    const detail = formatError(err);
    const missing = isNodeLlamaCppMissing(err);
    return [
        "Local embeddings unavailable.",
        missing
            ? "Reason: optional dependency node-llama-cpp is missing (or failed to install)."
            : detail
                ? `Reason: ${detail}`
                : undefined,
        missing && detail ? `Detail: ${detail}` : null,
        "To enable local embeddings:",
        "1) Use Node 22 LTS (recommended for installs/updates)",
        missing
            ? "2) Reinstall Clawdbot (this should install node-llama-cpp): npm i -g clawdbot@latest"
            : null,
        "3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
        'Or set agents.defaults.memorySearch.provider = "openai" (remote).',
    ]
        .filter(Boolean)
        .join("\n");
}
