/**
 * Antigravity model discovery via the Cloud Code Assist fetchAvailableModels
 * endpoint. Copied from oh-my-pi's catalog discovery/antigravity module.
 */
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
import { resolveAntigravityModelCost } from "./models.js";
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ensureAntigravityVersion, getAntigravityUserAgent } from "./wire.js";

const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
const DEFAULT_CONTEXT_WINDOW = 200_000;
const DEFAULT_MAX_TOKENS = 64_000;
const ANTIGRAVITY_DISCOVERY_DENYLIST = new Set(["chat_20706", "chat_23310", "gemini-2.5-pro"]);

interface AntigravityDiscoveryApiModel {
	displayName?: string;
	supportsImages?: boolean;
	supportsThinking?: boolean;
	maxTokens?: number;
	maxOutputTokens?: number;
	isInternal?: boolean;
}

interface AntigravityDiscoveryApiResponse {
	models?: Record<string, AntigravityDiscoveryApiModel>;
}

function toPositiveNumber(value: unknown, fallback: number): number {
	return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
}

function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscoveryApiResponse | null {
	if (typeof value !== "object" || value === null) return null;
	const payload = value as AntigravityDiscoveryApiResponse;
	if (typeof payload.models !== "object" || payload.models === null) return null;
	return payload;
}

function trimTrailingSlashes(value: string): string {
	return value.replace(/\/+$/, "");
}

/**
 * Fetch discoverable Antigravity models and normalize them into model configs.
 * Returns null on network/payload/auth failures; [] only on a successful empty response.
 */
export async function fetchAntigravityModels(
	token: string,
	options: { endpoint?: string; signal?: AbortSignal; fetcher?: typeof fetch } = {},
): Promise<ProviderModelConfig[] | null> {
	if (options.fetcher === undefined) {
		await ensureAntigravityVersion(fetch, options.signal);
	}

	const fetcher = options.fetcher ?? fetch;
	const endpoints = options.endpoint
		? [trimTrailingSlashes(options.endpoint)]
		: ANTIGRAVITY_ENDPOINT_FALLBACKS.map(trimTrailingSlashes);

	for (const endpoint of endpoints) {
		let response: Response;
		try {
			response = await fetcher(`${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`, {
				method: "POST",
				headers: {
					Authorization: `Bearer ${token}`,
					"Content-Type": "application/json",
					"User-Agent": getAntigravityUserAgent(),
				},
				body: JSON.stringify({}),
				signal: options.signal,
			});
		} catch {
			continue;
		}

		if (!response.ok) {
			continue;
		}

		let payload: unknown;
		try {
			payload = await response.json();
		} catch {
			continue;
		}

		const parsed = parseAntigravityDiscoveryResponse(payload);
		if (!parsed) {
			continue;
		}

		const models: ProviderModelConfig[] = [];
		for (const [modelId, model] of Object.entries(parsed.models ?? {})) {
			if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId)) continue;
			if (model.isInternal === true) continue;

			const supportsImages = model.supportsImages === true;
			models.push({
				id: modelId,
				name: model.displayName || modelId,
				reasoning: model.supportsThinking === true,
				input: supportsImages ? ["text", "image"] : ["text"],
				cost: resolveAntigravityModelCost(modelId),
				contextWindow: toPositiveNumber(model.maxTokens, DEFAULT_CONTEXT_WINDOW),
				maxTokens: toPositiveNumber(model.maxOutputTokens, DEFAULT_MAX_TOKENS),
			});
		}

		models.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
		return models;
	}

	return null;
}
