const MAX_SYMBOL_SOURCE_CHARS = 200;
const MAX_ERROR_DETAIL_CHARS = 200;
const EMBEDDING_TIMEOUT_MS = 30_000;
/**
 * Build a searchable text string from a symbol for embedding.
 * Format: "{kind} {name}\n{signature}\n{docstring first line}\n{body first N chars}"
 */
export function buildSymbolText(symbol) {
    const parts = [`${symbol.kind} ${symbol.name}`];
    if (symbol.signature) {
        parts.push(symbol.signature);
    }
    if (symbol.docstring) {
        const firstLine = symbol.docstring.split("\n")[0]?.trim();
        if (firstLine)
            parts.push(firstLine);
    }
    if (symbol.source) {
        parts.push(symbol.source.slice(0, MAX_SYMBOL_SOURCE_CHARS));
    }
    return parts.join("\n");
}
// ---------------------------------------------------------------------------
// Cosine similarity
// ---------------------------------------------------------------------------
export function cosineSimilarity(a, b) {
    if (a.length !== b.length)
        return 0;
    let dot = 0;
    let normA = 0;
    let normB = 0;
    for (let i = 0; i < a.length; i++) {
        const ai = a[i];
        const bi = b[i];
        dot += ai * bi;
        normA += ai * ai;
        normB += bi * bi;
    }
    const denom = Math.sqrt(normA) * Math.sqrt(normB);
    return denom > 0 ? dot / denom : 0;
}
/**
 * Search embeddings by cosine similarity (linear scan).
 * Returns top-k results sorted by similarity descending.
 */
export function searchSemantic(queryEmbedding, embeddings, symbols, topK) {
    const scored = [];
    for (const [id, vec] of embeddings) {
        if (vec.length !== queryEmbedding.length)
            continue;
        const score = cosineSimilarity(queryEmbedding, vec);
        scored.push({ id, score });
    }
    scored.sort((a, b) => b.score - a.score);
    const results = [];
    for (const { id, score } of scored.slice(0, topK)) {
        const symbol = symbols.get(id);
        if (symbol) {
            results.push({ symbol, score });
        }
    }
    return results;
}
// ---------------------------------------------------------------------------
// Response shape guard for OpenAI / Voyage embedding APIs
// ---------------------------------------------------------------------------
function isEmbeddingResponse(data) {
    if (!data || typeof data !== "object")
        return false;
    const obj = data;
    if (!Array.isArray(obj["data"]))
        return false;
    return obj["data"].every((item) => typeof item === "object" && item !== null && Array.isArray(item["embedding"]));
}
/**
 * Shared fetch+validate logic for OpenAI-compatible embedding APIs.
 * Handles: POST -> check response.ok -> parse JSON -> guard shape -> extract embeddings.
 */
async function fetchEmbeddings(url, apiKey, requestBody, providerName) {
    const response = await fetch(url, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${apiKey}`,
        },
        body: JSON.stringify(requestBody),
        signal: AbortSignal.timeout(EMBEDDING_TIMEOUT_MS),
    });
    if (!response.ok) {
        // SEC-004: Log raw body to stderr only — don't forward to MCP client
        const body = await response.text();
        console.error(`${providerName} API error ${response.status}:`, body);
        throw new Error(`${providerName} API error: ${response.status}`);
    }
    const data = await response.json();
    if (!isEmbeddingResponse(data)) {
        throw new Error(`Unexpected ${providerName} API response shape: ${JSON.stringify(data).slice(0, MAX_ERROR_DETAIL_CHARS)}`);
    }
    return data.data.map((d) => d.embedding);
}
// ---------------------------------------------------------------------------
// Voyage AI provider
// ---------------------------------------------------------------------------
export class VoyageProvider {
    model = "voyage-code-3";
    dimensions = 1024;
    apiKey;
    constructor(apiKey) {
        this.apiKey = apiKey;
    }
    async embed(texts, mode = "document") {
        return fetchEmbeddings("https://api.voyageai.com/v1/embeddings", this.apiKey, { input: texts, model: this.model, input_type: mode === "query" ? "query" : "document" }, "Voyage");
    }
}
// ---------------------------------------------------------------------------
// OpenAI provider
// ---------------------------------------------------------------------------
export class OpenAIProvider {
    model = "text-embedding-3-small";
    dimensions = 1536;
    apiKey;
    constructor(apiKey) {
        this.apiKey = apiKey;
    }
    async embed(texts, _mode = "document") {
        return fetchEmbeddings("https://api.openai.com/v1/embeddings", this.apiKey, { input: texts, model: this.model }, "OpenAI");
    }
}
// ---------------------------------------------------------------------------
// Ollama provider (local)
// ---------------------------------------------------------------------------
export class OllamaProvider {
    model = "nomic-embed-text";
    dimensions = 768;
    baseUrl;
    constructor(baseUrl) {
        this.baseUrl = baseUrl.replace(/\/$/, "");
    }
    async embed(texts, _mode = "document") {
        // Ollama doesn't support batch — call sequentially
        const results = [];
        for (const text of texts) {
            const response = await fetch(`${this.baseUrl}/api/embeddings`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    model: this.model,
                    prompt: text,
                }),
                signal: AbortSignal.timeout(EMBEDDING_TIMEOUT_MS),
            });
            if (!response.ok) {
                // SEC-004: Log raw body to stderr only — don't forward to MCP client
                const body = await response.text();
                console.error(`Ollama API error ${response.status}:`, body);
                throw new Error(`Ollama API error: ${response.status}`);
            }
            const data = await response.json();
            if (!data || typeof data !== "object" || !("embedding" in data) || !Array.isArray(data["embedding"])) {
                throw new Error(`Unexpected Ollama API response shape: ${JSON.stringify(data).slice(0, MAX_ERROR_DETAIL_CHARS)}`);
            }
            results.push(data.embedding);
        }
        return results;
    }
}
// ---------------------------------------------------------------------------
// Local provider (Xenova/transformers — zero-config, no API key)
// ---------------------------------------------------------------------------
const DEFAULT_LOCAL_MODEL = "nomic-ai/nomic-embed-text-v1.5";
const DEFAULT_LOCAL_DIMS = 768;
/**
 * Some open-weights instruction-tuned models require task-specific prefixes for
 * the embeddings to be high quality (Nomic, intfloat/E5). Without these, MTEB
 * drops 5–10 points and our semantic-search results regress vs OpenAI/Voyage.
 *
 * Keys are checked as substrings against the model id, so "Xenova/nomic-embed-*"
 * and "Xenova/multilingual-e5-*" all match without enumerating every variant.
 */
const LOCAL_PREFIX_RULES = [
    { match: "nomic-embed-text", document: "search_document: ", query: "search_query: " },
    { match: "multilingual-e5", document: "passage: ", query: "query: " },
    { match: "/e5-", document: "passage: ", query: "query: " },
];
/** Exported for unit tests. Returns "" when the model needs no task prefix. */
export function getPrefix(model, mode) {
    for (const rule of LOCAL_PREFIX_RULES) {
        if (model.includes(rule.match))
            return mode === "query" ? rule.query : rule.document;
    }
    return "";
}
/**
 * Known dimensions for popular Xenova/HuggingFace embedding models. Keeps
 * `LocalProvider.dimensions` honest when callers swap `CODESIFT_LOCAL_MODEL`.
 * Falls back to {@link DEFAULT_LOCAL_DIMS} for unknown models.
 */
const KNOWN_LOCAL_DIMS = {
    "nomic-ai/nomic-embed-text-v1.5": 768,
    "nomic-ai/nomic-embed-text-v1": 768,
    "Xenova/all-MiniLM-L6-v2": 384,
    "Xenova/all-MiniLM-L12-v2": 384,
    "Xenova/bge-small-en-v1.5": 384,
    "Xenova/bge-base-en-v1.5": 768,
    "Xenova/bge-large-en-v1.5": 1024,
    "Xenova/multilingual-e5-small": 384,
    "Xenova/multilingual-e5-base": 768,
    "Xenova/multilingual-e5-large": 1024,
};
function lookupDimensions(model) {
    return KNOWN_LOCAL_DIMS[model] ?? DEFAULT_LOCAL_DIMS;
}
const localPipelineCache = new Map();
const failedLocalModels = new Set();
let localLoadWarned = false;
let firstLoadAnnounced = false;
async function loadLocalPipeline(model) {
    const cached = localPipelineCache.get(model);
    if (cached)
        return cached;
    if (failedLocalModels.has(model))
        return null;
    try {
        if (!firstLoadAnnounced) {
            firstLoadAnnounced = true;
            console.error(`[codesift] Loading local embedding model ${model} (first-run download ~140MB, cached after).`);
        }
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const transformers = await import("@huggingface/transformers");
        const pipelineFn = transformers.pipeline ?? transformers.default?.pipeline;
        if (!pipelineFn) {
            failedLocalModels.add(model);
            return null;
        }
        // `dtype: "q8"` is the @huggingface/transformers v3 way to request the
        // INT8-quantized ONNX weights (the v2 `{ quantized: true }` flag is gone).
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const extractor = await pipelineFn("feature-extraction", model, { dtype: "q8" });
        const fn = (texts, opts) => extractor(texts, opts);
        localPipelineCache.set(model, fn);
        return fn;
    }
    catch (err) {
        failedLocalModels.add(model);
        if (!localLoadWarned) {
            localLoadWarned = true;
            const message = err instanceof Error ? err.message : String(err);
            console.error(`[codesift] Local embedding model ${model} unavailable: ${message}. Install @huggingface/transformers or set CODESIFT_VOYAGE_API_KEY/CODESIFT_OPENAI_API_KEY.`);
        }
        return null;
    }
}
export class LocalProvider {
    model;
    dimensions;
    constructor(model = DEFAULT_LOCAL_MODEL, dimensions) {
        this.model = model;
        this.dimensions = dimensions ?? lookupDimensions(model);
    }
    async embed(texts, mode = "document") {
        if (texts.length === 0)
            return [];
        const extractor = await loadLocalPipeline(this.model);
        if (!extractor) {
            throw new Error(`Local embedding model unavailable. Install @huggingface/transformers or configure another provider.`);
        }
        const prefix = getPrefix(this.model, mode);
        const prefixed = prefix ? texts.map((t) => prefix + t) : texts;
        const output = await extractor(prefixed, { pooling: "mean", normalize: true });
        const data = output.data instanceof Float32Array ? output.data : Float32Array.from(output.data);
        const dims = output.dims;
        const dim = dims[dims.length - 1] ?? this.dimensions;
        const results = [];
        for (let i = 0; i < texts.length; i++) {
            const start = i * dim;
            results.push(Array.from(data.subarray(start, start + dim)));
        }
        return results;
    }
}
/** Reset local pipeline caches — for testing only. */
export function _resetLocalProvider() {
    localPipelineCache.clear();
    failedLocalModels.clear();
    localLoadWarned = false;
    firstLoadAnnounced = false;
}
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
export function createEmbeddingProvider(provider, config) {
    switch (provider) {
        case "voyage": {
            if (!config.voyageApiKey)
                throw new Error("CODESIFT_VOYAGE_API_KEY not set");
            return new VoyageProvider(config.voyageApiKey);
        }
        case "openai": {
            if (!config.openaiApiKey)
                throw new Error("CODESIFT_OPENAI_API_KEY not set");
            return new OpenAIProvider(config.openaiApiKey);
        }
        case "ollama": {
            if (!config.ollamaUrl)
                throw new Error("CODESIFT_OLLAMA_URL not set");
            return new OllamaProvider(config.ollamaUrl);
        }
        case "local": {
            const model = config.localModel ?? DEFAULT_LOCAL_MODEL;
            return new LocalProvider(model);
        }
    }
}
//# sourceMappingURL=semantic.js.map