/**
 * Regenerate models.json: the account's entitled models, with metadata from models.dev.
 *
 * Run: npx tsx regen.ts
 *
 * Capabilities are frozen into the file on purpose. Looking them up at startup made
 * image support flip between runs, because ids missing from the `alibaba-token-plan`
 * entry were filled from whichever other provider models.dev happened to list first.
 */

import { writeFileSync } from "node:fs";
import { accountModelIds, type BakedCatalog, type BakedModel, baseUrlFromEnv, CATALOG, resolveKey } from "./index.js";

const CATALOG_URL = "https://models.dev/api.json";
const CATALOG_PROVIDER = "alibaba-token-plan";

interface DevModel {
	name?: string;
	reasoning?: boolean;
	tool_call?: boolean;
	attachment?: boolean;
	modalities?: { input?: string[]; output?: string[] };
	limit?: { context?: number; output?: number };
}

const isChatModel = (m: DevModel): boolean => m.tool_call === true && (m.modalities?.output ?? []).includes("text");

const toBaked = (id: string, m: DevModel): BakedModel => ({
	id,
	name: m.name ?? id,
	reasoning: m.reasoning === true,
	input: (m.modalities?.input ?? []).includes("image") ? ["text", "image"] : ["text"],
	contextWindow: m.limit?.context ?? 128000,
	maxTokens: m.limit?.output ?? 8192,
});

async function main(): Promise<void> {
	const baseUrl = baseUrlFromEnv();
	const key = resolveKey();
	if (!key) throw new Error("no API key: run /login alibaba or set ALIBABA_API_KEY");

	const [entitled, payload] = await Promise.all([
		accountModelIds(baseUrl, key),
		fetch(CATALOG_URL).then((r) => r.json() as Promise<Record<string, { models?: Record<string, DevModel> }>>),
	]);

	// other alibaba catalogs describe the same models, an unrelated one just shares the id:
	// `auto` is a route here and model-oracle-ai's model there
	const own = payload[CATALOG_PROVIDER]?.models ?? {};
	const elsewhere = new Map<string, DevModel[]>();
	for (const [provider, entry] of Object.entries(payload)) {
		if (provider === CATALOG_PROVIDER || !provider.startsWith("alibaba")) continue;
		for (const [id, model] of Object.entries(entry.models ?? {})) {
			elsewhere.set(id, [...(elsewhere.get(id) ?? []), model]);
		}
	}
	const best = (id: string): DevModel | undefined =>
		own[id] ??
		(elsewhere.get(id) ?? [])
			.filter(isChatModel)
			.sort((a, b) => (b.limit?.context ?? 0) - (a.limit?.context ?? 0))[0];

	const models: BakedModel[] = [];
	const skipped: string[] = [];
	for (const id of entitled) {
		const model = best(id);
		if (!model || !isChatModel(model)) {
			skipped.push(id);
			continue;
		}
		models.push(toBaked(id, model));
	}
	models.sort((a, b) => a.id.localeCompare(b.id));
	skipped.sort();

	const catalog: BakedCatalog = { generatedAt: new Date().toISOString(), baseUrl, models, skipped };
	writeFileSync(CATALOG, `${JSON.stringify(catalog, null, "\t")}\n`);
	for (const m of models) {
		console.log(`${m.id}\t${m.contextWindow}\t${m.maxTokens}\t${m.reasoning ? "thinking" : "-"}\t${m.input.join("+")}`);
	}
	if (skipped.length > 0) console.log(`skipped (not a chat model or unknown to models.dev): ${skipped.join(", ")}`);
}

await main();
