import { JinaModels } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { isNeuroLink } from "../neurolink.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../types/index.js";
import { logger } from "../utils/logger.js";
import { createJinaConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
const JINA_DEFAULT_BASE_URL = "https://api.jina.ai/v1";
const REQUEST_TIMEOUT_MS = 60_000;
const getJinaApiKey = () => validateApiKey(createJinaConfig());
const getDefaultJinaModel = () => getProviderModel("JINA_MODEL", JinaModels.JINA_EMBEDDINGS_V3);
/**
 * Jina AI Provider — embeddings + reranking.
 *
 * Native API at api.jina.ai/v1. Chat / streaming / tool calling are not
 * supported. Use `embed()` / `embedMany()` for embeddings, or call
 * `rerank()` directly for retrieval reranking (Jina's strength).
 *
 * @see https://jina.ai/embeddings/
 */
export class JinaProvider extends BaseProvider {
    apiKey;
    baseURL;
    proxyFetch;
    constructor(modelName, sdk, _region, credentials) {
        const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined;
        super(modelName, "jina", validatedNeurolink);
        const overrideKey = credentials?.apiKey?.trim();
        this.apiKey =
            overrideKey && overrideKey.length > 0 ? overrideKey : getJinaApiKey();
        this.baseURL =
            credentials?.baseURL ??
                process.env.JINA_BASE_URL ??
                JINA_DEFAULT_BASE_URL;
        this.proxyFetch = createProxyFetch();
        logger.debug("Jina Provider initialized (embeddings + reranking)", {
            modelName: this.modelName,
            baseURL: this.baseURL,
        });
    }
    getProviderName() {
        return this.providerName;
    }
    getDefaultModel() {
        return getDefaultJinaModel();
    }
    supportsTools() {
        return false;
    }
    getDefaultEmbeddingModel() {
        return getDefaultJinaModel();
    }
    getAISDKModel() {
        throw new Error("Jina AI is an embeddings + reranking provider; chat completions are not available. Use `embed()` / `embedMany()` / `rerank()`.");
    }
    async executeStream(_options, _analysisSchema) {
        throw new Error("Jina AI is an embeddings + reranking provider; streaming chat is not available.");
    }
    formatProviderError(error) {
        const message = error instanceof Error
            ? error.message
            : typeof error === "string"
                ? error
                : "Unknown error";
        if (message.includes("401") ||
            message.toLowerCase().includes("unauthorized")) {
            return new AuthenticationError("Invalid Jina AI API key. Get one at https://jina.ai/?sui=apikey", "jina");
        }
        if (message.includes("429") ||
            message.toLowerCase().includes("rate limit")) {
            return new RateLimitError("Jina AI rate limit exceeded. Back off and retry.", "jina");
        }
        if (message.includes("404") ||
            message.toLowerCase().includes("model_not_found")) {
            return new InvalidModelError(`Jina AI model '${this.modelName}' not found. See https://jina.ai/embeddings/`, "jina");
        }
        return new ProviderError(`Jina AI error: ${message}`, "jina");
    }
    async embed(text, modelName) {
        const vectors = await this.callEmbeddings([text], modelName);
        if (!vectors[0]) {
            throw new Error("Jina AI returned no embedding for the provided text");
        }
        return vectors[0];
    }
    async embedMany(texts, modelName) {
        if (texts.length === 0) {
            return [];
        }
        return this.callEmbeddings(texts, modelName);
    }
    /**
     * Rerank a list of documents against a query.
     *
     * Returns the documents sorted by relevance (highest first), with
     * score and original index preserved so callers can map back.
     *
     * Note: not exposed on `BaseProvider` — accessed by casting to
     * `JinaProvider` or via the dedicated rerank route on the public API
     * (`POST /api/agent/rerank` in the server module, when added).
     *
     * Per-call credentials can be supplied via `options.credentials?.jina`,
     * overriding the instance-level credentials for this request only.
     */
    async rerank(query, documents, options = {}) {
        if (documents.length === 0) {
            return [];
        }
        const model = options.model ?? JinaModels.JINA_RERANKER_V2_BASE_MULTILINGUAL;
        // Resolve per-call credentials first, then fall back to instance-level.
        const perCallCreds = options.credentials;
        const effectiveApiKey = perCallCreds?.apiKey?.trim() || this.apiKey;
        const effectiveBaseURL = perCallCreds?.baseURL || this.baseURL;
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
        let response;
        try {
            response = await this.proxyFetch(`${effectiveBaseURL}/rerank`, {
                method: "POST",
                headers: {
                    Authorization: `Bearer ${effectiveApiKey}`,
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    model,
                    query,
                    documents,
                    top_n: options.topN ?? documents.length,
                }),
                signal: controller.signal,
            });
        }
        catch (err) {
            if (err instanceof Error && err.name === "AbortError") {
                throw this.formatProviderError(new Error(`Jina rerank request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
            }
            throw this.formatProviderError(err);
        }
        finally {
            clearTimeout(timeoutId);
        }
        if (!response.ok) {
            const text = await response.text();
            throw this.formatProviderError(new Error(`Jina rerank failed: ${response.status} — ${text}`));
        }
        const data = (await response.json());
        return (data.results ?? []).map((r) => ({
            index: r.index,
            score: r.relevance_score,
            document: documents[r.index] ?? r.document?.text ?? "",
        }));
    }
    async callEmbeddings(inputs, modelName, credentials) {
        const model = modelName ?? this.modelName;
        // Resolve per-call credentials first, then fall back to instance-level.
        const effectiveApiKey = credentials?.apiKey?.trim() || this.apiKey;
        const effectiveBaseURL = credentials?.baseURL || this.baseURL;
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
        let response;
        try {
            response = await this.proxyFetch(`${effectiveBaseURL}/embeddings`, {
                method: "POST",
                headers: {
                    Authorization: `Bearer ${effectiveApiKey}`,
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    input: inputs,
                    model,
                }),
                signal: controller.signal,
            });
        }
        catch (err) {
            if (err instanceof Error && err.name === "AbortError") {
                throw this.formatProviderError(new Error(`Jina embeddings request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
            }
            throw this.formatProviderError(err);
        }
        finally {
            clearTimeout(timeoutId);
        }
        if (!response.ok) {
            const text = await response.text();
            throw this.formatProviderError(new Error(`Jina embeddings failed: ${response.status} — ${text}`));
        }
        const data = (await response.json());
        if (!data.data || data.data.length === 0) {
            throw new Error("Jina embeddings response missing data");
        }
        // Validate that the response covers all requested inputs.
        if (data.data.length !== inputs.length) {
            throw new Error(`Jina embeddings response count mismatch: expected ${inputs.length}, got ${data.data.length}`);
        }
        // Sort by index and verify sequential coverage (0 … n-1).
        const sorted = data.data.slice().sort((a, b) => a.index - b.index);
        for (let i = 0; i < sorted.length; i++) {
            if (sorted[i].index !== i) {
                throw new Error(`Jina embeddings response has unexpected index ordering: position ${i} has index ${sorted[i].index}`);
            }
        }
        return sorted.map((d) => d.embedding);
    }
    async validateConfiguration() {
        return typeof this.apiKey === "string" && this.apiKey.trim().length > 0;
    }
    getConfiguration() {
        return {
            provider: this.providerName,
            model: this.modelName,
            defaultModel: getDefaultJinaModel(),
            baseURL: this.baseURL,
        };
    }
}
export default JinaProvider;
