import {
	AppNode,
	BoundEnvVar,
	Env,
	Edge,
	GatewayHttpsNode,
	GatewayTCPNode,
	GithubNode,
	MongoDBNode,
	Network,
	NetworkNode,
	Port,
	PostgreSQLNode,
	ServiceNode,
	VolumeNode,
	Graph,
} from "./graph.js";
import { v4 as uuidv4 } from "uuid";
import { Ingress, Service, Volume, PostgreSQL, MongoDB, Config, PortDomain, isAgent } from "./types.js";
import { GithubRepository } from "./github.js";

export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): Config | null {
	try {
		const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
		const ingressNodes = nodes
			.filter((n) => n.type === "gateway-https")
			.filter((n) => n.data.https !== undefined && !n.data.readonly);
		const tcpNodes = nodes
			.filter((n) => n.type === "gateway-tcp")
			.filter((n) => n.data.exposed !== undefined && !n.data.readonly);
		const findExpose = (n: AppNode): PortDomain[] => {
			return n.data.ports
				.map((p) => [n.id, p.id, p.name])
				.flatMap((sp) => {
					return tcpNodes.flatMap((i) =>
						(i.data.exposed || [])
							.filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
							.map(() => ({
								nodeId: i.id,
								network: networkMap.get(i.data.network!)!,
								subdomain: i.data.subdomain!,
								port: { name: sp[2] },
							})),
					);
				});
		};
		const services = nodes
			.filter((n) => n.type === "app")
			.map((n): Service => {
				return {
					nodeId: n.id,
					type: n.data.type,
					name: n.data.label,
					source:
						n.data.repository != undefined
							? {
									repository: nodes
										.filter((i) => i.type === "github")
										.find((i) => i.id === n.data.repository?.repoNodeId)!.data.repository!.sshURL,
									branch:
										n.data.repository != undefined && "branch" in n.data.repository
											? n.data.repository.branch
											: "main",
									rootDir:
										n.data.repository != undefined && "rootDir" in n.data.repository
											? n.data.repository.rootDir
											: "/",
								}
							: undefined,
					ports: (n.data.ports || [])
						.filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
						.map((p) => ({
							name: p.name.toLowerCase(),
							value: p.value,
							protocol: "TCP", // TODO(gio)
						})),
					env: (n.data.envVars || [])
						.filter((e) => "name" in e)
						.map((e) => {
							if ("value" in e) {
								return { name: e.name, value: e.value };
							}
							return {
								name: e.name,
								alias: "alias" in e ? e.alias : undefined,
							};
						}),
					ingress: ingressNodes
						.filter((i) => i.data.https!.serviceId === n.id)
						.map(
							(i): Ingress => ({
								nodeId: i.id,
								network: networkMap.get(i.data.network!)!,
								subdomain: i.data.subdomain!,
								port: {
									name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name.toLowerCase(),
								},
								auth:
									i.data.auth?.enabled || false
										? {
												enabled: true,
												groups: (i.data.auth!.groups || []).join(","),
												noAuthPathPatterns: i.data.auth!.noAuthPathPatterns || [],
											}
										: {
												enabled: false,
											},
							}),
						),
					expose: findExpose(n),
					preBuildCommands: n.data.preBuildCommands
						? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
						: [],
					dev: n.data.dev?.enabled
						? n.data.dev.mode === "VM"
							? {
									enabled: true,
									mode: "VM",
									username: env.user.username,
									codeServer:
										n.data.dev.expose != null
											? {
													network: networkMap.get(n.data.dev.expose.network)!,
													subdomain: n.data.dev.expose.subdomain,
												}
											: undefined,
									ssh:
										n.data.dev.expose != null
											? {
													network: networkMap.get(n.data.dev.expose.network)!,
													subdomain: n.data.dev.expose.subdomain,
												}
											: undefined,
								}
							: {
									enabled: true,
									mode: "PROXY",
									address: n.data.dev.address,
									vpn: {
										enabled: true,
										username: env.user.username,
									},
								}
						: {
								enabled: false,
							},
					...(n.data.model?.name === "gemini" && {
						model: {
							name: "gemini",
							geminiApiKey: n.data.model.apiKey,
						},
					}),
					...(n.data.model?.name === "claude" && {
						model: {
							name: "claude",
							anthropicApiKey: n.data.model.apiKey,
						},
					}),
				};
			});
		return {
			service: services.filter((s) => !isAgent(s)),
			agent: services.filter(isAgent),
			volume: nodes
				.filter((n) => n.type === "volume")
				.map(
					(n): Volume => ({
						nodeId: n.id,
						name: n.data.label,
						accessMode: n.data.type,
						size: n.data.size,
					}),
				),
			postgresql: nodes
				.filter((n) => n.type === "postgresql")
				.map(
					(n): PostgreSQL => ({
						nodeId: n.id,
						name: n.data.label,
						size: "1Gi", // TODO(gio)
						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
					}),
				),
			mongodb: nodes
				.filter((n) => n.type === "mongodb")
				.map(
					(n): MongoDB => ({
						nodeId: n.id,
						name: n.data.label,
						size: "1Gi", // TODO(gio)
						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
					}),
				),
		};
	} catch (e) {
		console.log(e);
		return null;
	}
}

export function configToGraph(config: Config, networks: Network[], repos: GithubRepository[], current?: Graph): Graph {
	if (current == null) {
		current = { nodes: [], edges: [] };
	}
	const ret: Graph = {
		nodes: [],
		edges: [],
	};
	if (networks.length === 0) {
		return ret;
	}
	const repoNodes = [...(config.service || []), ...(config.agent || [])]
		.filter((s) => s.source?.repository != null)
		.map((s): GithubNode | null => {
			const existing = current.nodes.find(
				(n) => n.type === "github" && n.data.repository?.sshURL === s.source!.repository,
			);
			const repo = repos.find((r) => r.ssh_url === s.source!.repository);
			if (repo == null) {
				return null;
			}
			return {
				id: existing != null ? existing.id : uuidv4(),
				type: "github",
				data: {
					label: repo.full_name,
					repository: {
						id: repo.id,
						sshURL: repo.ssh_url,
						fullName: repo.full_name,
					},
					envVars: [],
					ports: [],
				},
				position:
					existing != null
						? existing.position
						: {
								x: 0,
								y: 0,
							},
			};
		})
		.filter((n) => n != null);
	const networkNodes = networks.map((n): NetworkNode => {
		let existing: NetworkNode | undefined = undefined;
		existing = current.nodes
			.filter((i): i is NetworkNode => i.type === "network")
			.find((i) => i.data.domain === n.domain);
		return {
			id: n.domain,
			type: "network",
			data: {
				label: n.name,
				domain: n.domain,
				envVars: [],
				ports: [],
			},
			position: existing != null ? existing.position : { x: 0, y: 0 },
		};
	});
	const services = [...(config.service || []), ...(config.agent || [])].map((s): ServiceNode => {
		let existing: ServiceNode | null = null;
		if (s.nodeId !== undefined) {
			existing = current.nodes.find((n) => n.id === s.nodeId) as ServiceNode;
		}
		return {
			id: existing != null ? existing.id : uuidv4(),
			type: "app",
			data: {
				label: s.name,
				type: s.type,
				repository:
					s.source != null
						? {
								id: repoNodes.find((r) => r.data.repository?.sshURL === s.source!.repository)!.data
									.repository!.id,
								repoNodeId: repoNodes.find((r) => r.data.repository?.sshURL === s.source!.repository)!
									.id,
								branch: s.source!.branch,
								rootDir: s.source!.rootDir,
							}
						: undefined,
				ports: (s.ports || []).map(
					(p): Port => ({
						id: uuidv4(),
						name: p.name,
						value: p.value,
					}),
				),
				envVars: (s.env || []).map((e): BoundEnvVar => {
					if (e.value != null) {
						return {
							id: uuidv4(),
							source: null,
							name: e.name,
							value: e.value,
						};
					} else if (e.alias != null && e.alias !== "") {
						return {
							id: uuidv4(),
							source: null,
							name: e.name,
							alias: e.alias,
							isEditting: false,
						};
					} else {
						return {
							id: uuidv4(),
							name: e.name,
							source: null,
							isEditting: false,
						};
					}
				}),
				volume: s.volume || [],
				preBuildCommands: s.preBuildCommands?.map((p) => p.bin).join("\n") || "",
				...(s.model != null && {
					model:
						s.model.name === "gemini"
							? { name: "gemini", apiKey: s.model.geminiApiKey }
							: { name: "claude", apiKey: s.model.anthropicApiKey },
				}),
				dev: s.dev?.enabled
					? s.dev.mode === "VM"
						? {
								enabled: true,
								mode: "VM",
								expose: s.dev.ssh
									? {
											network: s.dev.ssh.network,
											subdomain: s.dev.ssh.subdomain,
										}
									: undefined,
								codeServerNodeId: uuidv4(), // TODO: proper node tracking
								sshNodeId: uuidv4(), // TODO: proper node tracking
							}
						: {
								enabled: true,
								mode: "PROXY",
								address: s.dev.address,
							}
					: {
							enabled: false,
						},
				isChoosingPortToConnect: false,
			},
			// TODO(gio): generate position
			position:
				existing != null
					? existing.position
					: {
							x: 0,
							y: 0,
						},
		};
	});
	const serviceGateways = [...(config.service || []), ...(config.agent || [])]?.flatMap(
		(s, index): GatewayHttpsNode[] => {
			return (s.ingress || []).map((i): GatewayHttpsNode => {
				let existing: GatewayHttpsNode | null = null;
				if (i.nodeId !== undefined) {
					existing = current.nodes.find((n) => n.id === i.nodeId) as GatewayHttpsNode;
				}
				return {
					id: existing != null ? existing.id : uuidv4(),
					type: "gateway-https",
					data: {
						label: i.subdomain,
						envVars: [],
						ports: [],
						network: networks.find((n) => n.name.toLowerCase() === i.network.toLowerCase())!.domain,
						subdomain: i.subdomain,
						https: {
							serviceId: services![index]!.id,
							portId: services![index]!.data.ports.find((p) => {
								const port = i.port;
								if ("name" in port) {
									return p.name === port.name;
								} else {
									return `${p.value}` === port.value;
								}
							})!.id,
						},
						auth: i.auth.enabled
							? {
									enabled: true,
									groups: (i.auth.groups || "").split(","),
									noAuthPathPatterns: i.auth.noAuthPathPatterns || [],
								}
							: {
									enabled: false,
									groups: [],
									noAuthPathPatterns: [],
								},
					},
					position: {
						x: 0,
						y: 0,
					},
				};
			});
		},
	);
	const exposures = new Map<string, GatewayTCPNode>();
	[...(config.service || []), ...(config.agent || [])]
		?.flatMap((s, index): GatewayTCPNode[] => {
			return (s.expose || []).map((e): GatewayTCPNode => {
				let existing: GatewayTCPNode | null = null;
				if (e.nodeId !== undefined) {
					existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
				}
				return {
					id: existing != null ? existing.id : uuidv4(),
					type: "gateway-tcp",
					data: {
						label: e.subdomain,
						envVars: [],
						ports: [],
						network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
						subdomain: e.subdomain,
						exposed: [
							{
								serviceId: services![index]!.id,
								portId: services![index]!.data.ports.find((p) => {
									const port = e.port;
									if ("name" in port) {
										return p.name === port.name;
									} else {
										return p.value === port.value;
									}
								})!.id,
							},
						],
					},
					position: existing != null ? existing.position : { x: 0, y: 0 },
				};
			});
		})
		.forEach((n) => {
			const key = `${n.data.network}-${n.data.subdomain}`;
			if (!exposures.has(key)) {
				exposures.set(key, n);
			} else {
				exposures.get(key)!.data.exposed.push(...n.data.exposed);
			}
		});
	const volumes = config.volume?.map((v): VolumeNode => {
		let existing: VolumeNode | null = null;
		if (v.nodeId !== undefined) {
			existing = current.nodes.find((n) => n.id === v.nodeId) as VolumeNode;
		}
		return {
			id: existing != null ? existing.id : uuidv4(),
			type: "volume",
			data: {
				label: v.name,
				type: v.accessMode,
				size: v.size,
				attachedTo: [],
				envVars: [],
				ports: [],
			},
			position:
				existing != null
					? existing.position
					: {
							x: 0,
							y: 0,
						},
		};
	});
	const postgresql = config.postgresql?.map((p): PostgreSQLNode => {
		let existing: PostgreSQLNode | null = null;
		if (p.nodeId !== undefined) {
			existing = current.nodes.find((n) => n.id === p.nodeId) as PostgreSQLNode;
		}
		return {
			id: existing != null ? existing.id : uuidv4(),
			type: "postgresql",
			data: {
				label: p.name,
				envVars: [],
				ports: [
					{
						id: "connection",
						name: "connection",
						value: 5432,
					},
				],
			},
			position:
				existing != null
					? existing.position
					: {
							x: 0,
							y: 0,
						},
		};
	});
	config.postgresql
		?.flatMap((p, index): GatewayTCPNode[] => {
			return (p.expose || []).map((e): GatewayTCPNode => {
				let existing: GatewayTCPNode | null = null;
				if (e.nodeId !== undefined) {
					existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
				}
				return {
					id: existing != null ? existing.id : uuidv4(),
					type: "gateway-tcp",
					data: {
						label: e.subdomain,
						envVars: [],
						ports: [],
						network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
						subdomain: e.subdomain,
						exposed: [
							{
								serviceId: postgresql![index]!.id,
								portId: "connection",
							},
						],
					},
					position: existing != null ? existing.position : { x: 0, y: 0 },
				};
			});
		})
		.forEach((n) => {
			const key = `${n.data.network}-${n.data.subdomain}`;
			if (!exposures.has(key)) {
				exposures.set(key, n);
			} else {
				exposures.get(key)!.data.exposed.push(...n.data.exposed);
			}
		});
	const mongodb = config.mongodb?.map((m): MongoDBNode => {
		let existing: MongoDBNode | null = null;
		if (m.nodeId !== undefined) {
			existing = current.nodes.find((n) => n.id === m.nodeId) as MongoDBNode;
		}
		return {
			id: existing != null ? existing.id : uuidv4(),
			type: "mongodb",
			data: {
				label: m.name,
				envVars: [],
				ports: [
					{
						id: "connection",
						name: "connection",
						value: 27017,
					},
				],
			},
			position:
				existing != null
					? existing.position
					: {
							x: 0,
							y: 0,
						},
		};
	});
	config.mongodb
		?.flatMap((p, index): GatewayTCPNode[] => {
			return (p.expose || []).map((e): GatewayTCPNode => {
				let existing: GatewayTCPNode | null = null;
				if (e.nodeId !== undefined) {
					existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
				}
				return {
					id: existing != null ? existing.id : uuidv4(),
					type: "gateway-tcp",
					data: {
						label: e.subdomain,
						envVars: [],
						ports: [],
						network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
						subdomain: e.subdomain,
						exposed: [
							{
								serviceId: mongodb![index]!.id,
								portId: "connection",
							},
						],
					},
					position: existing != null ? existing.position : { x: 0, y: 0 },
				};
			});
		})
		.forEach((n) => {
			const key = `${n.data.network}-${n.data.subdomain}`;
			if (!exposures.has(key)) {
				exposures.set(key, n);
			} else {
				exposures.get(key)!.data.exposed.push(...n.data.exposed);
			}
		});
	ret.nodes = [
		...networkNodes,
		...repoNodes,
		...(services || []),
		...(serviceGateways || []),
		...(volumes || []),
		...(postgresql || []),
		...(mongodb || []),
		...(exposures.values() || []),
	];
	services?.forEach((s) => {
		s.data.envVars.forEach((e) => {
			if (!("name" in e)) {
				return;
			}
			if (!e.name.startsWith("DODO_")) {
				return;
			}
			let r: {
				type: string;
				name: string;
			} | null = null;
			if (e.name.startsWith("DODO_PORT_")) {
				return;
			} else if (e.name.startsWith("DODO_POSTGRESQL_")) {
				r = {
					type: "postgresql",
					name: e.name.replace("DODO_POSTGRESQL_", "").replace("_URL", "").toLowerCase(),
				};
			} else if (e.name.startsWith("DODO_MONGODB_")) {
				r = {
					type: "mongodb",
					name: e.name.replace("DODO_MONGODB_", "").replace("_URL", "").toLowerCase(),
				};
			} else if (e.name.startsWith("DODO_VOLUME_")) {
				r = {
					type: "volume",
					name: e.name.replace("DODO_VOLUME_", "").toLowerCase(),
				};
			}
			if (r != null) {
				e.source = ret.nodes.find((n) => n.type === r.type && n.data.label.toLowerCase() === r.name)!.id;
			}
		});
	});
	const envVarEdges = [...(services || [])].flatMap((n): Edge[] => {
		return n.data.envVars.flatMap((e): Edge[] => {
			if (e.source == null) {
				return [];
			}
			const sn = ret.nodes.find((n) => n.id === e.source!)!;
			const sourceHandle = sn.type === "app" ? "ports" : sn.type === "volume" ? "volume" : "env_var";
			return [
				{
					id: uuidv4(),
					source: e.source!,
					sourceHandle: sourceHandle,
					target: n.id,
					targetHandle: "env_var",
				},
			];
		});
	});
	const exposureEdges = [...exposures.values()].flatMap((n): Edge[] => {
		return n.data.exposed.flatMap((e): Edge[] => {
			return [
				{
					id: uuidv4(),
					source: e.serviceId,
					sourceHandle: ret.nodes.find((n) => n.id === e.serviceId)!.type === "app" ? "ports" : "env_var",
					target: n.id,
					targetHandle: "tcp",
				},
				{
					id: uuidv4(),
					source: n.id,
					sourceHandle: "subdomain",
					target: n.data.network!,
					targetHandle: "subdomain",
				},
			];
		});
	});
	const ingressEdges = [...(serviceGateways || [])].flatMap((n): Edge[] => {
		return [
			{
				id: uuidv4(),
				source: n.data.https!.serviceId,
				sourceHandle: "ports",
				target: n.id,
				targetHandle: "https",
			},
			{
				id: uuidv4(),
				source: n.id,
				sourceHandle: "subdomain",
				target: n.data.network!,
				targetHandle: "subdomain",
			},
		];
	});
	const repoEdges = (services || [])
		.map((s): Edge | null => {
			if (s.data.repository == null) {
				return null;
			}
			return {
				id: uuidv4(),
				source: s.data.repository!.repoNodeId!,
				sourceHandle: "repository",
				target: s.id,
				targetHandle: "repository",
			};
		})
		.filter((e) => e != null);
	ret.edges = [...repoEdges, ...envVarEdges, ...exposureEdges, ...ingressEdges];
	return ret;
}
