import { pipeline } from '@xenova/transformers';
import { DEFAULT_EMBEDDING_MODEL, INDEX_PATH } from './constants.js';
import { normalizeVector, tensorToVector } from './embeddings.js';
import { loadEmbeddingIndex } from './storage.js';
const MAX_TOP_K = 100;
const toFloat32 = (values, dimension) => {
    if (values.length !== dimension) {
        throw new Error(`Embedding dimension mismatch. Expected ${dimension}, received ${values.length}`);
    }
    return Float32Array.from(values);
};
const cosineSimilarity = (a, b) => {
    if (a.length !== b.length) {
        throw new Error('Embedding vectors have different dimensions');
    }
    let dot = 0;
    for (let index = 0; index < a.length; index += 1) {
        dot += a[index] * b[index];
    }
    return dot;
};
export class RagSearcher {
    indexPromise;
    embedderPromise;
    indexPath;
    constructor(indexPath = INDEX_PATH) {
        this.indexPath = indexPath;
    }
    async search(query, topK = 5, packageName, page = 1) {
        const sanitizedQuery = query.trim();
        const pageSize = clampPageSize(topK);
        const currentPage = clampPageNumber(page);
        if (!sanitizedQuery) {
            return emptySearchPage(pageSize, currentPage);
        }
        const index = await this.getIndex();
        if (index.chunks.length === 0) {
            return emptySearchPage(pageSize, currentPage);
        }
        const filteredChunks = filterChunksByPackage(index.chunks, packageName);
        if (filteredChunks.length === 0) {
            return emptySearchPage(pageSize, currentPage);
        }
        const embedder = await this.getEmbedder(index.model);
        const tensor = await embedder(sanitizedQuery, { pooling: 'mean' });
        const queryVector = Float32Array.from(normalizeVector(tensorToVector(tensor)));
        const ranked = filteredChunks
            .map((chunk) => ({
            chunk,
            score: cosineSimilarity(queryVector, chunk.embedding),
        }))
            .filter((item) => Number.isFinite(item.score))
            .sort((left, right) => right.score - left.score);
        const total = ranked.length;
        if (total === 0) {
            return emptySearchPage(pageSize, currentPage);
        }
        const startIndex = (currentPage - 1) * pageSize;
        if (startIndex >= total) {
            return {
                results: [],
                total,
                page: currentPage,
                pageSize,
                hasMore: false,
            };
        }
        const rows = ranked.slice(startIndex, startIndex + pageSize);
        const results = rows.map(({ chunk, score }) => ({
            id: chunk.id,
            text: chunk.text,
            metadata: chunk.metadata,
            score,
        }));
        const hasMore = startIndex + results.length < total;
        return {
            results,
            total,
            page: currentPage,
            pageSize,
            hasMore,
        };
    }
    async getIndex() {
        if (!this.indexPromise) {
            this.indexPromise = this.loadIndex();
        }
        return this.indexPromise;
    }
    async loadIndex() {
        const persisted = await loadEmbeddingIndex(this.indexPath).catch((error) => {
            if (isErrnoException(error) && error.code === 'ENOENT') {
                throw new Error(`Embedding index not found at ${this.indexPath}. Run the build index script before searching.`);
            }
            throw error;
        });
        const dimension = persisted.dimension;
        const chunks = persisted.chunks.map((chunk) => ({
            ...chunk,
            embedding: toFloat32(normalizeVector(chunk.embedding), dimension),
        }));
        return {
            ...persisted,
            model: persisted.model ?? DEFAULT_EMBEDDING_MODEL,
            chunks,
        };
    }
    async getEmbedder(modelFromIndex) {
        if (!this.embedderPromise) {
            const model = process.env.DOCS_MCP_EMBEDDING_MODEL ?? modelFromIndex ?? DEFAULT_EMBEDDING_MODEL;
            this.embedderPromise = pipeline('feature-extraction', model);
        }
        return this.embedderPromise;
    }
}
export const createRagSearcher = (indexPath = INDEX_PATH) => new RagSearcher(indexPath);
const isErrnoException = (error) => {
    return typeof error === 'object' && error !== null && 'code' in error;
};
const filterChunksByPackage = (chunks, packageName) => {
    if (!packageName) {
        return chunks;
    }
    const normalized = packageName.trim().toLowerCase();
    if (!normalized) {
        return chunks;
    }
    const prefix = `${normalized}/`;
    return chunks.filter((chunk) => chunk.metadata.source.startsWith(prefix));
};
const clampPageSize = (value) => {
    if (!Number.isFinite(value)) {
        return 5;
    }
    const rounded = Math.floor(value);
    if (rounded < 1) {
        return 1;
    }
    if (rounded > MAX_TOP_K) {
        return MAX_TOP_K;
    }
    return rounded;
};
const clampPageNumber = (value) => {
    if (!Number.isFinite(value)) {
        return 1;
    }
    const rounded = Math.floor(value);
    return rounded < 1 ? 1 : rounded;
};
const emptySearchPage = (pageSize, page) => ({
    results: [],
    total: 0,
    page,
    pageSize,
    hasMore: false,
});
//# sourceMappingURL=search.js.map