/**
 * Charm Hyper (https://hyper.charm.land) provider for Prime Agent.
 *
 * Hyper is a router over many coding models with one key or one subscription login.
 * All models are exposed through the OpenAI Chat Completions endpoint
 * (`POST /v1/chat/completions`), which is the only one of Hyper's three chat
 * endpoints that reports prompt-cache usage and accepts an explicit "stop thinking".
 *
 * Auth: `/login hyper` (device-code subscription login or a pasted key, stored in Prime
 * Agent's auth.json), or HYPER_API_KEY for throwaway shells. There is no key file.
 *
 * Models come from `GET /v1/models` and models.dev via the committed `catalog.json` and the last
 * successful refresh in ~/.prime/agent/hyper-catalog.json. The catalog that is newest is
 * registered immediately, so startup never waits on the network, and a background refresh
 * re-registers when Hyper's list changed.
 */

import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import {
	type Catalog,
	type HyperProviderModel,
	type ModelFacts,
	fetchFacts,
	readCatalog,
	sameModels,
	toCatalog,
	toProviderModels,
	writeCatalog,
} from "./catalog.js";
import { API_BASE_URL, API_KEY_CONFIG, DISPLAY_NAME, PROVIDER, cachePath, fetchBalance, isOffline } from "./hyper.js";
import { loginHyper, refreshHyperToken } from "./oauth.js";

/** The model list committed to this repository: first-run seed and the probed thinking controls. */
const BAKED_CATALOG = join(dirname(fileURLToPath(import.meta.url)), "catalog.json");

const reported = new Set<string>();

const report = (message: string): void => {
	if (reported.has(message)) return;
	reported.add(message);
	process.stderr.write(`hyper: ${message}\n`);
};

const newest = (baked: Catalog | undefined, cached: Catalog | undefined): Catalog | undefined =>
	!baked ? cached : !cached ? baked : cached.generatedAt > baked.generatedAt ? cached : baked;

export default async function hyper(pi: ExtensionAPI): Promise<void> {
	const baked = readCatalog(BAKED_CATALOG);
	const cached = readCatalog();
	// Waiting on the network here would put a request in every session worker's startup
	// path, so it only happens when no catalog file survived at all.
	const seed =
		newest(baked, cached) ??
		(isOffline()
			? undefined
			: await fetchFacts()
					.then(toCatalog)
					.catch((error: unknown) => {
						report(`no models: ${error instanceof Error ? error.message : String(error)}`);
						return undefined;
					}));

	let registered: HyperProviderModel[] = [];

	const register = (models: readonly ModelFacts[]): HyperProviderModel[] => {
		const configs = toProviderModels(models);
		pi.registerProvider(PROVIDER, {
			name: DISPLAY_NAME,
			baseUrl: API_BASE_URL,
			apiKey: API_KEY_CONFIG,
			api: "openai-completions",
			models: configs,
			oauth: {
				name: DISPLAY_NAME,
				login: loginHyper,
				refreshToken: refreshHyperToken,
				getApiKey: (credentials) => credentials.access,
			},
		});
		return configs;
	};

	registered = register(seed?.models ?? []);

	const refresh = async (): Promise<void> => {
		const fresh = await fetchFacts();
		const configs = toProviderModels(fresh);
		const changed = !sameModels(registered, configs);
		if (changed || cached === undefined) writeCatalog(cachePath(), toCatalog(fresh));
		if (changed) registered = register(fresh);
	};

	pi.registerCommand("hyper", {
		description: "Show the Charm Hyper account balance and model count",
		handler: async (_args, ctx) => {
			const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER);
			if (!apiKey) {
				ctx.ui.notify("No Charm Hyper key. Run /login hyper, or set HYPER_API_KEY.", "warning");
				return;
			}
			try {
				const balance = await fetchBalance(apiKey);
				ctx.ui.notify(
					`Charm Hyper: ${balance === undefined ? "balance unavailable" : `${balance} hypercredits`}, ${registered.length} models`,
					"info",
				);
			} catch (error) {
				ctx.ui.notify(`Charm Hyper: ${error instanceof Error ? error.message : String(error)}`, "warning");
			}
		},
	});

	// The refresh is what discovers models when no catalog file survived; never hold up
	// extension loading for it, since it also runs in every session worker.
	if (isOffline()) return;
	void refresh().catch((error: unknown) => {
		report(`keeping the previous model list: ${error instanceof Error ? error.message : String(error)}`);
	});
}
