/**
 * Model discovery adapters for runtime model enumeration.
 * Provider adapters fetch directly from provider APIs; catalog adapters fetch
 * from local catalog services such as Singularity Memory.
 */

export interface DiscoveredModel {
	id: string;
	provider?: string;
	name?: string;
	api?: string;
	baseUrl?: string;
	contextWindow?: number;
	maxTokens?: number;
	reasoning?: boolean;
	input?: ("text" | "image")[];
	cost?: {
		input: number;
		output: number;
		cacheRead: number;
		cacheWrite: number;
	};
}

export interface DiscoveryResult {
	provider: string;
	sourceType?: DiscoverySourceType;
	models: DiscoveredModel[];
	fetchedAt: number;
	error?: string;
}

export type DiscoverySourceType = "provider" | "catalog";

export interface ProviderDiscoveryAdapter {
	provider: string;
	sourceType?: DiscoverySourceType;
	supportsDiscovery: boolean;
	requiresAuthForDiscovery?: boolean;
	fetchModels(apiKey: string, baseUrl?: string): Promise<DiscoveredModel[]>;
}

/** Per-provider TTLs in milliseconds */
export const DISCOVERY_TTLS: Record<string, number> = {
	"ollama-cloud": 60 * 60 * 1000, // 1 hour
	openrouter: 60 * 60 * 1000, // 1 hour
	zai: 60 * 60 * 1000, // 1 hour
	minimax: 60 * 60 * 1000, // 1 hour
	"kimi-coding": 60 * 60 * 1000, // 1 hour
	xiaomi: 60 * 60 * 1000, // 1 hour
	mistral: 60 * 60 * 1000, // 1 hour
	"singularity-memory": 5 * 60 * 1000, // 5 minutes (local daemon catalog)
	default: 24 * 60 * 60 * 1000, // 24 hours
};

export function getDefaultTTL(provider: string): number {
	return DISCOVERY_TTLS[provider] ?? DISCOVERY_TTLS.default;
}

async function fetchWithTimeout(
	url: string,
	options: RequestInit = {},
	timeoutMs = 5000,
): Promise<Response> {
	const controller = new AbortController();
	const timeout = setTimeout(() => controller.abort(), timeoutMs);
	try {
		return await fetch(url, { ...options, signal: controller.signal });
	} finally {
		clearTimeout(timeout);
	}
}

// ─── Ollama Adapter ──────────────────────────────────────────────────────────

class OllamaDiscoveryAdapter implements ProviderDiscoveryAdapter {
	provider = "ollama";
	supportsDiscovery = false;

	async fetchModels(
		_apiKey: string,
		baseUrl?: string,
	): Promise<DiscoveredModel[]> {
		const url = `${baseUrl ?? "http://localhost:11434"}/api/tags`;
		const response = await fetchWithTimeout(url);

		if (!response.ok) {
			throw new Error(
				`Ollama tags API returned ${response.status}: ${response.statusText}`,
			);
		}

		const data = (await response.json()) as {
			models: Array<{
				name: string;
				size: number;
				details?: { parameter_size?: string };
			}>;
		};

		return (data.models ?? []).map((m) => ({
			id: m.name,
			name: m.name,
			input: ["text" as const],
		}));
	}
}

// ─── Ollama Cloud Adapter ────────────────────────────────────────────────────

class OllamaCloudDiscoveryAdapter implements ProviderDiscoveryAdapter {
	provider = "ollama-cloud";
	supportsDiscovery = true;
	requiresAuthForDiscovery = false;

	async fetchModels(
		apiKey: string,
		baseUrl?: string,
	): Promise<DiscoveredModel[]> {
		const root = (baseUrl ?? "https://ollama.com").replace(/\/+$/, "");
		const primaryUrl =
			root.endsWith("/v1/models") || root.endsWith("/api/tags")
				? root
				: `${root}/v1/models`;
		const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
		const response = await fetchWithTimeout(primaryUrl, { headers });

		if (response.ok) {
			return this.mapOpenAIModelsResponse(await response.json());
		}

		if (primaryUrl.endsWith("/v1/models")) {
			const fallbackUrl = `${root}/api/tags`;
			const fallback = await fetchWithTimeout(fallbackUrl, { headers });
			if (fallback.ok) return this.mapTagsResponse(await fallback.json());
		}

		throw new Error(
			`Ollama Cloud models API returned ${response.status}: ${response.statusText}`,
		);
	}

	private mapOpenAIModelsResponse(data: unknown): DiscoveredModel[] {
		const models = (data as { data?: Array<{ id?: string }> }).data ?? [];
		return models
			.map((m) => m.id)
			.filter((id): id is string => typeof id === "string" && id.length > 0)
			.map((id) => this.toOpenAICompletionsModel(id));
	}

	private mapTagsResponse(data: unknown): DiscoveredModel[] {
		const models =
			(data as { models?: Array<{ name?: string; model?: string }> }).models ??
			[];
		return models
			.map((m) => m.name ?? m.model)
			.filter((id): id is string => typeof id === "string" && id.length > 0)
			.map((id) => this.toOpenAICompletionsModel(id));
	}

	private toOpenAICompletionsModel(id: string): DiscoveredModel {
		return {
			id,
			name: id,
			api: "openai-completions",
			baseUrl: "https://ollama.com/v1",
			input: ["text" as const],
		};
	}
}

// ─── OpenRouter Adapter ──────────────────────────────────────────────────────

class OpenRouterDiscoveryAdapter implements ProviderDiscoveryAdapter {
	provider = "openrouter";
	supportsDiscovery = true;
	requiresAuthForDiscovery = false;

	async fetchModels(
		apiKey: string,
		baseUrl?: string,
	): Promise<DiscoveredModel[]> {
		const root = (baseUrl ?? "https://openrouter.ai").replace(/\/+$/, "");
		const url = root.endsWith("/api/v1")
			? `${root}/models`
			: `${root}/api/v1/models`;
		const response = await fetchWithTimeout(url, {
			headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
		});

		if (!response.ok) {
			throw new Error(
				`OpenRouter models API returned ${response.status}: ${response.statusText}`,
			);
		}

		const data = (await response.json()) as {
			data: Array<{
				id: string;
				name: string;
				context_length?: number;
				top_provider?: { max_completion_tokens?: number };
				pricing?: { prompt: string; completion: string };
			}>;
		};

		return (data.data ?? [])
			.filter((m) => m.id.endsWith(":free"))
			.map((m) => {
				const cost =
					m.pricing?.prompt !== undefined && m.pricing?.completion !== undefined
						? {
								input: parseFloat(m.pricing.prompt) * 1_000_000,
								output: parseFloat(m.pricing.completion) * 1_000_000,
								cacheRead: 0,
								cacheWrite: 0,
							}
						: undefined;

				return {
					id: m.id,
					name: m.name,
					contextWindow: m.context_length,
					maxTokens: m.top_provider?.max_completion_tokens,
					cost,
					input: ["text" as const, "image" as const],
				};
			});
	}
}

// ─── OpenAI-Style Model List Adapters ───────────────────────────────────────

interface ProviderListModel {
	id?: string;
	name?: string;
	display_name?: string;
	displayName?: string;
	context_length?: number;
	contextWindow?: number;
	context_window?: number;
	max_context_length?: number;
	inputTokenLimit?: number;
	max_tokens?: number;
	maxTokens?: number;
	outputTokenLimit?: number;
	max_output_tokens?: number;
	top_provider?: { max_completion_tokens?: number };
	pricing?: { prompt?: string; completion?: string };
	architecture?: { input_modalities?: string[]; output_modalities?: string[] };
	modalities?: { input?: string[]; output?: string[] };
	supported_parameters?: string[];
	reasoning?: boolean;
	capabilities?: {
		reasoning?: boolean;
		vision?: boolean;
		completion_chat?: boolean;
	};
}

interface ModelListAdapterOptions {
	provider: string;
	discoveryBaseUrl: string;
	executionBaseUrl: string;
	api: string;
}

function resolveProviderModelsUrl(baseUrl: string): string {
	const root = baseUrl.replace(/\/+$/, "");
	if (root.endsWith("/models")) return root;
	if (root.endsWith("/v1") || root.endsWith("/v4")) return `${root}/models`;
	return `${root}/v1/models`;
}

function hasImageInput(model: ProviderListModel): boolean {
	const architectureInput = model.architecture?.input_modalities ?? [];
	const modalityInput = model.modalities?.input ?? [];
	return (
		architectureInput.includes("image") ||
		modalityInput.includes("image") ||
		model.capabilities?.vision === true
	);
}

function modelReasoning(model: ProviderListModel): boolean | undefined {
	if (typeof model.reasoning === "boolean") return model.reasoning;
	if (typeof model.capabilities?.reasoning === "boolean") {
		return model.capabilities.reasoning;
	}
	if (model.supported_parameters?.includes("reasoning")) return true;
	if (model.supported_parameters?.includes("thinking")) return true;
	return undefined;
}

function modelContextWindow(model: ProviderListModel): number | undefined {
	return (
		model.context_length ??
		model.contextWindow ??
		model.context_window ??
		model.max_context_length ??
		model.inputTokenLimit
	);
}

function modelMaxTokens(model: ProviderListModel): number | undefined {
	return (
		model.max_output_tokens ??
		model.maxTokens ??
		model.max_tokens ??
		model.outputTokenLimit ??
		model.top_provider?.max_completion_tokens
	);
}

class ProviderModelListAdapter implements ProviderDiscoveryAdapter {
	provider: string;
	supportsDiscovery = true;
	private readonly discoveryBaseUrl: string;
	private readonly executionBaseUrl: string;
	private readonly api: string;

	constructor(options: ModelListAdapterOptions) {
		this.provider = options.provider;
		this.discoveryBaseUrl = options.discoveryBaseUrl;
		this.executionBaseUrl = options.executionBaseUrl;
		this.api = options.api;
	}

	async fetchModels(
		apiKey: string,
		baseUrl?: string,
	): Promise<DiscoveredModel[]> {
		const url = resolveProviderModelsUrl(baseUrl ?? this.discoveryBaseUrl);
		const response = await fetchWithTimeout(url, {
			headers: { Authorization: `Bearer ${apiKey}` },
		});

		if (!response.ok) {
			throw new Error(
				`${this.provider} models API returned ${response.status}: ${response.statusText}`,
			);
		}

		const data = (await response.json()) as {
			data?: ProviderListModel[];
			models?: ProviderListModel[];
		};
		const rows = data.data ?? data.models ?? [];
		return rows
			.map((m) => ({ ...m, id: m.id ?? m.name }))
			.filter(
				(m): m is ProviderListModel & { id: string } =>
					typeof m.id === "string" && m.id.length > 0,
			)
			.map((m) => ({
				id: m.id,
				name: m.display_name ?? m.displayName ?? m.name ?? m.id,
				provider: this.provider,
				api: this.api,
				baseUrl: this.executionBaseUrl,
				contextWindow: modelContextWindow(m),
				maxTokens: modelMaxTokens(m),
				reasoning: modelReasoning(m),
				input: hasImageInput(m) ? ["text", "image"] : ["text"],
				cost:
					m.pricing?.prompt !== undefined || m.pricing?.completion !== undefined
						? {
								input: parseOpenRouterPricePer1M(m.pricing?.prompt),
								output: parseOpenRouterPricePer1M(m.pricing?.completion),
								cacheRead: 0,
								cacheWrite: 0,
							}
						: undefined,
			}));
	}
}

// ─── Singularity Memory Adapter ─────────────────────────────────────────────

interface OpenRouterStyleModel {
	id: string;
	name?: string;
	canonical_slug?: string;
	context_length?: number;
	pricing?: { prompt?: string; completion?: string };
	top_provider?: { max_completion_tokens?: number };
	architecture?: { input_modalities?: string[]; output_modalities?: string[] };
	supported_parameters?: string[];
	provider?: string;
	api?: string;
	baseUrl?: string;
	base_url?: string;
	x_singularity?: {
		provider?: string;
		subprovider?: string;
		wire_model_id?: string;
		api?: string;
		baseUrl?: string;
		base_url?: string;
		reasoning?: boolean;
		max_tokens?: number;
	};
}

function resolveSingularityMemoryModelsUrl(baseUrl?: string): string {
	const root = (
		baseUrl ||
		process.env.SINGULARITY_MEMORY_URL ||
		process.env.SF_MODEL_CATALOG_URL ||
		"http://127.0.0.1:8888"
	).replace(/\/+$/, "");
	if (root.endsWith("/v1/models")) return root;
	if (root.endsWith("/v1")) return `${root}/models`;
	return `${root}/v1/models`;
}

function parseOpenRouterPricePer1M(value: string | undefined): number {
	if (value === undefined) return 0;
	const parsed = Number.parseFloat(value);
	return Number.isFinite(parsed)
		? Math.round(parsed * 1_000_000 * 1_000_000) / 1_000_000
		: 0;
}

function stripProviderPrefix(
	modelID: string,
	provider: string | undefined,
): string {
	if (!provider) return modelID;
	const prefix = `${provider}/`.toLowerCase();
	return modelID.toLowerCase().startsWith(prefix)
		? modelID.slice(provider.length + 1)
		: modelID;
}

class SingularityMemoryDiscoveryAdapter implements ProviderDiscoveryAdapter {
	provider = "singularity-memory";
	sourceType = "catalog" as const;
	supportsDiscovery = true;
	requiresAuthForDiscovery = false;

	async fetchModels(
		apiKey: string,
		baseUrl?: string,
	): Promise<DiscoveredModel[]> {
		const response = await fetchWithTimeout(
			resolveSingularityMemoryModelsUrl(baseUrl),
			{
				headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
			},
		);

		if (!response.ok) {
			throw new Error(
				`Singularity Memory models API returned ${response.status}: ${response.statusText}`,
			);
		}

		const data = (await response.json()) as { data?: OpenRouterStyleModel[] };
		return (data.data ?? []).map((m) => {
			const provider = m.x_singularity?.provider ?? m.provider;
			const modelID =
				m.x_singularity?.wire_model_id ?? stripProviderPrefix(m.id, provider);
			const inputModalities = m.architecture?.input_modalities ?? [];
			const input: ("text" | "image")[] = inputModalities.includes("image")
				? ["text", "image"]
				: ["text"];

			return {
				id: modelID,
				name: m.name ?? m.canonical_slug ?? m.id,
				provider,
				api: m.x_singularity?.api ?? m.api,
				baseUrl:
					m.x_singularity?.baseUrl ??
					m.x_singularity?.base_url ??
					m.baseUrl ??
					m.base_url,
				contextWindow: m.context_length,
				maxTokens:
					m.x_singularity?.max_tokens ?? m.top_provider?.max_completion_tokens,
				reasoning:
					m.x_singularity?.reasoning ??
					m.supported_parameters?.includes("reasoning"),
				input,
				cost: {
					input: parseOpenRouterPricePer1M(m.pricing?.prompt),
					output: parseOpenRouterPricePer1M(m.pricing?.completion),
					cacheRead: 0,
					cacheWrite: 0,
				},
			};
		});
	}
}

// ─── Static Adapter (no discovery) ───────────────────────────────────────────

class StaticDiscoveryAdapter implements ProviderDiscoveryAdapter {
	provider: string;
	supportsDiscovery = false;

	constructor(provider: string) {
		this.provider = provider;
	}

	async fetchModels(): Promise<DiscoveredModel[]> {
		return [];
	}
}

// ─── Registry ────────────────────────────────────────────────────────────────

const adapters: Record<string, ProviderDiscoveryAdapter> = {
	openai: new StaticDiscoveryAdapter("openai"),
	ollama: new OllamaDiscoveryAdapter(),
	"ollama-cloud": new OllamaCloudDiscoveryAdapter(),
	openrouter: new OpenRouterDiscoveryAdapter(),
	zai: new ProviderModelListAdapter({
		provider: "zai",
		discoveryBaseUrl: "https://api.z.ai/api/coding/paas/v4",
		executionBaseUrl: "https://api.z.ai/api/coding/paas/v4",
		api: "openai-completions",
	}),
	minimax: new ProviderModelListAdapter({
		provider: "minimax",
		discoveryBaseUrl: "https://api.minimax.io/anthropic/v1",
		executionBaseUrl: "https://api.minimax.io/anthropic",
		api: "anthropic-messages",
	}),
	"kimi-coding": new StaticDiscoveryAdapter("kimi-coding"),
	xiaomi: new ProviderModelListAdapter({
		provider: "xiaomi",
		discoveryBaseUrl: "https://token-plan-ams.xiaomimimo.com",
		executionBaseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic",
		api: "anthropic-messages",
	}),
	mistral: new ProviderModelListAdapter({
		provider: "mistral",
		discoveryBaseUrl: "https://api.mistral.ai/v1",
		executionBaseUrl: "https://api.mistral.ai",
		api: "mistral-conversations",
	}),
	"singularity-memory": new SingularityMemoryDiscoveryAdapter(),
	anthropic: new StaticDiscoveryAdapter("anthropic"),
	bedrock: new StaticDiscoveryAdapter("bedrock"),
	"azure-openai": new StaticDiscoveryAdapter("azure-openai"),
	groq: new StaticDiscoveryAdapter("groq"),
	cerebras: new StaticDiscoveryAdapter("cerebras"),
	xai: new StaticDiscoveryAdapter("xai"),
	google: new StaticDiscoveryAdapter("google"),
};

export function getDiscoveryAdapter(
	provider: string,
): ProviderDiscoveryAdapter {
	return adapters[provider] ?? new StaticDiscoveryAdapter(provider);
}

export function getDiscoverableProviders(): string[] {
	return Object.entries(adapters)
		.filter(
			([, adapter]) =>
				adapter.supportsDiscovery &&
				(adapter.sourceType ?? "provider") === "provider",
		)
		.map(([name]) => name);
}

export function getDiscoverableCatalogSources(): string[] {
	return Object.entries(adapters)
		.filter(
			([, adapter]) =>
				adapter.supportsDiscovery && adapter.sourceType === "catalog",
		)
		.map(([name]) => name);
}
