import { clamp, truncate } from '../util/text.js';
import { HashModelProvider } from './provider.js';
const DEFAULT_MODEL_ID = 'onnx-community/bge-reranker-v2-m3-ONNX';
const DEFAULT_TOP_K = 16;
export class LocalCrossEncoderProvider {
    name = 'local-cross-encoder';
    fallback;
    modelId;
    topK;
    cacheDir;
    scorerPromise = null;
    hasLoggedLoadFailure = false;
    constructor(options = {}) {
        this.fallback = options.fallback ?? new HashModelProvider(options.embeddingDimensions ?? 1536);
        this.modelId = options.modelId ?? process.env.TUBEROSA_RERANKER_MODEL ?? DEFAULT_MODEL_ID;
        this.topK = options.topK ?? Number(process.env.TUBEROSA_RERANKER_TOPK ?? DEFAULT_TOP_K);
        this.cacheDir = options.cacheDir ?? process.env.TUBEROSA_MODEL_CACHE_DIR;
        if (options.scorer) {
            this.scorerPromise = Promise.resolve(options.scorer);
        }
    }
    async embed(text) {
        return this.fallback.embed(text);
    }
    async rewriteQuery(input) {
        return this.fallback.rewriteQuery(input);
    }
    async rerank(input) {
        if (input.candidates.length === 0) {
            return { candidates: [] };
        }
        const scorer = await this.loadScorer();
        if (!scorer) {
            return this.fallback.rerank(input);
        }
        const windowSize = Math.min(this.topK, input.candidates.length);
        const window = input.candidates.slice(0, windowSize);
        const tail = input.candidates.slice(windowSize);
        const payload = window.map((candidate) => ({
            knowledgeId: candidate.knowledgeId,
            text: truncate(candidate.contextualContent || candidate.content || candidate.summary || candidate.title, 1024),
        }));
        let scores = [];
        try {
            scores = await scorer.score(input.prompt, payload);
        }
        catch (error) {
            this.logLoadFailure(`local reranker scoring threw: ${error instanceof Error ? error.message : String(error)}`);
            return this.fallback.rerank(input);
        }
        const reranked = window.map((candidate, index) => {
            const localScore = clamp(scores[index] ?? 0, 0, 1);
            const trustScore = clamp(candidate.trustLevel / 100, 0, 1);
            // 0.70 to the local cross-encoder, 0.22 to the fused retrieval score, 0.08 to trust.
            const blended = clamp(localScore * 0.7 + candidate.fusedScore * 0.22 + trustScore * 0.08, 0, 1);
            return {
                ...candidate,
                rerankScore: blended,
                finalScore: blended,
                matchReasons: [...candidate.matchReasons, `local-rerank:${this.modelId}:${localScore.toFixed(3)}`],
                metadata: {
                    ...(candidate.metadata ?? {}),
                    localRerank: {
                        model: this.modelId,
                        score: localScore,
                    },
                },
            };
        });
        const fallbackForTail = tail.length > 0
            ? (await this.fallback.rerank({ ...input, candidates: tail })).candidates
            : [];
        const merged = [...reranked, ...fallbackForTail]
            .sort((left, right) => right.finalScore - left.finalScore || left.rank - right.rank)
            .map((candidate, index) => ({ ...candidate, rank: index + 1 }));
        return { candidates: merged, model: this.modelId };
    }
    async loadScorer() {
        if (this.scorerPromise)
            return this.scorerPromise;
        this.scorerPromise = this.createDefaultScorer();
        return this.scorerPromise;
    }
    async createDefaultScorer() {
        try {
            const transformers = (await dynamicImport('@xenova/transformers'));
            if (!transformers || typeof transformers.pipeline !== 'function') {
                this.logLoadFailure('@xenova/transformers is not installed; install it to enable local reranking');
                return null;
            }
            if (this.cacheDir && transformers.env) {
                transformers.env.cacheDir = this.cacheDir;
            }
            const pipeline = await transformers.pipeline('text-classification', this.modelId, {
                quantized: true,
            });
            return new TransformersScorer(pipeline);
        }
        catch (error) {
            this.logLoadFailure(`local reranker init failed: ${error instanceof Error ? error.message : String(error)}`);
            return null;
        }
    }
    logLoadFailure(reason) {
        if (this.hasLoggedLoadFailure)
            return;
        this.hasLoggedLoadFailure = true;
        if ((process.env.NODE_ENV ?? '') === 'test' || process.env.TUBEROSA_SILENT_LOCAL_PROVIDER === 'true')
            return;
        process.stderr.write(`[tuberosa] local cross-encoder unavailable; falling back to hash rerank — ${reason}\n`);
    }
}
class TransformersScorer {
    pipeline;
    constructor(pipeline) {
        this.pipeline = pipeline;
    }
    async score(prompt, candidates) {
        if (candidates.length === 0)
            return [];
        const pairs = candidates.map((candidate) => ({ text: prompt, text_pair: candidate.text }));
        const raw = await this.pipeline(pairs, { topk: 1 });
        if (!Array.isArray(raw))
            return candidates.map(() => 0);
        return raw.map((entry) => normalizeScore(entry));
    }
}
function normalizeScore(entry) {
    if (Array.isArray(entry)) {
        return normalizeScore(entry[0]);
    }
    if (typeof entry === 'object' && entry !== null && 'score' in entry) {
        const value = entry.score;
        if (typeof value === 'number' && Number.isFinite(value))
            return clamp(value, 0, 1);
    }
    if (typeof entry === 'number' && Number.isFinite(entry))
        return clamp(entry, 0, 1);
    return 0;
}
/** Indirection so bundlers / tsc don't try to resolve the optional package statically. */
async function dynamicImport(specifier) {
    try {
        // eslint-disable-next-line no-new-func
        const importer = new Function('s', 'return import(s)');
        return await importer(specifier);
    }
    catch {
        return null;
    }
}
