import { clamp, truncate } from '../util/text.js';
import { HashModelProvider } from './provider.js';
const DEFAULT_MODEL_ID = 'dengcao/Qwen3-Reranker-0.6B';
const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
const DEFAULT_TOP_K = 16;
const DEFAULT_TIMEOUT_MS = 10_000;
export class OllamaRerankProvider {
    name = 'ollama-reranker';
    fallback;
    modelId;
    ollamaUrl;
    topK;
    timeoutMs;
    fetchFn;
    hasLoggedFailure = false;
    constructor(options = {}) {
        this.fallback = options.fallback ?? new HashModelProvider(options.embeddingDimensions ?? 1536);
        this.modelId = options.modelId ?? process.env.TUBEROSA_OLLAMA_RERANK_MODEL ?? DEFAULT_MODEL_ID;
        this.ollamaUrl = trimTrailingSlash(options.ollamaUrl ?? process.env.TUBEROSA_OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
        this.topK = options.topK ?? Number(process.env.TUBEROSA_RERANKER_TOPK ?? DEFAULT_TOP_K);
        this.timeoutMs = options.timeoutMs ?? Number(process.env.TUBEROSA_OLLAMA_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS);
        this.fetchFn = options.fetchFn ?? globalThis.fetch;
    }
    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 windowSize = Math.min(this.topK, input.candidates.length);
        const window = input.candidates.slice(0, windowSize);
        const tail = input.candidates.slice(windowSize);
        const documents = window.map((candidate) => truncate(candidate.contextualContent || candidate.content || candidate.summary || candidate.title, 1024));
        let scores;
        try {
            scores = await this.callOllama(input.prompt, documents);
        }
        catch (error) {
            this.logFailure(`ollama rerank failed: ${error instanceof Error ? error.message : String(error)}`);
            return this.fallback.rerank(input);
        }
        const reranked = window.map((candidate, index) => {
            const ollamaScore = clamp(scores[index] ?? 0, 0, 1);
            const trustScore = clamp(candidate.trustLevel / 100, 0, 1);
            // 0.70 to the Ollama cross-encoder, 0.22 to the fused retrieval score, 0.08 to trust.
            const blended = clamp(ollamaScore * 0.7 + candidate.fusedScore * 0.22 + trustScore * 0.08, 0, 1);
            return {
                ...candidate,
                rerankScore: blended,
                finalScore: blended,
                matchReasons: [...candidate.matchReasons, `ollama-rerank:${this.modelId}:${ollamaScore.toFixed(3)}`],
                metadata: {
                    ...(candidate.metadata ?? {}),
                    ollamaRerank: {
                        model: this.modelId,
                        score: ollamaScore,
                    },
                },
            };
        });
        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 callOllama(query, documents) {
        const response = await this.fetchFn(`${this.ollamaUrl}/api/rerank`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ model: this.modelId, query, documents }),
            signal: AbortSignal.timeout(this.timeoutMs),
        });
        if (!response.ok) {
            throw new Error(`Ollama returned HTTP ${response.status}`);
        }
        const body = (await response.json());
        const results = Array.isArray(body.results) ? body.results : [];
        const scores = new Array(documents.length).fill(0);
        for (const entry of results) {
            if (typeof entry?.index === 'number' &&
                entry.index >= 0 &&
                entry.index < documents.length &&
                typeof entry.relevance_score === 'number' &&
                Number.isFinite(entry.relevance_score)) {
                scores[entry.index] = clamp(entry.relevance_score, 0, 1);
            }
        }
        return scores;
    }
    logFailure(reason) {
        if (this.hasLoggedFailure)
            return;
        this.hasLoggedFailure = true;
        if ((process.env.NODE_ENV ?? '') === 'test' || process.env.TUBEROSA_SILENT_OLLAMA_PROVIDER === 'true')
            return;
        process.stderr.write(`[tuberosa] ollama reranker unavailable; falling back to hash rerank — ${reason}\n`);
    }
}
function trimTrailingSlash(url) {
    return url.endsWith('/') ? url.slice(0, -1) : url;
}
