// Path: Frontend/src/components/shell/ProjectSelector/ProjectSelector.tsx

import { For, Show, createEffect, createSignal, onCleanup, onMount, type JSX } from "solid-js";
import { ChevronDown, ChevronRight, Folder, LayoutGrid, ListCollapse, UnfoldVertical } from "../../../lib/icons";
import { ProjectContextMenu } from "../ProjectContextMenu/ProjectContextMenu";
import { useAppShellData } from "../data/app-shell.context";
import { resolveAPIBase } from "../../../lib/api";
import {
	collectBranchNodeIds,
	findTreeNodeDepth,
	findTreeNodeLocation,
	getPointerRelativeY,
	isUuidString,
	moveTreeNode,
	resolveTreeDropTarget,
	type NavTreeAdapter,
	type NavTreeDropTarget,
} from "../shared/navTreeDnd";
import {
	createProjectFolderTarget,
	createProjectSurfaceTarget,
	createProjectTarget,
	type ProjectItem,
	type ProjectMenuTarget,
} from "../data/shell.data";
import { createProjectContextMenuController } from "../ProjectContextMenu/createProjectContextMenuController";
import styles from "./ProjectSelector.module.scss";

type ProjectSelectorProps = {
	compact?: boolean;
	isOpen: boolean;
	onToggle: () => void;
	onClose: () => void;
};

type ProjectFolderNode = {
	kind: "folder";
	id: string;
	path: string;
	label: string;
	meta?: string;
	children: ProjectTreeNode[];
};

type ProjectLeafNode = {
	kind: "project";
	item: ProjectItem;
};

type ProjectTreeNode = ProjectFolderNode | ProjectLeafNode;

type PersistedProjectFolderRecord = {
	id: string;
	path: string;
	label: string;
	children: PersistedProjectFolderRecord[];
};

type ProjectFoldersResponse = {
	data?: {
		folders?: PersistedProjectFolderRecord[];
		renamedFolder?: PersistedProjectFolderRecord;
		movedFolder?: PersistedProjectFolderRecord;
		previousFolderId?: string;
		previousFolderPath?: string;
	};
	error?: string;
	message?: string;
};

type PendingProjectFolderDraft = {
	parentId: string | null;
	depth: number;
};

type PendingProjectFolderRename = {
	folderId: string;
	depth: number;
};

type ProjectDragTarget = NavTreeDropTarget;

type ProjectDragState = {
	draggedNodeId: string;
	dropTarget: ProjectDragTarget | null;
};

const LONG_PRESS_MS = 320;

const getProjectTreeNodeId = (node: ProjectTreeNode): string =>
	node.kind === "folder" ? node.id : node.item.id;

const buildPersistedFolderNodes = (folders: readonly PersistedProjectFolderRecord[] = []): ProjectTreeNode[] =>
	folders.map((folder) => ({
		kind: "folder",
		id: folder.id,
		path: folder.path,
		label: folder.label,
		children: buildPersistedFolderNodes(folder.children ?? []),
	}));

const buildProjectTree = (
	items: readonly ProjectItem[],
	folders: readonly PersistedProjectFolderRecord[] = [],
): ProjectTreeNode[] => [
	// The selector still presents scaffold project leaves beside persisted folders.
	// Keep that mixed root shape explicit here so the drag/drop logic can account
	// for folder-only ordering when we persist sibling positions.
	...items.map((item) => ({
		kind: "project" as const,
		item,
	})),
	...buildPersistedFolderNodes(folders),
];

const countProjectFolderSiblingsBeforeIndex = (
	siblings: readonly ProjectTreeNode[],
	index: number,
): number => siblings.slice(0, index).filter((node) => node.kind === "folder").length;

const readPersistedFolders = (body: ProjectFoldersResponse): PersistedProjectFolderRecord[] =>
	Array.isArray(body.data?.folders) ? body.data.folders : [];

const projectTreeAdapter: NavTreeAdapter<ProjectTreeNode> = {
	getNodeId: getProjectTreeNodeId,
	isBranchNode: (node) => node.kind === "folder",
	getChildren: (node) => (node.kind === "folder" ? node.children : []),
	withChildren: (node, children) =>
		node.kind === "folder"
			? {
				...node,
				children: [...children],
			}
			: node,
};

const ProjectFolderDraftRow = (props: {
	depth: number;
	value: string;
	onInput: (value: string) => void;
	onSubmit: () => void;
	onCancel: () => void;
}): JSX.Element => {
	let inputRef: HTMLInputElement | undefined;

	queueMicrotask(() => inputRef?.focus());

	return (
		<li>
			<div class={styles.treeInputRow} style={{ "--tree-depth": String(props.depth) }}>
				<Folder class={styles.icon} size={18} strokeWidth={2} />
				<input
					ref={inputRef}
					type="text"
					class={styles.treeInput}
					value={props.value}
					placeholder="Folder name"
					onInput={(event): void => props.onInput(event.currentTarget.value)}
					onBlur={props.onSubmit}
					onKeyDown={(event): void => {
						if (event.key === "Enter") {
							event.preventDefault();
							event.currentTarget.blur();
							return;
						}

						if (event.key === "Escape") {
							event.preventDefault();
							props.onCancel();
							event.currentTarget.blur();
						}
					}}
				/>
			</div>
		</li>
	);
};

const ProjectFolderBranch = (props: {
	nodes: readonly ProjectTreeNode[];
	depth: number;
	parentId: string | null;
	selectedProjectId: string;
	isFolderCollapsed: (folderId: string) => boolean;
	onToggleFolder: (folderId: string) => void;
	onSelectProject: (projectId: string) => void;
	onOpenFolderMenu: (event: MouseEvent, folder: ProjectFolderNode) => void;
	onOpenProjectMenu: (event: MouseEvent, item: ProjectItem) => void;
	onNodePointerDown: (event: PointerEvent, nodeId: string) => void;
	onNodePointerMove: (event: PointerEvent, parentId: string | null, index: number, node: ProjectTreeNode) => void;
	pendingFolderDraft: PendingProjectFolderDraft | null;
	pendingFolderName: string;
	onPendingFolderNameChange: (value: string) => void;
	onSubmitPendingFolder: () => void;
	onCancelPendingFolder: () => void;
	pendingFolderRename: PendingProjectFolderRename | null;
	pendingFolderRenameName: string;
	onPendingFolderRenameChange: (value: string) => void;
	onSubmitPendingFolderRename: () => void;
	onCancelPendingFolderRename: () => void;
	dragState: ProjectDragState | null;
	isTreeClickSuppressed: () => boolean;
}): JSX.Element => (
	<ul class={styles.treeList} role="list">
		<Show when={props.nodes.length === 0 && props.pendingFolderDraft?.parentId !== props.parentId}>
			<li>
				<div class={styles.treeEmptySlot} style={{ "--tree-depth": String(props.depth) }} />
			</li>
		</Show>

		<For each={props.nodes}>
			{(node, indexAccessor): JSX.Element => {
				const nodeId = (): string => getProjectTreeNodeId(node);
				const isDraggedNode = (): boolean => props.dragState?.draggedNodeId === nodeId();
				const dropIntent = (): ProjectDragTarget["intent"] | null => {
					if (props.dragState?.dropTarget?.targetNodeId !== nodeId()) {
						return null;
					}

					return props.dragState.dropTarget.intent;
				};

				if (node.kind === "folder") {
					const isCollapsed = (): boolean => props.isFolderCollapsed(node.id);
					const isRenaming = (): boolean => props.pendingFolderRename?.folderId === node.id;

					return (
						<li>
							<Show
								when={isRenaming()}
								fallback={
									<button
										type="button"
										classList={{
											[styles.treeItem]: true,
											[styles.treeItemFolder]: true,
											[styles.treeItemDragging]: isDraggedNode(),
											[styles.treeItemDropBefore]: dropIntent() === "before",
											[styles.treeItemDropAfter]: dropIntent() === "after",
											[styles.treeItemDropInside]: dropIntent() === "inside",
										}}
										style={{ "--tree-depth": String(props.depth) }}
										aria-expanded={!isCollapsed()}
										onClick={() => {
											if (props.dragState || props.isTreeClickSuppressed()) {
												return;
											}

											props.onToggleFolder(node.id);
										}}
										onContextMenu={(event): void => props.onOpenFolderMenu(event, node)}
										onPointerDown={(event): void => props.onNodePointerDown(event, node.id)}
										onPointerMove={(event): void =>
											props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
										}
										onPointerEnter={(event): void =>
											props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
										}
									>
										<ChevronRight
											classList={{
												[styles.folderChevron]: true,
												[styles.folderChevronOpen]: !isCollapsed(),
											}}
											size={16}
											strokeWidth={2}
										/>
										<Folder class={styles.icon} size={18} strokeWidth={2} />
										<span class={styles.label}>{node.label}</span>
										<Show when={node.meta}>
											<span class={styles.itemMeta}>{node.meta}</span>
										</Show>
									</button>
								}
							>
								<ProjectFolderDraftRow
									depth={props.pendingFolderRename?.depth ?? props.depth}
									value={props.pendingFolderRenameName}
									onInput={props.onPendingFolderRenameChange}
									onSubmit={props.onSubmitPendingFolderRename}
									onCancel={props.onCancelPendingFolderRename}
								/>
							</Show>

							<Show when={!isCollapsed() && ((node.children?.length ?? 0) > 0 || props.pendingFolderDraft?.parentId === node.id)}>
								<ProjectFolderBranch
									nodes={node.children}
									depth={props.depth + 1}
									parentId={node.id}
									selectedProjectId={props.selectedProjectId}
									isFolderCollapsed={props.isFolderCollapsed}
									onToggleFolder={props.onToggleFolder}
									onSelectProject={props.onSelectProject}
									onOpenFolderMenu={props.onOpenFolderMenu}
									onOpenProjectMenu={props.onOpenProjectMenu}
									onNodePointerDown={props.onNodePointerDown}
									onNodePointerMove={props.onNodePointerMove}
									pendingFolderDraft={props.pendingFolderDraft}
									pendingFolderName={props.pendingFolderName}
									onPendingFolderNameChange={props.onPendingFolderNameChange}
									onSubmitPendingFolder={props.onSubmitPendingFolder}
									onCancelPendingFolder={props.onCancelPendingFolder}
									pendingFolderRename={props.pendingFolderRename}
									pendingFolderRenameName={props.pendingFolderRenameName}
									onPendingFolderRenameChange={props.onPendingFolderRenameChange}
									onSubmitPendingFolderRename={props.onSubmitPendingFolderRename}
									onCancelPendingFolderRename={props.onCancelPendingFolderRename}
									dragState={props.dragState}
									isTreeClickSuppressed={props.isTreeClickSuppressed}
								/>
							</Show>
						</li>
					);
				}

				return (
					<li>
						<button
							type="button"
							classList={{
								[styles.treeItem]: true,
								[styles.treeItemActive]: props.selectedProjectId === node.item.id,
								[styles.treeItemDragging]: isDraggedNode(),
								[styles.treeItemDropBefore]: dropIntent() === "before",
								[styles.treeItemDropAfter]: dropIntent() === "after",
							}}
							style={{ "--tree-depth": String(props.depth) }}
							onClick={(): void => {
								if (props.dragState || props.isTreeClickSuppressed()) {
									return;
								}

								props.onSelectProject(node.item.id);
							}}
							onContextMenu={(event): void => props.onOpenProjectMenu(event, node.item)}
							onPointerDown={(event): void => props.onNodePointerDown(event, node.item.id)}
							onPointerMove={(event): void =>
								props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
							}
							onPointerEnter={(event): void =>
								props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
							}
						>
							<LayoutGrid class={styles.icon} size={18} strokeWidth={2} />
							<span class={styles.label}>{node.item.name}</span>
							<Show when={node.item.meta}>
								<span class={styles.itemMeta}>{node.item.meta}</span>
							</Show>
						</button>
					</li>
				);
			}}
		</For>

		<Show when={props.pendingFolderDraft?.parentId === props.parentId}>
			<ProjectFolderDraftRow
				depth={props.pendingFolderDraft?.depth ?? props.depth}
				value={props.pendingFolderName}
				onInput={props.onPendingFolderNameChange}
				onSubmit={props.onSubmitPendingFolder}
				onCancel={props.onCancelPendingFolder}
			/>
		</Show>
	</ul>
);

export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
	const appShellData = useAppShellData();
	const [selectedProject, setSelectedProject] = createSignal(appShellData.activeProject());
	const [drawerTop, setDrawerTop] = createSignal<number>(0);
	const [collapsedFolderIds, setCollapsedFolderIds] = createSignal<readonly string[]>([]);
	const [persistedFolders, setPersistedFolders] = createSignal<readonly PersistedProjectFolderRecord[]>([]);
	const [projectTreeNodes, setProjectTreeNodes] = createSignal<ProjectTreeNode[]>(
		buildProjectTree(appShellData.projectItems(), persistedFolders()),
	);
	const [pendingFolderDraft, setPendingFolderDraft] = createSignal<PendingProjectFolderDraft | null>(null);
	const [pendingFolderName, setPendingFolderName] = createSignal("");
	const [pendingFolderRename, setPendingFolderRename] = createSignal<PendingProjectFolderRename | null>(null);
	const [pendingFolderRenameName, setPendingFolderRenameName] = createSignal("");
	const [dragState, setDragState] = createSignal<ProjectDragState | null>(null);
	const [suppressNextTreeClick, setSuppressNextTreeClick] = createSignal(false);
	let rootRef: HTMLDivElement | undefined;
	let triggerRef: HTMLButtonElement | undefined;
	let contextMenuRef: HTMLDivElement | undefined;
	let longPressTimer: number | undefined;
	let suppressClickTimer: number | undefined;
	let lastSelectedProjectId: string | null = null;
	let latestPersistedFoldersRequest = 0;
	const contextMenu = createProjectContextMenuController();

	const clearLongPressTimer = (): void => {
		if (longPressTimer !== undefined) {
			window.clearTimeout(longPressTimer);
			longPressTimer = undefined;
		}
	};

	const suppressTreeClickTemporarily = (): void => {
		setSuppressNextTreeClick(true);

		if (suppressClickTimer !== undefined) {
			window.clearTimeout(suppressClickTimer);
		}

		suppressClickTimer = window.setTimeout(() => {
			setSuppressNextTreeClick(false);
			suppressClickTimer = undefined;
		}, 80);
	};

	const isFolderCollapsed = (folderId: string): boolean => collapsedFolderIds().includes(folderId);

	const toggleFolder = (folderId: string): void => {
		setCollapsedFolderIds((current) =>
			current.includes(folderId) ? current.filter((id) => id !== folderId) : [...current, folderId],
		);
	};

	const syncProjectTree = (): void => {
		const nextTree = buildProjectTree(appShellData.projectItems(), persistedFolders());
		const availableFolderIds = new Set(collectBranchNodeIds(nextTree, projectTreeAdapter));

		setProjectTreeNodes(nextTree);
		setCollapsedFolderIds((current) => current.filter((folderId) => availableFolderIds.has(folderId)));
	};

	const resetProjectTreeInteractionState = (): void => {
		setCollapsedFolderIds([]);
		setPendingFolderDraft(null);
		setPendingFolderName("");
		setPendingFolderRename(null);
		setPendingFolderRenameName("");
		setDragState(null);
	};

	const folderIds = (): string[] => collectBranchNodeIds(projectTreeNodes(), projectTreeAdapter);

	const expandAllFolders = (): void => {
		setCollapsedFolderIds([]);
	};

	const collapseAllFolders = (): void => {
		setCollapsedFolderIds(folderIds());
	};

	const totalFolderCount = (): number => folderIds().length;

	const areAllFoldersCollapsed = (): boolean => {
		const folderCount = totalFolderCount();

		return folderCount > 0 && collapsedFolderIds().length >= folderCount;
	};

	const toggleAllFolders = (): void => {
		if (areAllFoldersCollapsed()) {
			expandAllFolders();
			return;
		}

		collapseAllFolders();
	};

	const loadPersistedFolders = async (projectId: string): Promise<void> => {
		const requestId = latestPersistedFoldersRequest + 1;
		latestPersistedFoldersRequest = requestId;

		if (!projectId) {
			setPersistedFolders([]);
			return;
		}

		if (!isUuidString(projectId)) {
			setPersistedFolders([]);
			return;
		}

		try {
			const response = await fetch(`${resolveAPIBase()}/projects/${projectId}/folders`, {
				headers: {
					Accept: "application/json",
				},
			});
			const body = (await response.json()) as ProjectFoldersResponse;

			if (requestId !== latestPersistedFoldersRequest) {
				return;
			}

			if (!response.ok) {
				throw new Error(body.message || "Failed to load project folders.");
			}

			setPersistedFolders(readPersistedFolders(body));
		} catch (error) {
			if (requestId !== latestPersistedFoldersRequest) {
				return;
			}

			console.error(error);
			setPersistedFolders([]);
		}
	};

	createEffect(() => {
		setSelectedProject(appShellData.activeProject());
	});

	createEffect(() => {
		syncProjectTree();
	});

	createEffect(() => {
		const projectId = selectedProject().id;

		if (lastSelectedProjectId === null) {
			lastSelectedProjectId = projectId;
			return;
		}

		if (projectId === lastSelectedProjectId) {
			return;
		}

		lastSelectedProjectId = projectId;
		resetProjectTreeInteractionState();
	});

	createEffect(() => {
		const projectId = selectedProject().id;
		void loadPersistedFolders(projectId);
	});

	onMount(() => {
		if (triggerRef) {
			const updateDrawerTop = (): void => {
				if (!triggerRef) {
					return;
				}

				setDrawerTop(triggerRef.offsetTop + triggerRef.offsetHeight + 8);
			};

			updateDrawerTop();

			const observer = new ResizeObserver(() => {
				updateDrawerTop();
			});

			observer.observe(triggerRef);
			window.addEventListener("resize", updateDrawerTop);

			onCleanup(() => {
				observer.disconnect();
				window.removeEventListener("resize", updateDrawerTop);
			});
		}

		const handlePointerDown = (event: PointerEvent): void => {
			if (!props.isOpen || !rootRef) {
				return;
			}

			const target = event.target;

			if (target instanceof Node && rootRef.contains(target)) {
				return;
			}

			if (target instanceof Node && contextMenuRef?.contains(target)) {
				return;
			}

			props.onClose();
		};

		const handlePointerUp = (): void => {
			clearLongPressTimer();

			const nextDragState = dragState();

			if (!nextDragState?.dropTarget) {
				if (nextDragState) {
					suppressTreeClickTemporarily();
				}
				setDragState(null);
				return;
			}

			suppressTreeClickTemporarily();

			const currentNodes = projectTreeNodes();
			const draggedLocation = findTreeNodeLocation(currentNodes, nextDragState.draggedNodeId, projectTreeAdapter);
			const canPersistMove = isUuidString(selectedProject().id);
			const draggedFolderPath = draggedLocation?.node.kind === "folder" ? draggedLocation.node.path : null;
			const previewNodes = moveTreeNode(currentNodes, nextDragState.draggedNodeId, nextDragState.dropTarget as ProjectDragTarget, projectTreeAdapter);
			const previewLocation = findTreeNodeLocation(previewNodes, nextDragState.draggedNodeId, projectTreeAdapter);
			const persistedParentLocation = previewLocation?.parentId
				? findTreeNodeLocation(previewNodes, previewLocation.parentId, projectTreeAdapter)
				: null;
			const persistedParentFolderPath =
				persistedParentLocation?.node.kind === "folder" ? persistedParentLocation.node.path : null;
			const previewSiblings = previewLocation?.parentId
				? persistedParentLocation?.node.kind === "folder"
					? persistedParentLocation.node.children
					: []
				: previewNodes;
			// The preview tree includes project leaves and folders, but the backend only
			// stores sibling order for folders. Persist a folder-only index so the server
			// can reapply the same position against the authoritative ordered tree.
			const targetIndex = previewLocation
				? countProjectFolderSiblingsBeforeIndex(previewSiblings, previewLocation.index)
				: 0;

			if (
				canPersistMove &&
				draggedLocation?.node.kind === "folder" &&
				draggedFolderPath &&
				(!previewLocation?.parentId || persistedParentLocation?.node.kind === "folder")
			) {
				void movePersistedFolder(
					draggedFolderPath,
					persistedParentFolderPath,
					draggedLocation.node.id,
					persistedParentLocation?.node.kind === "folder" ? persistedParentLocation.node.id : null,
					targetIndex,
				);
			} else {
				setProjectTreeNodes((current) =>
					moveTreeNode(current, nextDragState.draggedNodeId, nextDragState.dropTarget as ProjectDragTarget, projectTreeAdapter),
				);
			}

			setDragState(null);
		};

		const handleEscape = (event: KeyboardEvent): void => {
			if (event.key !== "Escape") {
				return;
			}

			clearLongPressTimer();

			if (dragState()) {
				setDragState(null);
				return;
			}

			if (!props.isOpen) {
				return;
			}

			props.onClose();
			triggerRef?.focus();
		};

		document.addEventListener("pointerdown", handlePointerDown);
		window.addEventListener("pointerup", handlePointerUp);
		window.addEventListener("pointercancel", handlePointerUp);
		window.addEventListener("keydown", handleEscape);

		onCleanup(() => {
			clearLongPressTimer();
			if (suppressClickTimer !== undefined) {
				window.clearTimeout(suppressClickTimer);
			}
			document.removeEventListener("pointerdown", handlePointerDown);
			window.removeEventListener("pointerup", handlePointerUp);
			window.removeEventListener("pointercancel", handlePointerUp);
			window.removeEventListener("keydown", handleEscape);
		});
	});

	const toggleOpen = (): void => {
		if (!props.isOpen) {
			props.onToggle();
			return;
		}

		props.onClose();
	};

	const selectProject = (projectId: string): void => {
		const location = findTreeNodeLocation(projectTreeNodes(), projectId, projectTreeAdapter);

		if (!location || location.node.kind !== "project") {
			return;
		}

		setSelectedProject({ id: location.node.item.id, name: location.node.item.name });
		props.onClose();
	};

	const beginFolderDraft = (parentId: string | null, depth: number): void => {
		if (parentId) {
			setCollapsedFolderIds((current) => current.filter((id) => id !== parentId));
		}

		setPendingFolderRename(null);
		setPendingFolderRenameName("");
		setPendingFolderName("");
		setPendingFolderDraft({ parentId, depth });
	};

	const beginFolderRename = (folderId: string, label: string, depth: number): void => {
		setPendingFolderDraft(null);
		setPendingFolderName("");
		setPendingFolderRename({ folderId, depth });
		setPendingFolderRenameName(label);
	};

	const resolveFolderPath = (folderId: string): string | null => {
		const location = findTreeNodeLocation(projectTreeNodes(), folderId, projectTreeAdapter);
		return location && location.node.kind === "folder" ? location.node.path : null;
	};

	const submitPendingFolder = async (): Promise<void> => {
		const name = pendingFolderName().trim();
		const draft = pendingFolderDraft();
		const projectId = selectedProject().id;

		if (!draft) {
			return;
		}

		if (!name) {
			setPendingFolderDraft(null);
			setPendingFolderName("");
			return;
		}

		if (!isUuidString(projectId)) {
			cancelPendingFolder();
			return;
		}

		const parentFolderPath = draft.parentId ? resolveFolderPath(draft.parentId) : null;
		if (draft.parentId && !parentFolderPath) {
			cancelPendingFolder();
			return;
		}

		try {
			const response = await fetch(`${resolveAPIBase()}/projects/${projectId}/folders`, {
				method: "POST",
				headers: {
					Accept: "application/json",
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					name,
					parentFolderId: parentFolderPath,
				}),
			});

			const body = (await response.json()) as ProjectFoldersResponse;

			if (!response.ok) {
				throw new Error(body.message || "Failed to create project folder.");
			}

			setPersistedFolders(readPersistedFolders(body));
			setPendingFolderDraft(null);
			setPendingFolderName("");
		} catch (error) {
			console.error(error);
		}
	};

	const deletePersistedFolder = async (folderId: string): Promise<void> => {
		const projectId = selectedProject().id;
		if (!folderId || !isUuidString(projectId)) {
			return;
		}

		const folderPath = resolveFolderPath(folderId);
		if (!folderPath) {
			return;
		}

		try {
			const response = await fetch(
				`${resolveAPIBase()}/projects/${projectId}/folders?folderId=${encodeURIComponent(folderPath)}`,
				{
					method: "DELETE",
					headers: {
						Accept: "application/json",
					},
				},
			);

			const body = (await response.json()) as ProjectFoldersResponse;

			if (!response.ok) {
				throw new Error(body.message || "Failed to delete project folder.");
			}

			setPersistedFolders(readPersistedFolders(body));
			setCollapsedFolderIds((current) => current.filter((id) => id !== folderId));
		} catch (error) {
			console.error(error);
		}
	};

	const movePersistedFolder = async (
		folderPath: string,
		parentFolderPath: string | null,
		folderStableId: string,
		parentStableId: string | null,
		targetIndex: number,
	): Promise<void> => {
		const projectId = selectedProject().id;
		if (!folderPath || !folderStableId || !isUuidString(projectId)) {
			return;
		}

		try {
			const response = await fetch(`${resolveAPIBase()}/projects/${projectId}/folders/move`, {
				method: "PATCH",
				headers: {
					Accept: "application/json",
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					folderId: folderPath,
					folderNodeId: folderStableId,
					parentFolderId: parentFolderPath,
					parentNodeId: parentStableId,
					targetIndex,
				}),
			});

			const body = (await response.json()) as ProjectFoldersResponse;

			if (!response.ok) {
				throw new Error(body.message || "Failed to move project folder.");
			}

			setPersistedFolders(readPersistedFolders(body));
		} catch (error) {
			console.error(error);
		}
	};

	const submitPendingFolderRename = async (): Promise<void> => {
		const draft = pendingFolderRename();
		const name = pendingFolderRenameName().trim();
		const projectId = selectedProject().id;

		if (!draft) {
			return;
		}

		if (!name) {
			setPendingFolderRename(null);
			setPendingFolderRenameName("");
			return;
		}

		if (!isUuidString(projectId)) {
			cancelPendingFolderRename();
			return;
		}

		const folderPath = resolveFolderPath(draft.folderId);
		if (!folderPath) {
			cancelPendingFolderRename();
			return;
		}

		try {
			const response = await fetch(`${resolveAPIBase()}/projects/${projectId}/folders`, {
				method: "PATCH",
				headers: {
					Accept: "application/json",
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					folderId: folderPath,
					name,
				}),
			});

			const body = (await response.json()) as ProjectFoldersResponse;

			if (!response.ok) {
				throw new Error(body.message || "Failed to rename project folder.");
			}

			setPersistedFolders(readPersistedFolders(body));
			setPendingFolderRename(null);
			setPendingFolderRenameName("");
		} catch (error) {
			console.error(error);
		}
	};

	const cancelPendingFolder = (): void => {
		setPendingFolderDraft(null);
		setPendingFolderName("");
	};

	const cancelPendingFolderRename = (): void => {
		setPendingFolderRename(null);
		setPendingFolderRenameName("");
	};

	const handleContextActionSelect = (action: { id: string; label: string }, target: ProjectMenuTarget): void => {
		switch (action.id) {
			case "new-folder":
				switch (target.kind) {
					case "surface":
						beginFolderDraft(null, 0);
						return;
					case "folder":
						beginFolderDraft(target.id, (findTreeNodeDepth(projectTreeNodes(), target.id, projectTreeAdapter) ?? 0) + 1);
						return;
					case "project": {
						const parentId = findTreeNodeLocation(projectTreeNodes(), target.id, projectTreeAdapter)?.parentId ?? null;
						beginFolderDraft(parentId, parentId ? (findTreeNodeDepth(projectTreeNodes(), parentId, projectTreeAdapter) ?? 0) + 1 : 0);
						return;
					}
				}
				return;
			case "delete-folder":
				if (target.kind === "folder") {
					void deletePersistedFolder(target.id);
				}
				return;
			case "rename-folder":
				if (target.kind === "folder") {
					beginFolderRename(target.id, target.label, findTreeNodeDepth(projectTreeNodes(), target.id, projectTreeAdapter) ?? 0);
				}
				return;
			default:
				return;
		}
	};

	const handleSurfaceContextMenu = (event: MouseEvent): void => {
		event.stopPropagation();
		contextMenu.openMenu(event, createProjectSurfaceTarget("Projects"));
	};

	const treeControlLabel = (): string =>
		areAllFoldersCollapsed() ? "Expand all folders" : "Collapse all folders";

	const handleNodePointerDown = (event: PointerEvent, nodeId: string): void => {
		if (event.button !== 0 || pendingFolderDraft()) {
			return;
		}

		clearLongPressTimer();
		longPressTimer = window.setTimeout(() => {
			suppressTreeClickTemporarily();
			setDragState({ draggedNodeId: nodeId, dropTarget: null });
		}, LONG_PRESS_MS);
	};

	const handleNodePointerMove = (
		event: PointerEvent,
		parentId: string | null,
		index: number,
		node: ProjectTreeNode,
	): void => {
		const nextDragState = dragState();

		if (!nextDragState) {
			return;
		}

		if (nextDragState.draggedNodeId === getProjectTreeNodeId(node)) {
			return;
		}

		const relativeY = getPointerRelativeY(event);
		if (relativeY === null) {
			return;
		}

		setDragState({
			...nextDragState,
			dropTarget: resolveTreeDropTarget({
				parentId,
				index,
				node,
				relativeY,
				adapter: projectTreeAdapter,
			}),
		});
	};

	return (
		<div
			ref={rootRef}
			classList={{
				[styles.root]: true,
				[styles.rootCompact]: !!props.compact,
				[styles.rootDragMode]: !!dragState(),
			}}
			style={{
				"--project-drawer-top": `${drawerTop()}px`,
			}}
		>
			<button
				type="button"
				ref={triggerRef}
				classList={{
					[styles.trigger]: true,
					[styles.triggerCompact]: !!props.compact,
					[styles.triggerOpen]: props.isOpen,
				}}
				aria-label={`Open project menu for ${selectedProject().name}`}
				aria-expanded={props.isOpen}
				aria-haspopup="menu"
				title={selectedProject().name}
				onClick={toggleOpen}
			>
				<span class={styles.triggerLead} aria-hidden="true">
					<Folder size={18} strokeWidth={2} />
				</span>
				{!props.compact ? (
					<span class={styles.triggerCopy}>
						<span class={styles.eyebrow}>Projects</span>
						<span class={styles.value}>{selectedProject().name}</span>
					</span>
				) : null}
				<ChevronDown
					classList={{
						[styles.triggerIcon]: true,
						[styles.triggerIconOpen]: props.isOpen,
					}}
					size={16}
					strokeWidth={2}
				/>
			</button>

			<Show when={props.isOpen}>
				<>
					<button
						type="button"
						classList={{
							[styles.scrim]: true,
							[styles.scrimOpen]: props.isOpen,
						}}
						aria-hidden={!props.isOpen}
						tabIndex={props.isOpen ? 0 : -1}
						onClick={props.onClose}
					/>

					<div
						classList={{
							[styles.drawer]: true,
							[styles.drawerOpen]: props.isOpen,
						}}
						aria-hidden={!props.isOpen}
						onContextMenu={handleSurfaceContextMenu}
					>
						<div class={styles.drawerBody}>
							<div class={styles.treeSectionHeader}>
								<Show when={!props.compact}>
									<div class={styles.treeSectionLabel}>Projects</div>
								</Show>

								<div class={styles.treeControls}>
									<button
										type="button"
										class={styles.treeControlButton}
										onClick={toggleAllFolders}
										aria-label={treeControlLabel()}
										title={treeControlLabel()}
										disabled={totalFolderCount() === 0}
									>
										<Show
											when={areAllFoldersCollapsed()}
											fallback={<ListCollapse size={16} strokeWidth={2} />}
										>
											<UnfoldVertical size={16} strokeWidth={2} />
										</Show>
									</button>
								</div>
							</div>

							<ProjectFolderBranch
								nodes={projectTreeNodes()}
								depth={0}
								parentId={null}
								selectedProjectId={selectedProject().id}
								isFolderCollapsed={isFolderCollapsed}
								onToggleFolder={toggleFolder}
								onSelectProject={selectProject}
								onOpenFolderMenu={(event, folder): void => {
									event.stopPropagation();
									contextMenu.openMenu(event, createProjectFolderTarget(folder.id, folder.label));
								}}
								onOpenProjectMenu={(event, item): void => {
									event.stopPropagation();
									contextMenu.openMenu(event, createProjectTarget(item));
								}}
								onNodePointerDown={handleNodePointerDown}
								onNodePointerMove={handleNodePointerMove}
								pendingFolderDraft={pendingFolderDraft()}
								pendingFolderName={pendingFolderName()}
								onPendingFolderNameChange={setPendingFolderName}
								onSubmitPendingFolder={submitPendingFolder}
								onCancelPendingFolder={cancelPendingFolder}
								pendingFolderRename={pendingFolderRename()}
								pendingFolderRenameName={pendingFolderRenameName()}
								onPendingFolderRenameChange={setPendingFolderRenameName}
								onSubmitPendingFolderRename={submitPendingFolderRename}
								onCancelPendingFolderRename={cancelPendingFolderRename}
								dragState={dragState()}
								isTreeClickSuppressed={suppressNextTreeClick}
							/>
						</div>
					</div>
				</>
			</Show>

			<ProjectContextMenu
				target={contextMenu.menuState()?.target ?? null}
				position={(() => {
					const state = contextMenu.menuState();
					return state ? { x: state.x, y: state.y } : null;
				})()}
				menuRef={(element) => {
					contextMenuRef = element;
					contextMenu.setMenuRef(element);
				}}
				onClose={contextMenu.closeMenu}
				onSelect={handleContextActionSelect}
			/>
		</div>
	);
};
