/**
 * model-catalog-cache.js — live model-list discovery with disk cache.
 *
 * Fetches model lists from providers that publish a /v1/models (or equivalent)
 * endpoint. Results are cached to .sf/runtime/model-catalog/<providerId>.json
 * with a 6-hour TTL so the model picker and preference validation see fresh
 * model IDs without blocking the event loop.
 *
 * Provider configuration (base URL, auth format, model filter patterns) comes
 * from provider-catalog-config.js — not hardcoded here.
 */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { sfRuntimeRoot } from "./paths.js";
import {
	DISCOVERABLE_PROVIDER_IDS,
	getProviderCatalogConfig,
	getProviderModelExcludePatterns,
} from "./provider-catalog-config.js";

const CATALOG_TTL_MS = 6 * 60 * 60 * 1000;

function cacheDirPath(basePath) {
	return join(sfRuntimeRoot(basePath), "model-catalog");
}

function cacheFilePath(basePath, providerId) {
	return join(cacheDirPath(basePath), `${providerId}.json`);
}

/**
 * Read cached model IDs for a provider. Returns null if cache is missing or stale.
 */
export function readCachedModelIds(basePath, providerId) {
	try {
		const path = cacheFilePath(basePath, providerId);
		if (!existsSync(path)) return null;
		const entry = JSON.parse(readFileSync(path, "utf-8"));
		if (!entry?.fetchedAt || !Array.isArray(entry.modelIds)) return null;
		if (Date.now() - new Date(entry.fetchedAt).getTime() > CATALOG_TTL_MS)
			return null;
		return entry.modelIds;
	} catch {
		return null;
	}
}

function writeCacheEntry(basePath, providerId, modelIds) {
	try {
		mkdirSync(cacheDirPath(basePath), { recursive: true });
		writeFileSync(
			cacheFilePath(basePath, providerId),
			JSON.stringify({ fetchedAt: new Date().toISOString(), modelIds }),
			"utf-8",
		);
	} catch {
		// Best-effort — never fail the caller.
	}
}

function buildAuthHeaders(cfg, apiKey) {
	switch (cfg.auth.type) {
		case "bearer":
			return { Authorization: `Bearer ${apiKey}` };
		case "x-api-key":
			return { [cfg.auth.header ?? "x-api-key"]: apiKey };
		case "query-param":
			return {};
		default:
			return {};
	}
}

function buildUrl(cfg, apiKey) {
	const base = cfg.baseUrl.replace(/\/$/, "");
	const path = cfg.modelsPath;
	if (cfg.auth.type === "query-param" && cfg.auth.param) {
		return `${base}${path}?${cfg.auth.param}=${encodeURIComponent(apiKey)}`;
	}
	return `${base}${path}`;
}

function parseModelIds(cfg, json) {
	let items;
	if (cfg.type === "google") {
		// Google: { models: [{name: "models/gemini-..."}] }
		items = Array.isArray(json?.models) ? json.models : null;
		if (!items) return null;
		return items
			.map((m) => (m.name ?? "").replace(/^models\//, ""))
			.filter(Boolean);
	}
	// OpenAI-compatible: { data: [{id}] } or { models: [{id}] }
	items = Array.isArray(json?.data)
		? json.data
		: Array.isArray(json?.models)
			? json.models
			: null;
	if (!items) return null;
	return items.map((m) => m.id ?? m.name).filter(Boolean);
}

function applyModelFilter(providerId, modelIds) {
	const patterns = getProviderModelExcludePatterns(providerId);
	if (patterns.length === 0) return modelIds;
	return modelIds.filter((id) => !patterns.some((re) => re.test(id)));
}

/**
 * Fetch the live model list for one provider and update the disk cache.
 * Returns the filtered model ID array on success, null on any error.
 */
export async function refreshProviderCatalog(basePath, providerId, apiKey) {
	const cfg = getProviderCatalogConfig(providerId);
	if (!cfg?.modelsPath || !apiKey) return null;
	try {
		const url = buildUrl(cfg, apiKey);
		const res = await fetch(url, {
			headers: buildAuthHeaders(cfg, apiKey),
			signal: AbortSignal.timeout(10_000),
		});
		if (!res.ok) return null;
		const json = await res.json();
		const raw = parseModelIds(cfg, json);
		if (!raw) return null;
		const modelIds = applyModelFilter(providerId, raw);
		writeCacheEntry(basePath, providerId, modelIds);
		return modelIds;
	} catch {
		return null;
	}
}

/**
 * Fire-and-forget background refresh for all discoverable providers that have
 * a key in auth.json and a stale or absent cache. Safe to call at session
 * start — never throws, never blocks.
 */
export function scheduleModelCatalogRefresh(basePath, auth) {
	setImmediate(async () => {
		for (const providerId of DISCOVERABLE_PROVIDER_IDS) {
			try {
				const creds = auth.getCredentialsForProvider(providerId);
				const apiKey = creds.find((c) => c.type === "api_key" && c.key)?.key;
				if (!apiKey) continue;
				if (readCachedModelIds(basePath, providerId) !== null) continue;
				await refreshProviderCatalog(basePath, providerId, apiKey);
			} catch {
				// Per-provider failures are silently swallowed.
			}
		}
	});
}

/**
 * Return the union of SDK-static model IDs and live-discovered model IDs for a
 * provider. Hot path — reads only from disk cache, no network call.
 *
 * Consumer: model picker, preference validation for unknown model IDs.
 */
export function getKnownModelIds(basePath, providerId, sdkModelIds = []) {
	const cached = readCachedModelIds(basePath, providerId) ?? [];
	return [...new Set([...sdkModelIds, ...cached])];
}
