/**
 * factory.ts - EmbeddingProvider factory with config precedence.
 *
 * Resolution order (first match wins):
 *   1. Explicit `kind` argument or `--provider` CLI flag → forces a kind
 *   2. `QMD_EMBED_ENDPOINT` env var present and non-empty → "openai"
 *   3. Config file (`~/.config/qmd/config.json`) commercial endpoint
 *   4. Otherwise → typed HOLD (there is no local or self-hosted fallback)
 */
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { OpenAIEmbeddingsProvider, } from "./openai.js";
import { commercialApiHold } from "../model-policy.js";
export function defaultConfigPath() {
    const xdg = process.env.XDG_CONFIG_HOME;
    const base = xdg ? xdg : join(homedir(), ".config");
    return join(base, "qmd", "config.json");
}
/**
 * Load `~/.config/qmd/config.json` if present. Returns an empty object on
 * any read/parse error; provider construction then fails closed without an endpoint.
 */
export function loadConfigFile(path = defaultConfigPath()) {
    if (!existsSync(path))
        return {};
    try {
        const raw = readFileSync(path, "utf-8");
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === "object")
            return parsed;
    }
    catch {
        // Ignore — invalid JSON, missing read perm, etc.
    }
    return {};
}
/**
 * Resolve the provider kind without instantiating anything. Useful for
 * logging and tests.
 */
export function resolveProviderKind(opts = {}) {
    const env = opts.env ?? process.env;
    const cfg = loadConfigFile(opts.configPath);
    // 1. Explicit kind argument
    if (opts.kind === "local") {
        throw commercialApiHold('provider kind "local" is disabled; configure an approved commercial API');
    }
    if (opts.kind === "openai")
        return opts.kind;
    // 2a. Explicit env override
    const envKind = env.QMD_EMBED_PROVIDER?.trim().toLowerCase();
    if (envKind === "local") {
        throw commercialApiHold("QMD_EMBED_PROVIDER=local is forbidden");
    }
    if (envKind === "openai")
        return envKind;
    // 2b. Endpoint env present → openai
    if (env.QMD_EMBED_ENDPOINT && env.QMD_EMBED_ENDPOINT.trim() !== "") {
        return "openai";
    }
    // 3. Config file
    if (cfg.embedProvider?.kind === "local") {
        throw commercialApiHold("embedProvider.kind=local is forbidden");
    }
    if (cfg.embedProvider?.kind === "openai") {
        return "openai";
    }
    if (cfg.embedProvider?.endpoint && cfg.embedProvider.endpoint.trim() !== "") {
        return "openai";
    }
    // Commercial-only default. Missing endpoint is handled as typed HOLD by the factory.
    return "openai";
}
/**
 * Factory entry point — returns the appropriate `EmbeddingProvider`.
 * Throws if `openai` kind is requested but no endpoint is configured.
 */
export function createEmbeddingProvider(opts = {}) {
    const env = opts.env ?? process.env;
    const cfg = loadConfigFile(opts.configPath);
    const kind = resolveProviderKind(opts);
    if (kind === "local") {
        throw commercialApiHold('provider kind "local" is disabled');
    }
    // OpenAI
    const endpoint = opts.openai?.endpoint ??
        env.QMD_EMBED_ENDPOINT ??
        cfg.embedProvider?.endpoint;
    if (!endpoint || endpoint.trim() === "") {
        throw commercialApiHold('commercial provider requires an endpoint. ' +
            "Set QMD_EMBED_ENDPOINT env var, or `embedProvider.endpoint` in " +
            "~/.config/qmd/config.json, or pass `openai.endpoint`.");
    }
    assertCommercialEndpoint(endpoint);
    const apiKey = opts.openai?.apiKey ??
        env.QMD_EMBED_API_KEY ??
        cfg.embedProvider?.apiKey;
    const modelId = opts.openai?.modelId ??
        env.QMD_EMBED_MODEL_ID ??
        cfg.embedProvider?.modelId ??
        "embeddinggemma";
    const upstreamModel = opts.openai?.upstreamModel ??
        env.QMD_EMBED_UPSTREAM_MODEL ??
        cfg.embedProvider?.upstreamModel;
    const batchSizeRaw = opts.openai?.batchSize ??
        parsePositiveInt(env.QMD_EMBED_BATCH_SIZE) ??
        cfg.embedProvider?.batchSize;
    const timeoutMsRaw = opts.openai?.timeoutMs ??
        parsePositiveInt(env.QMD_EMBED_TIMEOUT_MS) ??
        cfg.embedProvider?.timeoutMs;
    const concurrencyRaw = opts.openai?.concurrency ??
        parsePositiveInt(env.QMD_EMBED_CONCURRENCY) ??
        cfg.embedProvider?.concurrency;
    const openaiProvider = new OpenAIEmbeddingsProvider({
        endpoint,
        apiKey,
        modelId,
        upstreamModel,
        batchSize: batchSizeRaw,
        concurrency: concurrencyRaw,
        timeoutMs: timeoutMsRaw,
        fetchImpl: opts.openai?.fetchImpl,
        retryBackoffsMs: opts.openai?.retryBackoffsMs,
        sleep: opts.openai?.sleep,
        now: opts.openai?.now,
    });
    // Historical fallback inputs are rejected instead of silently weakening policy.
    const autoFallback = resolveAutoFallback(opts, env, cfg);
    if (autoFallback) {
        throw commercialApiHold("local auto-fallback is forbidden; commercial API failures must remain HOLD");
    }
    return openaiProvider;
}
export function assertCommercialEndpoint(endpoint) {
    let parsed;
    try {
        parsed = new URL(endpoint);
    }
    catch {
        throw commercialApiHold("commercial provider endpoint is malformed");
    }
    const host = parsed.hostname.toLowerCase();
    const privateIpv4 = /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/;
    const localHost = host === "localhost" || host === "models" || host.endsWith(".local");
    const localIpv6 = host === "::1" || host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd");
    if (parsed.protocol !== "https:" || localHost || privateIpv4.test(host) || localIpv6) {
        throw commercialApiHold(`endpoint ${parsed.protocol}//${host} is local, private, or non-TLS; use an approved commercial HTTPS API`);
    }
}
function resolveAutoFallback(opts, env, cfg) {
    if (typeof opts.autoFallback === "boolean")
        return opts.autoFallback;
    const envVal = env.QMD_EMBED_AUTO_FALLBACK?.trim().toLowerCase();
    if (envVal === "1" || envVal === "true" || envVal === "yes")
        return true;
    if (envVal === "0" || envVal === "false" || envVal === "no")
        return false;
    if (typeof cfg.embedProvider?.autoFallback === "boolean") {
        return cfg.embedProvider.autoFallback;
    }
    return false;
}
// ─────────────────────────── Helpers ────────────────────────────────────────
function parsePositiveInt(v) {
    if (!v)
        return undefined;
    const parsed = Number.parseInt(v, 10);
    if (!Number.isFinite(parsed) || parsed <= 0)
        return undefined;
    return parsed;
}
