/**
 * Fetch the admin-managed list of allowed base models from the backend.
 *
 * Used by `AgentBridge` at startup to register synthetic providers
 * (`spectral-proxy-anthropic` / `spectral-proxy-openai`) that route every
 * inference call through the backend's `/v1/messages` and
 * `/v1/chat/completions` endpoints. The backend authenticates the call
 * with the machine JWT (Bearer) and forwards to the upstream provider
 * using its own (centralized) API keys.
 *
 * Why GraphQL (not REST):
 *  - The backend already exposes the whitelist via `availableAgentModels`
 *    in its public GraphQL schema (`schema.graphql`); there is no
 *    equivalent REST endpoint and we don't want to add one just for the
 *    CLI.
 *  - The query is parameterless and authenticated via the same Bearer
 *    machine JWT used by `/v1/*`, so a single `fetch` call is enough —
 *    no Zeus client setup required.
 *
 * Caching:
 *  - In-memory TTL cache (default 5 min), keyed by `${backendUrl}|${jwt}`.
 *    Pi sessions are short-lived but a single `spectral serve` process
 *    creates many of them, so we don't want to hammer the GraphQL
 *    endpoint on every reconnect.
 */
const cache = new Map();
/** Test-only helper: drops every cached entry. */
export function clearAllowedModelsCache() {
    cache.clear();
}
const QUERY = `query AvailableAgentModels { availableAgentModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M contextWindow supportsImages supportsReasoning isDefault isVisionDefault } }`;
/**
 * Fetch the whitelist of allowed base models. Throws on any failure with a
 * message tailored for an operator running `spectral serve` — the caller
 * (AgentBridge.start) lets the throw propagate so the WS subscriber sees a
 * clear error event instead of a silent fall-through to "no models".
 */
export async function fetchAllowedModels(opts) {
    const ttlMs = opts.cacheTtlMs ?? 5 * 60 * 1000;
    const key = `${opts.backendUrl}|${opts.machineJwt}`;
    if (!opts.bypassCache) {
        const hit = cache.get(key);
        if (hit && Date.now() - hit.fetchedAt < hit.ttlMs) {
            return hit.models;
        }
    }
    const fetchImpl = opts.fetchImpl ?? fetch;
    const url = `${opts.backendUrl.replace(/\/$/, "")}/graphql`;
    let res;
    try {
        res = await fetchImpl(url, {
            method: "POST",
            headers: {
                Authorization: `Bearer ${opts.machineJwt}`,
                "Content-Type": "application/json",
            },
            body: JSON.stringify({ query: QUERY }),
        });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        throw new Error(`Failed to fetch allowed models from backend (${url}): ${msg}. ` +
            `Check SPECTRAL_BACKEND_URL and machine JWT.`);
    }
    if (!res.ok) {
        let detail = "";
        try {
            detail = await res.text();
        }
        catch {
            // ignore
        }
        throw new Error(`Failed to fetch allowed models from backend (HTTP ${res.status} ${res.statusText})` +
            (detail ? `: ${detail.slice(0, 500)}` : "") +
            ". Check SPECTRAL_BACKEND_URL and machine JWT.");
    }
    let payload;
    try {
        payload = (await res.json());
    }
    catch (err) {
        throw new Error(`Failed to fetch allowed models: backend returned non-JSON (${err instanceof Error ? err.message : String(err)})`);
    }
    if (payload.errors && payload.errors.length > 0) {
        const msg = payload.errors
            .map((e) => (typeof e?.message === "string" ? e.message : "unknown"))
            .join("; ");
        throw new Error(`Failed to fetch allowed models: GraphQL errors: ${msg}`);
    }
    const rows = payload.data?.availableAgentModels;
    if (!Array.isArray(rows)) {
        throw new Error(`Failed to fetch allowed models: response missing availableAgentModels array`);
    }
    const models = [];
    for (const row of rows) {
        const name = typeof row?.name === "string" ? row.name : null;
        const provider = typeof row?.provider === "string" ? row.provider : null;
        if (!name || !provider)
            continue; // skip malformed rows defensively
        const asOptionalNumber = (v) => typeof v === "number" ? v : v == null ? null : undefined;
        const contextWindow = typeof row?.contextWindow === "number" && row.contextWindow > 0
            ? row.contextWindow
            : null;
        const supportsImages = typeof row?.supportsImages === "boolean"
            ? row.supportsImages
            : null;
        const supportsReasoning = typeof row?.supportsReasoning === "boolean"
            ? row.supportsReasoning
            : null;
        const isDefault = typeof row?.isDefault === "boolean"
            ? row.isDefault
            : null;
        const isVisionDefault = typeof row?.isVisionDefault === "boolean"
            ? row.isVisionDefault
            : null;
        const model = {
            modelId: name,
            displayName: name,
            provider,
            creditInputPer1M: asOptionalNumber(row?.creditInputPer1M),
            creditOutputPer1M: asOptionalNumber(row?.creditOutputPer1M),
            creditCachedInputPer1M: asOptionalNumber(row?.creditCachedInputPer1M),
            creditCacheReadPer1M: asOptionalNumber(row?.creditCacheReadPer1M),
            creditCacheWritePer1M: asOptionalNumber(row?.creditCacheWritePer1M),
            contextWindow,
            supportsImages,
            supportsReasoning,
            isDefault,
            isVisionDefault,
        };
        if (typeof row?.userModelId === "string") {
            model.userModelId = row.userModelId;
        }
        models.push(model);
    }
    cache.set(key, { fetchedAt: Date.now(), ttlMs, models });
    return models;
}
