import { v4 as uuidv4 } from "uuid";
import { NodeRect } from "./node-rect";
import { useStateStore, nodeLabel, AppState, nodeIsConnectable, useEnv, useGithubRepositories } from "@/lib/state";
import { ServiceNode, ServiceTypes, GatewayHttpsNode, GatewayTCPNode, BoundEnvVar, AppNode, GithubNode } from "config";
import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { useForm, EventType, DeepPartial } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormField, FormItem, FormMessage, FormLabel } from "./ui/form";
import { Button } from "./ui/button";
import { Handle, Position, useNodes } from "@xyflow/react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
import { Textarea } from "./ui/textarea";
import { Input } from "./ui/input";
import { Switch } from "./ui/switch";
import { Label } from "./ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
import { Code, Container, Network, Pencil, Variable } from "lucide-react";
import { Badge } from "./ui/badge";
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "./ui/accordion";
import { Name } from "./node-name";
import { NodeDetailsProps } from "@/lib/types";
import { Gateway } from "@/Gateways";

export function NodeApp(node: ServiceNode) {
	const { id, selected } = node;
	const isConnectablePorts = useMemo(() => nodeIsConnectable(node, "ports"), [node]);
	const isConnectableRepository = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
	return (
		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
			<div style={{ padding: "10px 20px" }}>
				{nodeLabel(node)}
				<Handle
					id="repository"
					type={"target"}
					position={Position.Left}
					isConnectableStart={isConnectableRepository}
					isConnectableEnd={isConnectableRepository}
					isConnectable={isConnectableRepository}
				/>
				<Handle
					id="ports"
					type={"source"}
					position={Position.Top}
					isConnectableStart={isConnectablePorts}
					isConnectableEnd={isConnectablePorts}
					isConnectable={isConnectablePorts}
				/>
				<Handle
					id="env_var"
					type={"target"}
					position={Position.Bottom}
					isConnectableStart={true}
					isConnectableEnd={true}
					isConnectable={true}
				/>
			</div>
		</NodeRect>
	);
}

const schema = z.object({
	name: z.string().min(1, "requried"),
	type: z.enum(ServiceTypes),
});

const sourceSchema = z.object({
	id: z.string().min(1, "required"),
	branch: z.string(),
	rootDir: z.string(),
});

const devSchema = z.object({
	enabled: z.boolean(),
});

const exposeSchema = z.object({
	network: z.string().min(1, "reqired"),
	subdomain: z.string().min(1, "required"),
});

const agentSchema = z.object({
	model: z.enum(["gemini", "claude"]),
	apiKey: z.string().optional(),
});

export function NodeAppDetails({ node, disabled, showName = true, isOverview = false }: NodeDetailsProps<ServiceNode>) {
	const { data } = node;
	return (
		<>
			{showName ? <Name node={node} disabled={disabled} /> : null}
			<Tabs defaultValue="runtime">
				<TabsList className="w-full flex flex-row justify-between">
					<TabsTrigger value="runtime">
						{isOverview ? (
							<div className="flex flex-row gap-1 items-center">
								<Container /> Runtime
							</div>
						) : (
							<TooltipProvider>
								<Tooltip>
									<TooltipTrigger>
										<Container />
									</TooltipTrigger>
									<TooltipContent>Runtime</TooltipContent>
								</Tooltip>
							</TooltipProvider>
						)}
					</TabsTrigger>
					<TabsTrigger value="ports">
						{isOverview ? (
							<div className="flex flex-row gap-1 items-center">
								<Network /> Ports
								<Badge className="rounded-full">{data.ports?.length ?? 0}</Badge>
							</div>
						) : (
							<TooltipProvider>
								<Tooltip>
									<TooltipTrigger className="flex flex-row gap-1 items-center">
										<Network />
									</TooltipTrigger>
									<TooltipContent>
										Ports{" "}
										<Badge variant="secondary" className="rounded-full">
											{data.ports?.length ?? 0}
										</Badge>
									</TooltipContent>
								</Tooltip>
							</TooltipProvider>
						)}
					</TabsTrigger>
					<TabsTrigger value="vars">
						{isOverview ? (
							<div className="flex flex-row gap-1 items-center">
								<Variable /> Variables
								<Badge className="rounded-full">{data.envVars?.length ?? 0}</Badge>
							</div>
						) : (
							<TooltipProvider>
								<Tooltip>
									<TooltipTrigger className="flex flex-row gap-1 items-center">
										<Variable />
									</TooltipTrigger>
									<TooltipContent>
										Variables{" "}
										<Badge variant="secondary" className="rounded-full">
											{data.envVars?.length ?? 0}
										</Badge>
									</TooltipContent>
								</Tooltip>
							</TooltipProvider>
						)}
					</TabsTrigger>
					{node.data.type !== "sketch:latest" && (
						<TabsTrigger value="dev">
							{isOverview ? (
								<div className="flex flex-row gap-1 items-center">
									<Code /> Dev
								</div>
							) : (
								<TooltipProvider>
									<Tooltip>
										<TooltipTrigger className="flex flex-row gap-1 items-center">
											<Code />
										</TooltipTrigger>
										<TooltipContent>Dev</TooltipContent>
									</Tooltip>
								</TooltipProvider>
							)}
						</TabsTrigger>
					)}
				</TabsList>
				<TabsContent value="runtime">
					<Runtime node={node} disabled={disabled} />
				</TabsContent>
				<TabsContent value="ports">
					<Ports node={node} disabled={disabled} />
				</TabsContent>
				<TabsContent value="vars">
					<EnvVars node={node} disabled={disabled} />
				</TabsContent>
				{node.data.type !== "sketch:latest" && (
					<TabsContent value="dev">
						<Dev node={node} disabled={disabled} />
					</TabsContent>
				)}
			</Tabs>
		</>
	);
}

function Runtime({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const store = useStateStore();
	const form = useForm<z.infer<typeof schema>>({
		resolver: zodResolver(schema),
		mode: "onChange",
		defaultValues: {
			name: data.label,
			type: data.type,
		},
	});
	useEffect(() => {
		const sub = form.watch(
			(
				value: DeepPartial<z.infer<typeof schema>>,
				{ name, type }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
			) => {
				if (type !== "change") {
					return;
				}
				switch (name) {
					case "name":
						if (!value.name) {
							break;
						}
						store.updateNodeData<"app">(id, {
							label: value.name,
						});
						break;
					case "type":
						if (!value.type) {
							break;
						}
						store.updateNodeData<"app">(id, {
							type: value.type,
						});
						break;
				}
			},
		);
		return () => sub.unsubscribe();
	}, [id, form, store]);
	const [typeProps, setTypeProps] = useState({});
	useEffect(() => {
		if (data.activeField === "type") {
			setTypeProps({
				open: true,
				onOpenChange: () => store.updateNodeData(id, { activeField: undefined }),
			});
		} else {
			setTypeProps({});
		}
	}, [id, data, store, setTypeProps]);
	const setPreBuildCommands = useCallback(
		(e: React.ChangeEvent<HTMLTextAreaElement>) => {
			store.updateNodeData<"app">(id, {
				preBuildCommands: e.currentTarget.value,
			});
		},
		[id, store],
	);
	const agentForm = useForm<z.infer<typeof agentSchema>>({
		resolver: zodResolver(agentSchema),
		mode: "onChange",
		defaultValues: {
			apiKey: data.model?.apiKey,
			model: data.model?.name,
		},
	});
	useEffect(() => {
		const sub = agentForm.watch((value, { name }: { name?: keyof z.infer<typeof agentSchema> | undefined }) => {
			switch (name) {
				case "model":
					agentForm.setValue("apiKey", "", { shouldDirty: true });
					store.updateNodeData<"app">(id, {
						model: {
							name: value.model,
							apiKey: undefined,
						},
					});
					break;
				case "apiKey":
					store.updateNodeData<"app">(id, {
						model: {
							name: data.model?.name,
							apiKey: value.apiKey,
						},
					});
					break;
			}
		});
		return () => sub.unsubscribe();
	}, [id, agentForm, store, data]);
	return (
		<>
			<SourceRepo node={node} disabled={disabled} />
			{node.data.type !== "sketch:latest" && (
				<Form {...form}>
					<form className="space-y-2">
						<Label>Container Image</Label>
						<FormField
							control={form.control}
							name="type"
							render={({ field }) => (
								<FormItem>
									<Select
										onValueChange={field.onChange}
										value={field.value || ""}
										{...typeProps}
										disabled={disabled}
									>
										<FormControl>
											<SelectTrigger>
												<SelectValue />
											</SelectTrigger>
										</FormControl>
										<SelectContent>
											{ServiceTypes.filter((t) => t !== "sketch:latest").map((t) => (
												<SelectItem key={t} value={t}>
													{t}
												</SelectItem>
											))}
										</SelectContent>
									</Select>
									<FormMessage />
								</FormItem>
							)}
						/>
					</form>
				</Form>
			)}
			{node.data.type === "sketch:latest" && (
				<Form {...agentForm}>
					<form className="space-y-2">
						<FormField
							control={agentForm.control}
							name="model"
							render={({ field }) => (
								<FormItem>
									<FormLabel>AI Model</FormLabel>
									<Select
										onValueChange={field.onChange}
										defaultValue={field.value}
										disabled={disabled}
									>
										<FormControl>
											<SelectTrigger>
												<SelectValue placeholder="Select a model" />
											</SelectTrigger>
										</FormControl>
										<SelectContent>
											<SelectItem value="gemini">Gemini</SelectItem>
											<SelectItem value="claude">Claude</SelectItem>
										</SelectContent>
									</Select>
									<FormMessage />
								</FormItem>
							)}
						/>
						<Label>API Key</Label>
						<FormField
							control={agentForm.control}
							name="apiKey"
							render={({ field }) => (
								<FormItem>
									<FormControl>
										<Input
											type="password"
											placeholder="Override AI Model API key"
											{...field}
											value={field.value || ""}
											disabled={disabled}
										/>
									</FormControl>
									<FormMessage />
								</FormItem>
							)}
						/>
					</form>
				</Form>
			)}
			{node.data.type !== "sketch:latest" && (
				<>
					<Label>Pre-Build Commands</Label>
					<Textarea
						placeholder="new line separated list of commands to run before running the service"
						value={data.preBuildCommands}
						onChange={setPreBuildCommands}
						disabled={disabled}
					/>
				</>
			)}
		</>
	);
}

function Ports({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const store = useStateStore();
	const nodes = useNodes<AppNode>();
	const [portIngresses, setPortIngresses] = useState<Record<string, string[]>>({});

	const httpsGateways = useMemo(
		() => nodes.filter((n): n is GatewayHttpsNode => n.type === "gateway-https"),
		[nodes],
	);

	useEffect(() => {
		if (!data.ports) {
			setPortIngresses({});
			return;
		}
		const newIngresses: Record<string, string[]> = {};
		for (const port of data.ports) {
			newIngresses[port.id] = [];
		}
		for (const gateway of httpsGateways) {
			const https = gateway.data.https;
			if (https && https.serviceId === id && https.portId && gateway.data.network && gateway.data.subdomain) {
				const url = `https://${gateway.data.subdomain}.${gateway.data.network}`;
				if (newIngresses[https.portId]) {
					newIngresses[https.portId].push(url);
				} else {
					newIngresses[https.portId] = [url];
				}
			}
		}
		setPortIngresses(newIngresses);
		console.log(newIngresses);
	}, [id, data.ports, httpsGateways]);

	const [name, setName] = useState("");
	const [value, setValue] = useState("");
	const onSubmit = useCallback(() => {
		const portId = uuidv4();
		store.updateNodeData<"app">(id, {
			ports: (data.ports || []).concat({
				id: portId,
				name: name.toUpperCase(),
				value: Number(value),
			}),
			envVars: (data.envVars || []).concat(
				{
					id: uuidv4(),
					source: null,
					portId,
					name: `DODO_PORT_${name.toUpperCase()}`,
				},
				{
					id: uuidv4(),
					source: null,
					portId,
					name: `DODO_PORT_${name.toUpperCase()}`,
					alias: name.toUpperCase(),
				},
			),
		});
		setName("");
		setValue("");
	}, [id, data, store, name, value, setName, setValue]);
	const removePort = useCallback(
		(portId: string) => {
			// TODO(gio): this is ugly
			const tcpRemoved = new Set<string>();
			store.setEdges(
				store.edges.filter((e) => {
					if (e.source !== id || e.sourceHandle !== "ports") {
						return true;
					}
					const tn = store.nodes.find((n) => n.id == e.target)!;
					if (e.targetHandle === "https") {
						const t = tn as GatewayHttpsNode;
						if (t.data.https?.serviceId === id && t.data.https.portId === portId) {
							return false;
						}
					}
					if (e.targetHandle === "tcp") {
						const t = tn as GatewayTCPNode;
						if (tcpRemoved.has(t.id)) {
							return true;
						}
						if (t.data.exposed.find((e) => e.serviceId === id && e.portId === portId)) {
							tcpRemoved.add(t.id);
							return false;
						}
					}
					if (e.targetHandle === "env_var") {
						if (
							tn &&
							(tn.data.envVars || []).find(
								(ev) => ev.source === id && "portId" in ev && ev.portId === portId,
							)
						) {
							return false;
						}
					}
					return true;
				}),
			);
			store.nodes
				.filter(
					(n) =>
						n.type === "gateway-https" &&
						n.data.https &&
						n.data.https.serviceId === id &&
						n.data.https.portId === portId,
				)
				.forEach((n) => {
					store.updateNodeData<"gateway-https">(n.id, {
						https: undefined,
					});
				});
			store.nodes
				.filter((n) => n.type === "gateway-tcp")
				.forEach((n) => {
					const filtered = n.data.exposed.filter((e) => {
						if (e.serviceId === id && e.portId === portId) {
							return false;
						} else {
							return true;
						}
					});
					if (filtered.length != n.data.exposed.length) {
						store.updateNodeData<"gateway-tcp">(n.id, {
							exposed: filtered,
						});
					}
				});
			store.nodes
				.filter((n) => n.type === "app" && n.data.envVars)
				.forEach((n) => {
					store.updateNodeData<"app">(n.id, {
						envVars: n.data.envVars.filter((ev) => {
							if (ev.source === id && "portId" in ev && ev.portId === portId) {
								return false;
							}
							return true;
						}),
					});
				});
			store.updateNodeData<"app">(id, {
				ports: (data.ports || []).filter((p) => p.id !== portId),
				envVars: (data.envVars || []).filter(
					(ev) => !(ev.source === null && "portId" in ev && ev.portId === portId),
				),
			});
		},
		[id, data, store],
	);
	return (
		<div className="flex flex-col gap-1">
			<div className="grid grid-cols-[1fr_1fr_auto] gap-1">
				{data &&
					data.ports &&
					data.ports.map((p) => (
						<>
							<div className="contents">
								<div className="flex items-center px-3">{p.name.toUpperCase()}</div>
								<div className="flex items-center px-3">{p.value}</div>
								<div className="flex items-center">
									<Button
										variant="destructive"
										className="w-full"
										onClick={() => removePort(p.id)}
										disabled={disabled}
									>
										Remove
									</Button>
								</div>
							</div>
							{portIngresses[p.id]?.length > 0 && (
								<div key={p.id} className="col-span-full pl-6">
									{portIngresses[p.id].map((url) => (
										<Gateway g={{ type: "https", address: url, name: p.name }} />
									))}
								</div>
							)}
						</>
					))}
				<div>
					<Input
						placeholder="name"
						className="uppercase w-0 min-w-full"
						disabled={disabled}
						value={name}
						onChange={(e) => setName(e.target.value)}
					/>
				</div>
				<div>
					<Input
						placeholder="0"
						className="w-0 min-w-full"
						disabled={disabled}
						value={value}
						onChange={(e) => setValue(e.target.value)}
					/>
				</div>
				<div>
					<Button type="submit" className="w-full" disabled={disabled} onClick={onSubmit}>
						Add
					</Button>
				</div>
			</div>
		</div>
	);
}

function EnvVars({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const store = useStateStore();
	const [name, setName] = useState("");
	const [value, setValue] = useState("");

	const addEnvVar = useCallback(() => {
		if (!name.trim() || !value.trim()) return;
		store.updateNodeData<"app">(id, {
			envVars: (data.envVars || []).concat({
				id: uuidv4(),
				source: null,
				name: name.toUpperCase(),
				value: value,
			}),
		});
		setName("");
		setValue("");
	}, [id, data, store, name, value]);

	const removeEnvVar = useCallback(
		(varId: string) => {
			store.updateNodeData<"app">(id, {
				envVars: (data.envVars || []).filter((v) => v.id !== varId),
			});
		},
		[id, data, store],
	);

	const editValueEnvVar = useCallback(
		(varId: string) => {
			if (disabled) return;
			store.updateNodeData<"app">(id, {
				envVars: (data.envVars || []).map((v) => (v.id === varId ? { ...v, isEditting: true } : v)),
			});
		},
		[id, data, store, disabled],
	);

	const saveValueEnvVar = useCallback(
		(varId: string, newName: string, newValue: string) => {
			store.updateNodeData<"app">(id, {
				envVars: (data.envVars || []).map((v) => {
					if (v.id === varId) {
						return { ...v, name: newName.toUpperCase(), value: newValue, isEditting: false };
					}
					return v;
				}),
			});
		},
		[id, data, store],
	);

	const editAlias = useCallback(
		(e: BoundEnvVar) => {
			return () => {
				if (disabled) {
					return;
				}
				store.updateNodeData(id, {
					...data,
					envVars: data.envVars!.map((o) => {
						if (o.id !== e.id) {
							return o;
						} else
							return {
								...o,
								isEditting: true,
							};
					}),
				});
			};
		},
		[id, data, store, disabled],
	);

	const saveAlias = useCallback(
		(e: BoundEnvVar, value: string, store: AppState) => {
			store.updateNodeData(id, {
				...data,
				envVars: data.envVars!.map((o) => {
					if (o.id !== e.id) {
						return o;
					}
					if (value) {
						if ("name" in o && value.toUpperCase() === o.name.toUpperCase()) {
							return {
								...o,
								isEditting: false,
								alias: undefined,
							};
						} else {
							return {
								...o,
								isEditting: false,
								alias: value.toUpperCase(),
							};
						}
					}
					if ("alias" in o) {
						const { alias: _, ...rest } = o;
						return {
							...rest,
							isEditting: false,
						};
					}
					return {
						...o,
						isEditting: false,
					};
				}),
			});
		},
		[id, data],
	);

	const saveAliasOnEnter = useCallback(
		(e: BoundEnvVar) => {
			return (event: KeyboardEvent<HTMLInputElement>) => {
				if (event.key === "Enter") {
					saveAlias(e, event.currentTarget.value, store);
				} else if (event.key === "Escape") {
					store.updateNodeData(id, {
						...data,
						envVars: data.envVars!.map((o) => (o.id === e.id ? { ...o, isEditting: false } : o)),
					});
				}
			};
		},
		[store, saveAlias, id, data],
	);

	const saveAliasOnBlur = useCallback(
		(e: BoundEnvVar) => {
			return (event: FocusEvent<HTMLInputElement>) => {
				saveAlias(e, event.currentTarget.value, store);
			};
		},
		[store, saveAlias],
	);

	return (
		<div className="flex flex-col gap-1">
			<div className="grid grid-cols-[auto_1fr_1fr_auto] gap-1">
				{data?.envVars?.map((v) => {
					if ("value" in v) {
						if (v.isEditting) {
							return (
								<div key={v.id} className="contents">
									<Input
										className="uppercase col-start-2"
										defaultValue={v.name}
										onKeyUp={(e) => {
											if (e.key === "Enter") {
												const nameInput = e.currentTarget;
												const valueInput = nameInput.parentElement?.querySelector(
													'input[placeholder="Value"]',
												) as HTMLInputElement;
												if (valueInput) {
													saveValueEnvVar(v.id, nameInput.value, valueInput.value);
												}
											} else if (e.key === "Escape") {
												store.updateNodeData(id, {
													...data,
													envVars: data.envVars!.map((o) =>
														o.id === v.id ? { ...o, isEditting: false } : o,
													),
												});
											}
										}}
										autoFocus
										disabled={disabled}
									/>
									<Input
										placeholder="Value"
										defaultValue={v.value}
										onKeyUp={(e) => {
											if (e.key === "Enter") {
												const valueInput = e.currentTarget;
												const nameInput = valueInput.parentElement?.querySelector(
													'input:not([placeholder="Value"])',
												) as HTMLInputElement;
												if (nameInput) {
													saveValueEnvVar(v.id, nameInput.value, valueInput.value);
												}
											} else if (e.key === "Escape") {
												store.updateNodeData(id, {
													...data,
													envVars: data.envVars!.map((o) =>
														o.id === v.id ? { ...o, isEditting: false } : o,
													),
												});
											}
										}}
										disabled={disabled}
									/>
									<Button
										variant="destructive"
										size="sm"
										onClick={() => removeEnvVar(v.id)}
										disabled={disabled}
									>
										Remove
									</Button>
								</div>
							);
						}
						return (
							<div
								key={v.id}
								className={`contents ${disabled ? "" : "cursor-text"}`}
								onClick={() => editValueEnvVar(v.id)}
							>
								<div>{!disabled && <Pencil className="w-4 h-4" />}</div>
								<div className={`${disabled ? "col-span-2" : ""} col-start-2`}>{v.name}</div>
								<div>{v.value}</div>
								<Button
									variant="destructive"
									size="sm"
									onClick={(e) => {
										e.stopPropagation();
										removeEnvVar(v.id);
									}}
									disabled={disabled}
								>
									Remove
								</Button>
							</div>
						);
					}
					if ("name" in v) {
						const value = "alias" in v ? v.alias : v.name;
						if (v.isEditting) {
							return (
								<Input
									type="text"
									className="uppercase col-start-2 col-span-3"
									defaultValue={value}
									onKeyUp={saveAliasOnEnter(v)}
									onBlur={saveAliasOnBlur(v)}
									autoFocus={true}
									disabled={disabled}
								/>
							);
						}
						return (
							<div
								key={v.id}
								onClick={editAlias(v)}
								className={`contents ${disabled ? "" : "cursor-text"}`}
							>
								{!disabled && <Pencil className="w-4 h-4" />}
								<div className="col-start-2 col-span-3">
									<TooltipProvider>
										<Tooltip>
											<TooltipTrigger className="uppercase">{value}</TooltipTrigger>
											<TooltipContent>{v.name}</TooltipContent>
										</Tooltip>
									</TooltipProvider>
								</div>
							</div>
						);
					}
					return null;
				})}
				{!disabled && (
					<div className="contents">
						<Input
							placeholder="Name"
							className="uppercase col-start-2"
							value={name}
							onChange={(e) => setName(e.target.value)}
							disabled={disabled}
						/>
						<Input
							placeholder="Value"
							value={value}
							onChange={(e) => setValue(e.target.value)}
							disabled={disabled}
						/>
						<Button onClick={addEnvVar} disabled={disabled || !name.trim() || !value.trim()}>
							Add
						</Button>
					</div>
				)}
			</div>
		</div>
	);
}

function Dev({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const env = useEnv();
	const store = useStateStore();
	const devForm = useForm<z.infer<typeof devSchema>>({
		resolver: zodResolver(devSchema),
		mode: "onChange",
		defaultValues: {
			enabled: data.dev ? data.dev.enabled : false,
		},
	});
	useEffect(() => {
		const sub = devForm.watch((value, { name }) => {
			if (name === "enabled") {
				if (value.enabled) {
					const csGateway: Omit<GatewayHttpsNode, "position"> = {
						id: uuidv4(),
						type: "gateway-https",
						data: {
							readonly: true,
							https: {
								serviceId: id,
								portId: `${id}-code-server`,
							},
							network: data.dev?.expose?.network,
							subdomain: data.dev?.expose?.subdomain,
							label: "",
							envVars: [],
							ports: [],
						},
					};
					const sshGateway: Omit<GatewayTCPNode, "position"> = {
						id: uuidv4(),
						type: "gateway-tcp",
						data: {
							readonly: true,
							exposed: [
								{
									serviceId: id,
									portId: `${id}-ssh`,
								},
							],
							network: data.dev?.expose?.network,
							subdomain: data.dev?.expose?.subdomain,
							label: "",
							envVars: [],
							ports: [],
						},
					};
					store.addNode(csGateway);
					store.addNode(sshGateway);
					store.updateNodeData<"app">(id, {
						dev: {
							enabled: true,
							expose: data.dev?.expose,
							codeServerNodeId: csGateway.id,
							sshNodeId: sshGateway.id,
						},
						ports: (data.ports || []).concat(
							{
								id: `${id}-code-server`,
								name: "code-server",
								value: 9090,
							},
							{
								id: `${id}-ssh`,
								name: "ssh",
								value: 22,
							},
						),
					});
					let edges = store.edges.concat([
						{
							id: uuidv4(),
							source: id,
							sourceHandle: "ports",
							target: csGateway.id,
							targetHandle: "https",
						},
						{
							id: uuidv4(),
							source: id,
							sourceHandle: "ports",
							target: sshGateway.id,
							targetHandle: "tcp",
						},
					]);
					if (data.dev?.expose?.network !== undefined) {
						edges = edges.concat([
							{
								id: uuidv4(),
								source: csGateway.id,
								sourceHandle: "subdomain",
								target: data.dev.expose.network,
								targetHandle: "subdomain",
							},
							{
								id: uuidv4(),
								source: sshGateway.id,
								sourceHandle: "subdomain",
								target: data.dev.expose.network,
								targetHandle: "subdomain",
							},
						]);
					}
					store.setEdges(edges);
				} else {
					const { dev } = data;
					if (dev?.enabled) {
						store.setNodes(
							store.nodes.filter((n) => n.id !== dev.codeServerNodeId && n.id !== dev.sshNodeId),
						);
						store.setEdges(
							store.edges.filter((e) => e.target !== dev.codeServerNodeId && e.target !== dev.sshNodeId),
						);
					}
					store.updateNodeData<"app">(id, {
						dev: {
							enabled: false,
							expose: dev?.expose,
						},
						ports: (data.ports || []).filter((p) => p.name !== "code-server" && p.name !== "ssh"),
					});
				}
			}
		});
		return () => sub.unsubscribe();
	}, [id, data, devForm, store]);
	const exposeForm = useForm<z.infer<typeof exposeSchema>>({
		resolver: zodResolver(exposeSchema),
		mode: "onChange",
		defaultValues: {
			network: data.dev?.expose?.network,
			subdomain: data.dev?.expose?.subdomain,
		},
	});
	useEffect(() => {
		const sub = exposeForm.watch(
			(
				value: DeepPartial<z.infer<typeof exposeSchema>>,
				{ name }: { name?: keyof z.infer<typeof exposeSchema> | undefined; type?: EventType | undefined },
			) => {
				const { dev } = data;
				if (!dev?.enabled) {
					return;
				}
				if (name === "network") {
					let edges = store.edges;
					if (dev.enabled && dev.expose?.network !== undefined) {
						edges = edges.filter((e) => {
							if (
								(e.source === dev.codeServerNodeId || e.source === dev.sshNodeId) &&
								e.sourceHandle === "subdomain" &&
								e.target === dev.expose?.network &&
								e.targetHandle === "subdomain"
							) {
								return false;
							} else {
								return true;
							}
						});
					}
					if (value.network !== undefined) {
						edges = edges.concat(
							{
								id: uuidv4(),
								source: dev.codeServerNodeId,
								sourceHandle: "subdomain",
								target: value.network,
								targetHandle: "subdomain",
							},
							{
								id: uuidv4(),
								source: dev.sshNodeId,
								sourceHandle: "subdomain",
								target: value.network,
								targetHandle: "subdomain",
							},
						);
					}
					store.setEdges(edges);
					store.updateNodeData<"app">(id, {
						dev: {
							...dev,
							expose: {
								network: value.network,
								subdomain: dev.expose?.subdomain,
							},
						},
					});
					store.updateNodeData<"gateway-https">(dev.codeServerNodeId, { network: value.network });
					store.updateNodeData<"gateway-tcp">(dev.sshNodeId, { network: value.network });
				} else if (name === "subdomain") {
					store.updateNodeData<"app">(id, {
						dev: {
							...dev,
							expose: {
								network: dev.expose?.network,
								subdomain: value.subdomain,
							},
						},
					});
					store.updateNodeData<"gateway-https">(dev.codeServerNodeId, { subdomain: value.subdomain });
					store.updateNodeData<"gateway-tcp">(dev.sshNodeId, { subdomain: value.subdomain });
				}
			},
		);
		return () => sub.unsubscribe();
	}, [id, data, exposeForm, store]);
	return (
		<>
			<Form {...devForm}>
				<form className="space-y-2">
					<FormField
						control={devForm.control}
						name="enabled"
						render={({ field }) => (
							<FormItem>
								<div className="flex flex-row gap-1 items-center">
									<Switch
										id="devEnabled"
										onCheckedChange={field.onChange}
										checked={field.value}
										disabled={disabled}
									/>
									<Label htmlFor="devEnabled">Dev VM</Label>
								</div>
								<FormMessage />
							</FormItem>
						)}
					/>
				</form>
			</Form>
			{data.dev && data.dev.enabled && (
				<Form {...exposeForm}>
					<form className="space-y-2">
						<Label>Network</Label>
						<FormField
							control={exposeForm.control}
							name="network"
							render={({ field }) => (
								<FormItem>
									<Select
										onValueChange={field.onChange}
										value={field.value || ""}
										disabled={disabled}
									>
										<FormControl>
											<SelectTrigger>
												<SelectValue />
											</SelectTrigger>
										</FormControl>
										<SelectContent>
											{env.networks.map((n) => (
												<SelectItem
													key={n.name}
													value={n.domain}
												>{`${n.name} - ${n.domain}`}</SelectItem>
											))}
										</SelectContent>
									</Select>
									<FormMessage />
								</FormItem>
							)}
						/>
						<Label>Subdomain</Label>
						<FormField
							control={exposeForm.control}
							name="subdomain"
							render={({ field }) => (
								<FormItem>
									<FormControl>
										<Input {...field} disabled={disabled} />
									</FormControl>
									<FormMessage />
								</FormItem>
							)}
						/>
					</form>
				</Form>
			)}
		</>
	);
}

function SourceRepo({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const store = useStateStore();
	const nodes = useNodes<AppNode>();
	const repo = useMemo(() => {
		return nodes
			.filter((n): n is GithubNode => n.type === "github")
			.find((n) => n.id === data.repository?.repoNodeId);
	}, [nodes, data.repository?.repoNodeId]);
	const repos = useGithubRepositories();
	const sourceForm = useForm<z.infer<typeof sourceSchema>>({
		resolver: zodResolver(sourceSchema),
		mode: "onChange",
		defaultValues: {
			id: data?.repository?.id?.toString(),
			branch: data.repository && "branch" in data.repository ? data.repository.branch : undefined,
			rootDir: data.repository && "rootDir" in data.repository ? data.repository.rootDir : undefined,
		},
	});
	useEffect(() => {
		const sub = sourceForm.watch(
			(
				value: DeepPartial<z.infer<typeof sourceSchema>>,
				{ name }: { name?: keyof z.infer<typeof sourceSchema> | undefined; type?: EventType | undefined },
			) => {
				if (name === "id") {
					const newRepoId = value.id ? parseInt(value.id, 10) : undefined;
					if (!newRepoId) return;

					const oldGithubNodeId = data.repository?.repoNodeId;
					const selectedRepo = repos.find((r) => r.id === newRepoId);

					if (!selectedRepo) return;

					// If a node for the selected repo already exists, connect to it.
					const existingNodeForSelectedRepo = nodes
						.filter((n): n is GithubNode => n.type === "github")
						.find((n) => n.data.repository?.id === selectedRepo.id);

					if (existingNodeForSelectedRepo) {
						let { nodes, edges } = store;
						if (oldGithubNodeId) {
							edges = edges.filter(
								(e) =>
									!(
										e.target === id &&
										e.source === oldGithubNodeId &&
										e.targetHandle === "repository"
									),
							);
						}
						edges = edges.concat({
							id: uuidv4(),
							source: existingNodeForSelectedRepo.id,
							sourceHandle: "repository",
							target: id,
							targetHandle: "repository",
						});
						nodes = nodes.map((n) => {
							if (n.id !== id) {
								return n;
							} else {
								const sn = n as ServiceNode;
								return {
									...sn,
									data: {
										...sn.data,
										repository: {
											...sn.data.repository,
											id: newRepoId,
											repoNodeId: existingNodeForSelectedRepo.id,
										},
									},
								};
							}
						});
						if (oldGithubNodeId && oldGithubNodeId !== existingNodeForSelectedRepo.id) {
							const isOldNodeStillUsed = edges.some(
								(e) => e.source === oldGithubNodeId && e.sourceHandle === "repository",
							);
							if (!isOldNodeStillUsed) {
								nodes = nodes.filter((n) => n.id !== oldGithubNodeId);
							}
						}
						store.setNodes(nodes);
						store.setEdges(edges);
						return;
					}

					// No node for selected repo, decide whether to update old node or create a new one.
					if (oldGithubNodeId) {
						const isOldNodeShared =
							store.edges.filter(
								(e) =>
									e.source === oldGithubNodeId && e.target !== id && e.sourceHandle === "repository",
							).length > 0;

						if (!isOldNodeShared) {
							// Update old node
							store.updateNodeData<"github">(oldGithubNodeId, {
								repository: {
									id: selectedRepo.id,
									sshURL: selectedRepo.ssh_url,
									fullName: selectedRepo.full_name,
								},
								label: selectedRepo.full_name,
							});
							store.updateNodeData<"app">(id, {
								repository: {
									...data.repository,
									id: newRepoId,
								},
							});
						} else {
							// Create new node because old one is shared
							const newGithubNodeId = uuidv4();
							store.addNode({
								id: newGithubNodeId,
								type: "github",
								data: {
									repository: {
										id: selectedRepo.id,
										sshURL: selectedRepo.ssh_url,
										fullName: selectedRepo.full_name,
									},
									label: selectedRepo.full_name,
									envVars: [],
									ports: [],
								},
							});

							let edges = store.edges;
							// remove old edge
							edges = edges.filter(
								(e) =>
									!(
										e.target === id &&
										e.source === oldGithubNodeId &&
										e.targetHandle === "repository"
									),
							);
							// add new edge
							edges = edges.concat({
								id: uuidv4(),
								source: newGithubNodeId,
								sourceHandle: "repository",
								target: id,
								targetHandle: "repository",
							});
							store.setEdges(edges);
							store.updateNodeData<"app">(id, {
								repository: {
									...data.repository,
									id: newRepoId,
									repoNodeId: newGithubNodeId,
								},
							});
						}
					} else {
						// No old github node, so create a new one
						const newGithubNodeId = uuidv4();
						store.addNode({
							id: newGithubNodeId,
							type: "github",
							data: {
								repository: {
									id: selectedRepo.id,
									sshURL: selectedRepo.ssh_url,
									fullName: selectedRepo.full_name,
								},
								label: selectedRepo.full_name,
								envVars: [],
								ports: [],
							},
						});
						store.setEdges(
							store.edges.concat({
								id: uuidv4(),
								source: newGithubNodeId,
								sourceHandle: "repository",
								target: id,
								targetHandle: "repository",
							}),
						);
						store.updateNodeData<"app">(id, {
							repository: {
								...data.repository,
								id: newRepoId,
								repoNodeId: newGithubNodeId,
							},
						});
					}
				} else if (name === "branch") {
					store.updateNodeData<"app">(id, {
						repository: {
							...data?.repository,
							branch: value.branch,
						},
					});
				} else if (name === "rootDir") {
					store.updateNodeData<"app">(id, {
						repository: {
							...data?.repository,
							rootDir: value.rootDir,
						},
					});
				}
			},
		);
		return () => sub.unsubscribe();
	}, [id, data, sourceForm, store, nodes, repos]);
	const [isExpanded, setIsExpanded] = useState(false);
	// useEffect(() => {
	// 	if (data.repository === undefined) {
	// 		setIsExpanded(true);
	// 	}
	// }, [data.repository, setIsExpanded]);
	console.log(data.repository, isExpanded, repo);
	return (
		<Accordion type="single" collapsible>
			<AccordionItem value="repository" className="border-none">
				<AccordionTrigger onClick={() => setIsExpanded(!isExpanded)}>
					Source {!isExpanded && repo !== undefined && repo.data.repository?.fullName}
				</AccordionTrigger>
				<AccordionContent className="px-1">
					<Form {...sourceForm}>
						<form className="space-y-2">
							<Label>Repository</Label>
							<FormField
								control={sourceForm.control}
								name="id"
								render={({ field }) => (
									<FormItem>
										<Select onValueChange={field.onChange} value={field.value} disabled={disabled}>
											<FormControl>
												<SelectTrigger>
													<SelectValue />
												</SelectTrigger>
											</FormControl>
											<SelectContent>
												{repos.map((r) => (
													<SelectItem
														key={r.id}
														value={r.id.toString()}
													>{`${r.full_name}`}</SelectItem>
												))}
											</SelectContent>
										</Select>
										<FormMessage />
									</FormItem>
								)}
							/>
							<Label>Branch</Label>
							<FormField
								control={sourceForm.control}
								name="branch"
								render={({ field }) => (
									<FormItem>
										<FormControl>
											<Input
												placeholder="master"
												className="lowercase"
												{...field}
												disabled={disabled}
											/>
										</FormControl>
										<FormMessage />
									</FormItem>
								)}
							/>
							<Label>Root Directory</Label>
							<FormField
								control={sourceForm.control}
								name="rootDir"
								render={({ field }) => (
									<FormItem>
										<FormControl>
											<Input placeholder="/" {...field} disabled={disabled} />
										</FormControl>
										<FormMessage />
									</FormItem>
								)}
							/>
						</form>
					</Form>
				</AccordionContent>
			</AccordionItem>
		</Accordion>
	);
}
