import { logger } from "@oh-my-pi/pi-utils";
import type { MCPServerConfig } from "./types";

const SMITHERY_REGISTRY_BASE_URL = "https://registry.smithery.ai";

type SmitherySearchEntry = {
	id?: string;
	qualifiedName?: string;
	namespace?: string;
	slug?: string;
	displayName?: string;
	description?: string;
	remote?: boolean;
	score?: number;
	useCount?: number;
	homepage?: string;
	verified?: boolean;
	isDeployed?: boolean;
	createdAt?: string;
	owner?: string;
	iconUrl?: string;
};

type SmitheryConnection = {
	type?: "http" | "stdio";
	deploymentUrl?: string;
	configSchema?: SmitheryConfigSchema;
};

type SmitheryConfigSchema = {
	type?: string;
	required?: string[];
	properties?: Record<string, SmitheryConfigProperty>;
};

type SmitheryConfigProperty = {
	type?: string;
	description?: string;
	default?: unknown;
	enum?: unknown[];
	format?: string;
};

type SmitheryServerDetails = {
	qualifiedName?: string;
	displayName?: string;
	description?: string;
	remote?: boolean;
	deploymentUrl?: string;
	connections?: SmitheryConnection[];
	security?: unknown;
	tools?: unknown;
};

type SmitheryToolDefinition = {
	name?: string;
	description?: string;
	inputSchema?: {
		type?: string;
		properties?: Record<string, unknown>;
		required?: string[];
	};
};

type RegistryInputType = "string" | "number" | "boolean";

export type SmitherySearchResult = {
	id: string;
	name: string;
	title?: string;
	description?: string;
	score?: number;
	useCount?: number;
	display: {
		displayName: string;
		description: string;
		useCount: number;
		verified: boolean;
		deployed: boolean;
		transport: string;
		connectionType: string;
		createdAt?: string;
		homepage?: string;
		tools: Array<{
			name: string;
			description?: string;
			params: string[];
		}>;
	};
	sourceType: "remote" | "package";
	config: MCPServerConfig;
	warnings: string[];
	requiredInputs: Array<{
		key: string;
		label: string;
		type: RegistryInputType;
		required: boolean;
		defaultValue?: string;
		description?: string;
		enumValues?: string[];
		sensitive: boolean;
	}>;
};

export interface SmitherySearchOptions {
	limit?: number;
	apiKey?: string;
	includeSemantic?: boolean;
}

export class SmitheryRegistryError extends Error {
	status: number;

	constructor(message: string, status: number) {
		super(message);
		this.name = "SmitheryRegistryError";
		this.status = status;
	}
}

function clampLimit(limit: number | undefined): number {
	if (!limit || Number.isNaN(limit)) return 20;
	if (limit < 1) return 1;
	if (limit > 100) return 100;
	return Math.trunc(limit);
}

function matchesIdentityQuery(query: string, entry: SmitherySearchEntry): boolean {
	const normalizedQuery = query.trim().toLowerCase();
	if (!normalizedQuery) return true;
	const displayName = entry.displayName?.toLowerCase() ?? "";
	const qualifiedName = entry.qualifiedName?.toLowerCase() ?? "";
	return displayName.includes(normalizedQuery) || qualifiedName.includes(normalizedQuery);
}

function resolveDetailPathCandidates(entry: SmitherySearchEntry): string[] {
	const candidates: string[] = [];
	const pushUnique = (value: string | undefined): void => {
		if (!value) return;
		if (!candidates.includes(value)) candidates.push(value);
	};

	if (entry.namespace && entry.slug) {
		pushUnique(`${entry.namespace}/${entry.slug}`);
	}
	if (entry.slug) {
		pushUnique(entry.slug);
	}
	const qualifiedName = entry.qualifiedName?.trim();
	if (qualifiedName) {
		pushUnique(qualifiedName.replace(/^@/, ""));
	}
	return candidates;
}

function getEntryIdentityKey(entry: SmitherySearchEntry): string | null {
	const candidates = resolveDetailPathCandidates(entry);
	if (candidates.length > 0) {
		return candidates[0] ?? null;
	}
	if (entry.id) return `id:${entry.id}`;
	return null;
}

function toConfigNameFromQualifiedName(qualifiedName: string): string {
	const normalized = qualifiedName
		.toLowerCase()
		.replace(/^@/, "")
		.replace(/\//g, "-")
		.replace(/[^a-z0-9_.-]+/g, "-")
		.replace(/-+/g, "-")
		.replace(/^-+|-+$/g, "");
	return normalized.length > 0 ? normalized : "mcp-server";
}

function normalizeQualifiedName(value: string): string {
	return value.startsWith("@") ? value : `@${value}`;
}

function scalarToString(value: unknown): string | undefined {
	if (typeof value === "string") return value;
	if (typeof value === "number" || typeof value === "boolean") return String(value);
	return undefined;
}

function unknownToString(value: unknown): string | undefined {
	if (value === null || value === undefined) return undefined;
	if (typeof value === "string") return value;
	if (typeof value === "number" || typeof value === "boolean") return String(value);
	try {
		return JSON.stringify(value);
	} catch {
		return undefined;
	}
}

function safeMetadataValue(value: unknown): string | undefined {
	const raw = unknownToString(value);
	if (!raw) return undefined;
	const normalized = raw
		.replace(/[\r\n\t]+/g, " ")
		.replace(/\s+/g, " ")
		.trim();
	return normalized.length > 0 ? normalized : undefined;
}

function toDateLabel(value: string | undefined): string | undefined {
	if (!value) return undefined;
	const date = new Date(value);
	if (Number.isNaN(date.getTime())) return undefined;
	return date.toISOString().slice(0, 10);
}

function getToolsList(tools: unknown): SmitherySearchResult["display"]["tools"] {
	if (!Array.isArray(tools)) return [];
	const output: SmitherySearchResult["display"]["tools"] = [];
	for (const item of tools) {
		const tool = item as SmitheryToolDefinition;
		const name = safeMetadataValue(tool.name);
		if (!name) continue;
		const description = safeMetadataValue(tool.description);
		const params = tool.inputSchema?.properties ? Object.keys(tool.inputSchema.properties) : [];
		output.push({
			name,
			description,
			params,
		});
	}
	return output;
}

function getInputType(propertyType: string | undefined): RegistryInputType {
	if (propertyType === "number" || propertyType === "integer") return "number";
	if (propertyType === "boolean") return "boolean";
	return "string";
}

function isSensitiveInput(key: string, format: string | undefined): boolean {
	if (format?.toLowerCase() === "password") return true;
	return /(api[_-]?key|token|secret|password)/i.test(key);
}

function getSchemaInputs(schema: SmitheryConfigSchema | undefined): SmitherySearchResult["requiredInputs"] {
	const required = new Set(schema?.required ?? []);
	const properties = schema?.properties ?? {};
	const inputs: SmitherySearchResult["requiredInputs"] = [];

	for (const [key, property] of Object.entries(properties)) {
		const type = getInputType(property.type);
		const enumValues = Array.isArray(property.enum)
			? property.enum.map(scalarToString).filter((value): value is string => Boolean(value))
			: undefined;
		inputs.push({
			key,
			label: key.replace(/[_-]+/g, " "),
			type,
			required: required.has(key),
			defaultValue: scalarToString(property.default),
			description: property.description,
			enumValues: enumValues && enumValues.length > 0 ? enumValues : undefined,
			sensitive: isSensitiveInput(key, property.format),
		});
	}

	return inputs;
}

function chooseConnection(
	details: SmitheryServerDetails,
): { connection: SmitheryConnection; useDirectHttp: boolean } | null {
	const connections = details.connections ?? [];
	const httpConnection = connections.find(connection => connection.type === "http" && !!connection.deploymentUrl);
	if (httpConnection) {
		const hasConfigInputs = getSchemaInputs(httpConnection.configSchema).length > 0;
		if (!hasConfigInputs) {
			return { connection: httpConnection, useDirectHttp: true };
		}
	}

	const stdioConnection = connections.find(connection => connection.type === "stdio");
	if (stdioConnection) {
		return { connection: stdioConnection, useDirectHttp: false };
	}

	if (httpConnection) {
		return { connection: httpConnection, useDirectHttp: false };
	}

	return null;
}

function createConfig(
	qualifiedName: string,
	selected: { connection: SmitheryConnection; useDirectHttp: boolean },
): MCPServerConfig | null {
	if (selected.useDirectHttp && selected.connection.type === "http" && selected.connection.deploymentUrl) {
		return {
			type: "http",
			url: selected.connection.deploymentUrl,
		};
	}

	return {
		type: "stdio",
		command: "bunx",
		args: ["-y", "@smithery/cli", "run", normalizeQualifiedName(qualifiedName), "--config", "{}"],
	};
}

async function fetchServerDetails(path: string, options?: { apiKey?: string }): Promise<SmitheryServerDetails | null> {
	const headers = new Headers();
	if (options?.apiKey) {
		headers.set("Authorization", `Bearer ${options.apiKey}`);
	}
	const response = await fetch(`${SMITHERY_REGISTRY_BASE_URL}/servers/${path}`, {
		headers,
	});
	if (!response.ok) return null;
	return (await response.json()) as SmitheryServerDetails;
}

async function fetchServerDetailsFromEntry(
	entry: SmitherySearchEntry,
	options?: { apiKey?: string },
): Promise<SmitheryServerDetails | null> {
	const candidates = resolveDetailPathCandidates(entry);
	for (const candidate of candidates) {
		try {
			const details = await fetchServerDetails(candidate, options);
			if (details) return details;
		} catch (error) {
			logger.debug("Smithery detail fetch candidate failed", { candidate, error: String(error) });
		}
	}
	return null;
}

function toSearchResult(entry: SmitherySearchEntry, details: SmitheryServerDetails): SmitherySearchResult | null {
	if (!entry.id) return null;
	const qualifiedName = normalizeQualifiedName(
		details.qualifiedName ?? entry.qualifiedName ?? `${entry.namespace}/${entry.slug}`,
	);
	const selected = chooseConnection(details);
	if (!selected) return null;

	const config = createConfig(qualifiedName, selected);
	if (!config) return null;

	const requiredInputs = getSchemaInputs(selected.connection.configSchema);
	const warnings: string[] = [];
	if (config.type === "stdio") {
		warnings.push("Runs through Smithery CLI at runtime (`bunx @smithery/cli run ...`).");
	}
	if (requiredInputs.length > 0) {
		warnings.push("Provider requires configuration input defined by Smithery schema.");
	}
	const displayName = safeMetadataValue(details.displayName ?? entry.displayName) ?? qualifiedName.replace(/^@/, "");
	const description = safeMetadataValue(details.description ?? entry.description) ?? "No description";
	const connectionType = safeMetadataValue(selected.connection.type) ?? "unknown";
	const transport = safeMetadataValue(config.type ?? "stdio") ?? "stdio";
	const createdAt = toDateLabel(entry.createdAt);
	const homepage = safeMetadataValue(entry.homepage);
	const tools = getToolsList(details.tools);

	return {
		id: entry.id,
		name: qualifiedName.replace(/^@/, ""),
		title: details.displayName ?? entry.displayName,
		description: details.description ?? entry.description,
		score: entry.score,
		useCount: entry.useCount,
		display: {
			displayName,
			description,
			useCount: entry.useCount ?? 0,
			verified: entry.verified === true,
			deployed: entry.isDeployed === true,
			transport,
			connectionType,
			createdAt,
			homepage,
			tools,
		},
		sourceType: selected.useDirectHttp || details.remote ? "remote" : "package",
		config,
		requiredInputs,
		warnings,
	};
}

export async function searchSmitheryRegistry(
	keyword: string,
	options?: SmitherySearchOptions,
): Promise<SmitherySearchResult[]> {
	const query = keyword.trim();
	if (!query) return [];

	const limit = clampLimit(options?.limit);
	const isSemantic = options?.includeSemantic === true;
	const pageSize = Math.max(limit * 2, 20);
	const headers = new Headers();
	if (options?.apiKey) {
		headers.set("Authorization", `Bearer ${options.apiKey}`);
	}

	// Fetch pages until we have enough filtered entries or run out of results.
	const maxPages = 3;
	const allEntries: SmitherySearchEntry[] = [];
	for (let page = 1; page <= maxPages; page++) {
		const url = new URL(`${SMITHERY_REGISTRY_BASE_URL}/servers`);
		url.searchParams.set("q", query);
		url.searchParams.set("pageSize", String(pageSize));
		if (page > 1) url.searchParams.set("page", String(page));
		const response = await fetch(url.toString(), { headers });
		if (!response.ok) {
			throw new SmitheryRegistryError(`Smithery search failed with status ${response.status}`, response.status);
		}
		const payload = (await response.json()) as { servers?: SmitherySearchEntry[] };
		const pageEntries = payload.servers ?? [];
		if (pageEntries.length === 0) break;
		allEntries.push(...pageEntries);

		// Stop early if we already have enough identity-matching entries.
		const filtered = isSemantic ? allEntries : allEntries.filter(entry => matchesIdentityQuery(query, entry));
		if (filtered.length >= limit * 2) break;
		if (pageEntries.length < pageSize) break;
	}

	const entries = isSemantic ? [...allEntries] : [...allEntries].filter(entry => matchesIdentityQuery(query, entry));

	// Only apply local useCount sort when not in semantic mode (preserve API relevance ranking).
	if (!isSemantic) {
		entries.sort((a, b) => (b.useCount ?? 0) - (a.useCount ?? 0));
	}

	const uniqueEntries = entries.filter((entry, index) => {
		const identity = getEntryIdentityKey(entry);
		if (!identity) return false;
		return (
			entries.findIndex(candidate => {
				const candidateIdentity = getEntryIdentityKey(candidate);
				return candidateIdentity === identity;
			}) === index
		);
	});

	const detailFailures: Array<{ identity: string; error: string }> = [];
	const results = await Promise.all(
		uniqueEntries.map(async entry => {
			try {
				const details = await fetchServerDetailsFromEntry(entry, { apiKey: options?.apiKey });
				if (!details) return null;
				return toSearchResult(entry, details);
			} catch (error) {
				detailFailures.push({
					identity: getEntryIdentityKey(entry) ?? entry.id ?? "unknown",
					error: String(error),
				});
				return null;
			}
		}),
	);

	if (detailFailures.length > 0) {
		logger.warn("Smithery detail fetch failed for some entries", {
			query,
			failedEntries: detailFailures.length,
			totalEntries: uniqueEntries.length,
			sample: detailFailures.slice(0, 3),
		});
	}
	return results.filter((result): result is SmitherySearchResult => result !== null).slice(0, limit);
}

export function toConfigName(candidate: string): string {
	return toConfigNameFromQualifiedName(candidate);
}