import { AppNode, Env, NodeType, ServiceNode } from "config";
import { Message, MessageType } from "./state";

export interface Validator {
	(nodes: AppNode[], env: Env): Message[];
}

function CombineValidators(...v: Validator[]): Validator {
	return (n, env) => v.flatMap((v) => v(n, env));
}

function MessageTypeToNumber(t: MessageType) {
	switch (t) {
		case "FATAL":
			return 0;
		case "WARNING":
			return 1;
		case "INFO":
			return 2;
	}
}

function NodeTypeToNumber(t?: NodeType) {
	switch (t) {
		case "github":
			return 0;
		case "app":
			return 1;
		case "volume":
			return 2;
		case "postgresql":
			return 3;
		case "mongodb":
			return 4;
		case "gateway-https":
			return 5;
		case "gateway-tcp":
			return 7;
		case "network":
			return 8;
		case undefined: // For NANode
			return 100;
	}
}

function SortingValidator(v: Validator): Validator {
	return (nodes, env) => {
		const nt = new Map(nodes.map((n) => [n.id, NodeTypeToNumber(n.type)]));
		return v(nodes, env).sort((a, b) => {
			const at = MessageTypeToNumber(a.type);
			const bt = MessageTypeToNumber(b.type);
			if (a.nodeId === undefined && b.nodeId === undefined) {
				if (at !== bt) {
					return at - bt;
				}
				return a.id.localeCompare(b.id);
			}
			if (a.nodeId === undefined) {
				return -1;
			}
			if (b.nodeId === undefined) {
				return 1;
			}
			if (a.nodeId === b.nodeId) {
				if (at !== bt) {
					return at - bt;
				}
				return a.id.localeCompare(b.id);
			}
			const ant = nt.get(a.id)!;
			const bnt = nt.get(b.id)!;
			if (ant !== bnt) {
				return ant - bnt;
			}
			return a.id.localeCompare(b.id);
		});
	};
}

export function CreateValidators(): Validator {
	return SortingValidator(
		CombineValidators(
			EmptyValidator,
			GitRepositoryValidator,
			ServiceValidator,
			ServiceAnalyzisValidator,
			GatewayHTTPSValidator,
			GatewayTCPValidator,
			AgentApiKeyValidator,
		),
	);
}

function EmptyValidator(nodes: AppNode[]): Message[] {
	if (nodes.some((n) => n.type !== "network")) {
		return [];
	}
	return [
		{
			id: "no-nodes",
			type: "FATAL",
			message: "Start by importing application source code",
			onHighlight: (store) => store.setHighlightCategory("repository", true),
			onLooseHighlight: (store) => store.setHighlightCategory("repository", false),
		},
	];
}

function GitRepositoryValidator(nodes: AppNode[]): Message[] {
	const git = nodes.filter((n) => n.type === "github");
	const noAddress: Message[] = git
		.filter((n) => n.data == null || n.data.repository == null)
		.map(
			(n) =>
				({
					id: `${n.id}-no-address`,
					type: "FATAL",
					nodeId: n.id,
					message: "Configure repository address",
					onHighlight: (store) => store.updateNode(n.id, { selected: true }),
					onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
				}) satisfies Message,
		);
	const noApp = git
		.filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.repoNodeId === n.id))
		.map(
			(n) =>
				({
					id: `${n.id}-no-app`,
					type: "WARNING",
					nodeId: n.id,
					message: "Connect to service",
					onHighlight: (store) => store.setHighlightCategory("Services", true),
					onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
				}) satisfies Message,
		);
	return noAddress.concat(noApp);
}

function ServiceValidator(nodes: AppNode[]): Message[] {
	const apps = nodes.filter((n) => n.type === "app");
	const noName = apps
		.filter((n) => n.data == null || n.data.label == null || n.data.label === "")
		.map(
			(n): Message => ({
				id: `${n.id}-no-name`,
				type: "FATAL",
				nodeId: n.id,
				message: "Name the service",
				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
				onClick: (store) => {
					store.updateNode(n.id, { selected: true });
					store.updateNodeData<"app">(n.id, {
						activeField: "name",
					});
				},
			}),
		);
	const noSource = apps
		.filter((n) => n.data.type !== "sketch:latest")
		.filter((n) => n.data == null || n.data.repository == null || n.data.repository.repoNodeId === "")
		.map(
			(n): Message => ({
				id: `${n.id}-no-repo`,
				type: "FATAL",
				nodeId: n.id,
				message: "Connect to source repository",
				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
			}),
		);
	const noRuntime = apps
		.filter((n) => n.data == null || n.data.type == null)
		.map(
			(n): Message => ({
				id: `${n.id}-no-runtime`,
				type: "FATAL",
				nodeId: n.id,
				message: "Choose runtime",
				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
				onClick: (store) => {
					store.updateNode(n.id, { selected: true });
					store.updateNodeData<"app">(n.id, {
						activeField: "type",
					});
				},
			}),
		);
	const noPorts = apps
		.filter((n) => n.data == null || n.data.ports == null || n.data.ports.length === 0)
		.map(
			(n): Message => ({
				id: `${n.id}-no-ports`,
				type: "INFO",
				nodeId: n.id,
				message: "Expose ports",
				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
			}),
		);
	const noIngress = apps
		.filter((n) => n.data.type !== "sketch:latest")
		.flatMap((n): Message[] => {
			if (n.data == null) {
				return [];
			}
			return (n.data.ports || [])
				.filter(
					(p) =>
						!nodes
							.filter((i) => i.type === "gateway-https")
							.some((i) => {
								if (
									i.data &&
									i.data.https &&
									i.data.https.serviceId === n.id &&
									i.data.https.portId === p.id
								) {
									return true;
								}
								return false;
							}),
				)
				.map(
					(p): Message => ({
						id: `${n.id}-${p.id}-no-ingress`,
						type: "WARNING",
						nodeId: n.id,
						message: `Connect to gateway: ${p.name} - ${p.value}`,
						onHighlight: (store) => {
							store.updateNode(n.id, { selected: true });
							store.setHighlightCategory("gateways", true);
						},
						onLooseHighlight: (store) => {
							store.updateNode(n.id, { selected: false });
							store.setHighlightCategory("gateways", false);
						},
					}),
				);
		});
	const multipleIngress = apps
		.filter((n) => n.data != null && n.data.ports != null)
		.flatMap((n) =>
			n.data.ports.map((p): Message | undefined => {
				const ing = nodes
					.filter((i) => i.type === "gateway-https")
					.filter(
						(i) =>
							i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id,
					);
				if (ing.length < 2) {
					return undefined;
				}
				return {
					id: `${n.id}-${p.id}-multiple-ingress`,
					type: "FATAL",
					nodeId: n.id,
					message: `Can not expose same port using multiple ingresses: ${p.name} - ${p.value}`,
					onHighlight: (store) => store.updateNode(n.id, { selected: true }),
					onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
				};
			}),
		)
		.filter((m) => m !== undefined);
	return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
}

function ServiceAnalyzisValidator(nodes: AppNode[]): Message[] {
	const apps = nodes.filter((n) => n.type === "app");
	return apps
		.filter((n) => n.data.info != null)
		.flatMap((n) => {
			return n.data
				.info!.configVars.map((cv): Message | undefined => {
					if (cv.semanticType === "PORT") {
						if (
							!(n.data.envVars || []).some(
								(p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
							)
						) {
							return {
								id: `${n.id}-missing-port-${cv.name}`,
								type: "WARNING",
								nodeId: n.id,
								message: `Service requires port env variable ${cv.name}`,
							};
						}
					}
					if (cv.category === "EnvironmentVariable") {
						if (
							!(n.data.envVars || []).some(
								(p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
							)
						) {
							if (cv.semanticType === "FILESYSTEM_PATH") {
								return {
									id: `${n.id}-missing-env-${cv.name}`,
									type: "FATAL",
									nodeId: n.id,
									message: `Service requires env variable ${cv.name}, representing filesystem path`,
								};
							} else if (cv.semanticType === "POSTGRES_URL") {
								return {
									id: `${n.id}-missing-env-${cv.name}`,
									type: "FATAL",
									nodeId: n.id,
									message: `Service requires env variable ${cv.name}, representing postgres connection URL`,
								};
							}
						}
					}
					return undefined;
				})
				.filter((m) => m !== undefined);
		});
}

function GatewayHTTPSValidator(nodes: AppNode[]): Message[] {
	const ing = nodes.filter((n) => n.type === "gateway-https");
	const noNetwork: Message[] = ing
		.filter(
			(n) =>
				n.data == null ||
				n.data.network == null ||
				n.data.network == "" ||
				n.data.subdomain == null ||
				n.data.subdomain == "",
		)
		.map(
			(n): Message => ({
				id: `${n.id}-no-network`,
				type: "FATAL",
				nodeId: n.id,
				message: "Network and subdomain must be defined",
				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
			}),
		);
	const notConnected: Message[] = ing
		.filter(
			(n) =>
				n.data == null ||
				n.data.https == null ||
				n.data.https.serviceId == null ||
				n.data.https.serviceId == "" ||
				n.data.https.portId == null ||
				n.data.https.portId == "",
		)
		.map((n) => ({
			id: `${n.id}-not-connected`,
			type: "FATAL",
			nodeId: n.id,
			message: "Connect to a service port",
			onHighlight: (store) => store.updateNode(n.id, { selected: true }),
			onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
		}));
	return noNetwork.concat(notConnected);
}

function GatewayTCPValidator(nodes: AppNode[]): Message[] {
	const ing = nodes.filter((n) => n.type === "gateway-tcp");
	const noNetwork: Message[] = ing
		.filter(
			(n) =>
				n.data == null ||
				n.data.network == null ||
				n.data.network == "" ||
				n.data.subdomain == null ||
				n.data.subdomain == "",
		)
		.map(
			(n): Message => ({
				id: `${n.id}-no-network`,
				type: "FATAL",
				nodeId: n.id,
				message: "Network and subdomain must be defined",
				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
			}),
		);
	const notConnected: Message[] = ing
		.filter((n) => n.data == null || n.data.exposed == null || n.data.exposed.length === 0)
		.map((n) => ({
			id: `${n.id}-not-connected`,
			type: "FATAL",
			nodeId: n.id,
			message: "Connect to a service port",
			onHighlight: (store) => store.updateNode(n.id, { selected: true }),
			onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
		}));
	return noNetwork.concat(notConnected);
}

function AgentApiKeyValidator(nodes: AppNode[], env: Env): Message[] {
	return nodes
		.filter((n): n is ServiceNode => n.type === "app" && n.data.type === "sketch:latest")
		.flatMap((n) => {
			const messages: Message[] = [];
			const model = n.data.model;

			if (!model || !model.name) {
				messages.push({
					id: `${n.id}-no-agent-model`,
					type: "FATAL",
					nodeId: n.id,
					message: "Select an AI model for the agent (Gemini or Claude).",
				});
				return messages;
			}

			if (model.name === "gemini" && !model.apiKey && !env.integrations.gemini) {
				messages.push({
					id: `${n.id}-no-gemini-api-key`,
					type: "FATAL",
					nodeId: n.id,
					message: "Configure Gemini API key either on the service or in the project integrations.",
				});
			}

			if (model.name === "claude" && !model.apiKey && !env.integrations.anthropic) {
				messages.push({
					id: `${n.id}-no-anthropic-api-key`,
					type: "FATAL",
					nodeId: n.id,
					message: "Configure Anthropic API key either on the service or in the project integrations.",
				});
			}

			return messages;
		});
}
