// sf — Ollama model discovery and capability detection
/**
 * Discovers locally available Ollama models and enriches them with
 * capability metadata (context window, vision, reasoning) from the
 * known model table and /api/show responses.
 *
 * Returns models in the format expected by pi.registerProvider().
 */
import {
	estimateContextFromParams,
	formatModelSize,
	getModelCapabilities,
	humanizeModelName,
} from "./model-capabilities.js";
import { listModels, showModel } from "./ollama-client.js";

/**
 * Extract context window from /api/show model_info.
 * Keys follow the pattern "{architecture}.context_length" (e.g. "llama.context_length").
 */
function extractContextFromModelInfo(modelInfo) {
	for (const [key, value] of Object.entries(modelInfo)) {
		if (
			key.endsWith(".context_length") &&
			typeof value === "number" &&
			value > 0
		) {
			return value;
		}
	}
	return undefined;
}
const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
async function enrichModel(info, deps) {
	const caps = getModelCapabilities(info.name);
	const parameterSize = info.details?.parameter_size ?? "";
	// /api/tags doesn't include context length; /api/show does via "{arch}.context_length" in model_info.
	let showContextWindow;
	if (caps.contextWindow === undefined) {
		try {
			const showData = await deps.showModel(info.name);
			showContextWindow = extractContextFromModelInfo(showData.model_info);
		} catch (err) {
			// non-fatal: fall through to estimate
			if (process.env.SF_DEBUG)
				console.warn(
					`[ollama] /api/show failed for ${info.name}:`,
					err instanceof Error ? err.message : String(err),
				);
		}
	}
	// Determine context window: known table > /api/show > estimate from param size > default
	const contextWindow =
		caps.contextWindow ??
		showContextWindow ??
		(parameterSize ? estimateContextFromParams(parameterSize) : 8192);
	// Determine max tokens: known table > fraction of context > default
	const maxTokens =
		caps.maxTokens ?? Math.min(Math.floor(contextWindow / 4), 16384);
	// Detect vision from families or known table
	const hasVision =
		caps.input?.includes("image") ??
		info.details?.families?.some((f) => f === "clip" || f === "mllama") ??
		false;
	// Detect reasoning from known table
	const reasoning = caps.reasoning ?? false;
	return {
		id: info.name,
		name: humanizeModelName(info.name),
		reasoning,
		input: hasVision ? ["text", "image"] : ["text"],
		cost: ZERO_COST,
		contextWindow,
		maxTokens,
		sizeBytes: info.size,
		parameterSize,
		ollamaOptions: caps.ollamaOptions,
	};
}
/**
 * Discover all locally available Ollama models with enriched capabilities.
 */
export async function discoverModels(deps = { listModels, showModel }) {
	const tags = await deps.listModels();
	if (!tags.models || tags.models.length === 0) return [];
	return Promise.all(tags.models.map((m) => enrichModel(m, deps)));
}
/**
 * Format a discovered model for display in model list.
 */
export function formatModelForDisplay(model) {
	const parts = [model.id];
	if (model.sizeBytes > 0) {
		parts.push(`(${formatModelSize(model.sizeBytes)})`);
	}
	const flags = [];
	if (model.reasoning) flags.push("reasoning");
	if (model.input.includes("image")) flags.push("vision");
	if (flags.length > 0) {
		parts.push(`[${flags.join(", ")}]`);
	}
	return parts.join(" ");
}
