import { v4 as uuidv4 } from "uuid";
import { NodeRect } from "./node-rect";
import {
	useStateStore,
	nodeLabel,
	AppState,
	nodeIsConnectable,
	useEnv,
	useGithubRepositories,
	useMode,
} from "@/lib/state";
import {
	ServiceNode,
	ServiceTypes,
	GatewayHttpsNode,
	GatewayTCPNode,
	BoundEnvVar,
	AppNode,
	GithubNode,
	Machines,
	Machine,
	MachinesSchema,
} from "config";
import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState, useRef } 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 { Check, Code, Container, Copy, 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";
import { Port } from "config";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
import { useToast } from "@/hooks/use-toast";
import { LoaderCircle } from "lucide-react";

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

const devSchema = z.object({
	enabled: z.boolean(),
	mode: z.enum(["VM", "PROXY"]).optional(),
});

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(),
});

const proxySchema = z.object({
	address: z.string().min(1, "required"),
});

const portExposeSchema = z
	.object({
		type: z.enum(["https", "tcp"]),
		network: z.string().min(1, "Required"),
		subdomain: z.string().optional(),
	})
	.refine(
		(data) => {
			if (data.type === "https" || data.type === "tcp") {
				return !!data.subdomain && data.subdomain.length > 0;
			}
			return true;
		},
		{
			message: "Subdomain is required",
			path: ["subdomain"],
		},
	);

type PortExposeFormValues = z.infer<typeof portExposeSchema>;

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),
});

function ExposeForm({
	node,
	port,
	onDone,
	disabled,
}: {
	node: ServiceNode;
	port: Port;
	onDone: () => void;
	disabled?: boolean;
}) {
	const store = useStateStore();
	const nodes = useNodes<AppNode>();
	const env = useEnv();
	const form = useForm<PortExposeFormValues>({
		resolver: zodResolver(portExposeSchema),
		mode: "onChange",
		defaultValues: {
			type: "https",
		},
	});

	const onSubmit = (data: PortExposeFormValues) => {
		const networkNode = nodes.find((n) => n.type === "network" && n.data.domain === data.network);
		if (!networkNode) {
			// TODO: should show an error to the user
			return;
		}
		if (data.type === "https") {
			const newNode: Omit<GatewayHttpsNode, "position"> = {
				id: uuidv4(),
				type: "gateway-https",
				data: {
					https: {
						serviceId: node.id,
						portId: port.id,
					},
					network: data.network,
					subdomain: data.subdomain!,
					label: "",
					envVars: [],
					ports: [],
				},
			};
			store.addNode(newNode);
			store.setEdges(
				store.edges.concat(
					{
						id: uuidv4(),
						source: node.id,
						sourceHandle: "ports",
						target: newNode.id,
						targetHandle: "https",
					},
					{
						id: uuidv4(),
						source: newNode.id,
						sourceHandle: "subdomain",
						target: networkNode.id,
						targetHandle: "subdomain",
					},
				),
			);
		} else if (data.type === "tcp") {
			const existingGateway = nodes.find(
				(n): n is GatewayTCPNode =>
					n.type === "gateway-tcp" && n.data.network === data.network && n.data.subdomain === data.subdomain,
			);
			if (existingGateway) {
				store.updateNodeData<"gateway-tcp">(existingGateway.id, {
					exposed: [...existingGateway.data.exposed, { serviceId: node.id, portId: port.id }],
				});
				let edges = store.edges.concat({
					id: uuidv4(),
					source: node.id,
					sourceHandle: "ports",
					target: existingGateway.id,
					targetHandle: "tcp",
				});
				if (
					!edges.find(
						(e) =>
							e.source === existingGateway.id &&
							e.target === networkNode.id &&
							e.sourceHandle === "subdomain" &&
							e.targetHandle === "subdomain",
					)
				) {
					edges = edges.concat({
						id: uuidv4(),
						source: existingGateway.id,
						sourceHandle: "subdomain",
						target: networkNode.id,
						targetHandle: "subdomain",
					});
				}
				store.setEdges(edges);
			} else {
				const newNode: Omit<GatewayTCPNode, "position"> = {
					id: uuidv4(),
					type: "gateway-tcp",
					data: {
						exposed: [{ serviceId: node.id, portId: port.id }],
						network: data.network,
						subdomain: data.subdomain,
						label: "",
						envVars: [],
						ports: [],
					},
				};
				store.addNode(newNode);
				store.setEdges(
					store.edges.concat(
						{
							id: uuidv4(),
							source: node.id,
							sourceHandle: "ports",
							target: newNode.id,
							targetHandle: "tcp",
						},
						{
							id: uuidv4(),
							source: newNode.id,
							sourceHandle: "subdomain",
							target: networkNode.id,
							targetHandle: "subdomain",
						},
					),
				);
			}
		}
		onDone();
	};

	const type = form.watch("type");

	return (
		<Form {...form}>
			<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 border-t mt-2 pt-2">
				<FormField
					control={form.control}
					name="type"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Gateway Type</FormLabel>
							<Select onValueChange={field.onChange} defaultValue={field.value} disabled={disabled}>
								<FormControl>
									<SelectTrigger>
										<SelectValue placeholder="Select a type" />
									</SelectTrigger>
								</FormControl>
								<SelectContent>
									<SelectItem value="https">HTTPS</SelectItem>
									<SelectItem value="tcp">TCP</SelectItem>
								</SelectContent>
							</Select>
							<FormMessage />
						</FormItem>
					)}
				/>
				<FormField
					control={form.control}
					name="network"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Network</FormLabel>
							<Select onValueChange={field.onChange} defaultValue={field.value} disabled={disabled}>
								<FormControl>
									<SelectTrigger>
										<SelectValue placeholder="Select a network" />
									</SelectTrigger>
								</FormControl>
								<SelectContent>
									{env.networks.map((n) => (
										<SelectItem key={n.domain} value={n.domain}>
											{n.name} - {n.domain}
										</SelectItem>
									))}
								</SelectContent>
							</Select>
							<FormMessage />
						</FormItem>
					)}
				/>
				{(type === "https" || type === "tcp") && (
					<FormField
						control={form.control}
						name="subdomain"
						render={({ field }) => (
							<FormItem>
								<FormLabel>Subdomain</FormLabel>
								<FormControl>
									<Input placeholder="subdomain" {...field} disabled={disabled} />
								</FormControl>
								<FormMessage />
							</FormItem>
						)}
					/>
				)}
				<div className="flex justify-end gap-2">
					<Button type="button" variant="ghost" onClick={onDone} disabled={disabled}>
						Cancel
					</Button>
					<Button type="submit" disabled={disabled || !form.formState.isValid}>
						Expose
					</Button>
				</div>
			</form>
		</Form>
	);
}

export function NodeAppDetails({ node, disabled, showName = true, isOverview = false }: NodeDetailsProps<ServiceNode>) {
	const { data } = node;
	const defaultTab = useMemo(() => {
		if (data.dev?.enabled) {
			return "dev";
		}
		return "runtime";
	}, [data]);
	return (
		<>
			{showName ? <Name node={node} disabled={disabled} /> : null}
			<Tabs defaultValue={defaultTab}>
				<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} isOverview={isOverview} />
				</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,
	isOverview,
}: {
	node: ServiceNode;
	disabled?: boolean;
	isOverview?: boolean;
}): React.ReactNode {
	const { id, data } = node;
	const store = useStateStore();
	const nodes = useNodes<AppNode>();
	const [portIngresses, setPortIngresses] = useState<Record<string, string[]>>({});
	const [exposingPortId, setExposingPortId] = useState<string | null>(null);

	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 key={p.id} className="contents">
							<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 gap-1">
									{isOverview && (
										<Button
											variant="outline"
											onClick={() => setExposingPortId(p.id)}
											disabled={disabled}
										>
											Expose
										</Button>
									)}
									<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 key={url} g={{ type: "https", address: url, name: p.name }} />
									))}
								</div>
							)}
							{exposingPortId === p.id && (
								<Dialog open={true} onOpenChange={() => setExposingPortId(null)}>
									<DialogContent>
										<DialogHeader>
											<DialogTitle>
												Expose Port {p.name}:{p.value}
											</DialogTitle>
										</DialogHeader>
										<ExposeForm
											node={node}
											port={p}
											onDone={() => setExposingPortId(null)}
											disabled={disabled}
										/>
									</DialogContent>
								</Dialog>
							)}
						</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 mode = useMode();
	const env = useEnv();
	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],
	);
	const envVars = useMemo(() => {
		if (mode !== "deploy") {
			return [];
		}
		return env.access
			.filter((a) => a.name === data.label)
			.filter((a) => a.type === "env_var")
			.map((a) => ({
				name: a.var.split("=", 2)[0],
				value: a.var.split("=", 2)[1],
			}));
	}, [mode, env.access, data.label]);

	const hiddenEnvVars = useMemo(() => {
		return envVars
			.map((v) => {
				const { name, value } = v;
				const match = value.match(/^(postgresql|mongodb):\/\/([^:]+):([^@]+)@([^:/]+)(?::(\d+))?\/(.+)$/);
				if (match) {
					const [_, protocol, username, _password, host, port, database] = match;
					return {
						name,
						value,
						hidden: `${protocol}://${username}:*****@${host}${port ? `:${port}` : ""}/${database}`,
					};
				}
				return {
					name,
					value,
					hidden: value,
				};
			})
			.map((v) => {
				return {
					...v,
					hidden: v.hidden.length > 50 ? v.hidden.slice(0, 50) + "..." : v.hidden,
				};
			});
	}, [envVars]);

	const [copied, setCopied] = useState(false);
	const [blip, setBlip] = useState(false);

	const handleCopy = () => {
		navigator.clipboard.writeText(envVars.map((v) => `${v.name}=${v.value}`).join("\n"));
		setCopied(true);
		setBlip(true);
		setTimeout(() => setCopied(false), 1000);
		setTimeout(() => setBlip(false), 300);
	};

	if (hiddenEnvVars.length > 0) {
		return (
			<div className="flex flex-col gap-1">
				<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-1 flex-shrink">
					{hiddenEnvVars.map((v) => (
						<div key={v.name} className="contents">
							<div className="uppercase">{v.name}</div>
							<div className="min-w-0 truncate">{v.hidden}</div>
						</div>
					))}
				</div>
				<div className="flex justify-end">
					<Button onClick={handleCopy} className={blip ? "bg-green-100 transition-colors" : ""}>
						{copied ? (
							<>
								<Check className="w-4 h-4 text-green-600" /> Copy
							</>
						) : (
							<>
								<Copy className="w-4 h-4" /> Copy
							</>
						)}
					</Button>
				</div>
			</div>
		);
	}

	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 usePrevious<T>(value: T) {
	const ref = useRef<T>();
	useEffect(() => {
		ref.current = value;
	}, [value]);
	return ref.current;
}

function DevVM({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const { dev } = data;
	const prevDev = usePrevious(dev);
	const env = useEnv();
	const store = useStateStore();
	useEffect(() => {
		console.log("DDDEV", prevDev, dev);
		if (!dev && !prevDev) {
			return;
		}
		if (
			dev &&
			prevDev &&
			dev.enabled === prevDev.enabled &&
			"mode" in dev &&
			"mode" in prevDev &&
			dev.mode === prevDev.mode
		) {
			return;
		}
		if (!dev?.enabled || dev.mode !== "VM") {
			if (prevDev?.enabled && prevDev.mode === "VM") {
				store.setNodes(
					store.nodes.filter((n) => n.id !== prevDev.codeServerNodeId && n.id !== prevDev.sshNodeId),
				);
				store.setEdges(
					store.edges.filter((e) => e.target !== prevDev.codeServerNodeId && e.target !== prevDev.sshNodeId),
				);
				if (dev?.enabled) {
					store.updateNodeData<"app">(id, {
						dev: {
							enabled: dev.enabled,
							mode: dev.mode,
						},
						ports: (data.ports || []).filter((p) => p.name !== "code-server" && p.name !== "ssh"),
					});
				} else {
					store.updateNodeData<"app">(id, {
						dev: {
							enabled: false,
						},
						ports: (data.ports || []).filter((p) => p.name !== "code-server" && p.name !== "ssh"),
					});
				}
			}
		} else {
			if (!prevDev?.enabled || prevDev.mode !== "VM") {
				const csGateway: Omit<GatewayHttpsNode, "position"> = {
					id: uuidv4(),
					type: "gateway-https",
					data: {
						readonly: true,
						https: {
							serviceId: id,
							portId: `${id}-code-server`,
						},
						network: dev?.expose?.network,
						subdomain: 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: dev?.expose?.network,
						subdomain: dev?.expose?.subdomain,
						label: "",
						envVars: [],
						ports: [],
					},
				};
				store.addNode(csGateway);
				store.addNode(sshGateway);
				store.updateNodeData<"app">(id, {
					dev: {
						enabled: true,
						mode: "VM",
						expose: 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 (dev?.expose?.network !== undefined) {
					edges = edges.concat([
						{
							id: uuidv4(),
							source: csGateway.id,
							sourceHandle: "subdomain",
							target: dev.expose.network,
							targetHandle: "subdomain",
						},
						{
							id: uuidv4(),
							source: sshGateway.id,
							sourceHandle: "subdomain",
							target: dev.expose.network,
							targetHandle: "subdomain",
						},
					]);
				}
				store.setEdges(edges);
			}
		}
	}, [id, data, dev, prevDev, store]);
	const exposeForm = useForm<z.infer<typeof exposeSchema>>({
		resolver: zodResolver(exposeSchema),
		mode: "onChange",
		defaultValues: {
			network: dev && "expose" in dev ? dev.expose?.network : undefined,
			subdomain: dev && "expose" in dev ? dev.expose?.subdomain : undefined,
		},
	});
	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 || dev.mode !== "VM") {
					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, dev, prevDev, exposeForm, store]);
	if (!dev?.enabled || dev.mode !== "VM") {
		return null;
	}
	return (
		<div>
			{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>
			)}
		</div>
	);
}

function DevProxy({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const store = useStateStore();
	const { toast } = useToast();
	const [machines, setMachines] = useState<Machines>([]);
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState<string | null>(null);

	const fetchMachines = useCallback(async () => {
		setLoading(true);
		setError(null);
		try {
			const response = await fetch("/api/machines", {
				method: "GET",
				headers: {
					"Content-Type": "application/json",
				},
			});

			if (!response.ok) {
				throw new Error(`Failed to fetch machines: ${response.statusText}`);
			}

			const machinesData = MachinesSchema.safeParse(await response.json());
			if (machinesData.success) {
				setMachines(
					machinesData.data
						.filter((m) => !m.name.startsWith("proxy-dodo-app-"))
						.filter(
							(m) => m.last_seen && m.last_seen.seconds * 1000 >= Date.now() - 7 * 24 * 60 * 60 * 1000,
						),
				);
			} else {
				throw new Error("Invalid machines data");
			}
		} catch (err) {
			const errorMessage = err instanceof Error ? err.message : "Failed to fetch machines";
			setError(errorMessage);
			toast({
				variant: "destructive",
				title: "Error",
				description: errorMessage,
			});
		} finally {
			setLoading(false);
		}
	}, [toast]);

	useEffect(() => {
		if (data.dev?.enabled && "mode" in data.dev && data.dev.mode === "PROXY") {
			fetchMachines();
		}
	}, [data.dev, fetchMachines]);

	const proxyForm = useForm<z.infer<typeof proxySchema>>({
		resolver: zodResolver(proxySchema),
		mode: "onChange",
		defaultValues: {
			address: data.dev && "address" in data.dev ? data.dev.address : undefined,
		},
	});

	useEffect(() => {
		const sub = proxyForm.watch((value, { name }) => {
			if (name === "address" && value.address) {
				store.updateNodeData<"app">(id, {
					dev: {
						enabled: true,
						mode: "PROXY",
						address: value.address,
					},
				});
			}
		});
		return () => sub.unsubscribe();
	}, [id, proxyForm, store]);

	if (!data.dev?.enabled || data.dev.mode !== "PROXY") {
		return null;
	}
	return (
		<div className="space-y-2">
			<Form {...proxyForm}>
				<form className="space-y-2">
					<FormField
						control={proxyForm.control}
						name="address"
						render={({ field }) => (
							<FormItem>
								<Select
									onValueChange={field.onChange}
									value={field.value || ""}
									disabled={disabled || loading}
								>
									<FormControl>
										<SelectTrigger>
											{loading ? (
												<div className="flex items-center gap-2">
													<LoaderCircle className="h-4 w-4 animate-spin" />
													<span>Loading machines...</span>
												</div>
											) : (
												<SelectValue placeholder="Select a machine" />
											)}
										</SelectTrigger>
									</FormControl>
									<SelectContent>
										{loading ? (
											<div className="flex items-center justify-center p-4">
												<LoaderCircle className="h-4 w-4 animate-spin" />
												<span className="ml-2">Loading...</span>
											</div>
										) : error ? (
											<div className="flex flex-col items-center justify-center p-4 text-destructive">
												<span className="text-sm">Failed to load machines</span>
												<Button
													variant="ghost"
													size="sm"
													className="mt-2"
													onClick={fetchMachines}
												>
													Retry
												</Button>
											</div>
										) : machines.length === 0 ? (
											<div className="flex items-center justify-center p-4 text-muted-foreground">
												<span className="text-sm">No machines available</span>
											</div>
										) : (
											machines.map((machine: Machine) => (
												<SelectItem key={machine.name} value={machine.name}>
													{machine.name}
												</SelectItem>
											))
										)}
									</SelectContent>
								</Select>
								<FormMessage />
							</FormItem>
						)}
					/>
				</form>
			</Form>
		</div>
	);
}

function Dev({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
	const { id, data } = node;
	const store = useStateStore();
	const devForm = useForm<z.infer<typeof devSchema>>({
		resolver: zodResolver(devSchema),
		mode: "onChange",
		defaultValues: {
			enabled: data.dev ? data.dev.enabled : false,
			mode: data.dev?.enabled ? data.dev.mode : undefined,
		},
	});
	useEffect(() => {
		const sub = devForm.watch((value, { name }) => {
			console.log("DDDEVV", name, value, data.dev);
			if (name === "enabled") {
				if (value.enabled) {
					if (data.dev?.enabled && data.dev.mode === "VM") {
						return;
					}
					store.updateNodeData<"app">(id, {
						dev: {
							enabled: true,
							mode: "VM",
						},
					});
					devForm.setValue("mode", "VM");
				} else {
					store.updateNodeData<"app">(id, {
						dev: {
							enabled: false,
						},
					});
				}
			} else if (name === "mode") {
				if (data.dev?.enabled && data.dev.mode === value.mode) {
					return;
				}
				store.updateNodeData<"app">(id, {
					dev: {
						enabled: true,
						mode: value.mode,
					},
				});
			}
		});
		return () => sub.unsubscribe();
	}, [id, data, devForm, 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">Development Mode</Label>
								</div>
								<FormMessage />
							</FormItem>
						)}
					/>
					{data.dev?.enabled && (
						<FormField
							control={devForm.control}
							name="mode"
							render={({ field }) => (
								<FormItem>
									<div className="flex flex-row gap-1 items-center">
										<RadioGroup
											onValueChange={field.onChange}
											value={field.value}
											disabled={disabled}
										>
											<div className="flex items-center space-x-2">
												<RadioGroupItem value="VM" id="vm" />
												<Label htmlFor="vm">Create a VM</Label>
											</div>
											<div className="flex items-center space-x-2">
												<RadioGroupItem value="PROXY" id="proxy" />
												<Label htmlFor="proxy">Proxy to existing machine</Label>
											</div>
										</RadioGroup>
									</div>
								</FormItem>
							)}
						/>
					)}
				</form>
			</Form>
			<DevVM node={node} disabled={disabled} />
			<DevProxy node={node} disabled={disabled} />
		</>
	);
}

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>
	);
}
