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

import { For, Show, createEffect, createSignal, onCleanup, onMount, type JSX } from "solid-js";
import { resolveAPIBase } from "../../../lib/api";
import { ChevronLeft, ChevronRight, Folder, ListCollapse, UnfoldVertical } from "../../../lib/icons";
import { useAppShellData } from "../data/app-shell.context";
import { ProjectSelector } from "../ProjectSelector/ProjectSelector";
import {
	collectBranchNodeIds,
	findTreeNodeDepth,
	findTreeNodeLocation,
	getPointerRelativeY,
	isUuidString,
	moveTreeNode,
	resolveTreeDropTarget,
	type NavTreeAdapter,
	type NavTreeDropTarget,
} from "../shared/navTreeDnd";
import {
	createWorkspaceStaticTarget,
	createWorkspaceSurfaceTarget,
	createWorkspaceTreeTarget,
	getWorkspaceNodeIcon,
	workspaceSidebarHeaderActions,
	workspaceStaticItems,
	type WorkspaceContextMenuAction,
	type WorkspaceContextMenuTarget,
	type WorkspaceStaticItem,
	type WorkspaceTreeNode,
} from "../data/shell.data";
import { WorkspaceContextMenu } from "../WorkspaceContextMenu/WorkspaceContextMenu";
import { createWorkspaceContextMenuController } from "../WorkspaceContextMenu/createWorkspaceContextMenuController";
import styles from "./WorkspaceSidebar.module.scss";

type WorkspaceSidebarProps = {
	collapsed: boolean;
	railCollapsed: boolean;
	onToggleRailCollapse: () => void;
};

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

type WorkspaceDragTarget = NavTreeDropTarget;

type WorkspaceDragState = {
	draggedNodeId: string;
	dropTarget: WorkspaceDragTarget | null;
};

type PersistedWorkspaceFolderRecord = {
	id: string;
	label: string;
	children?: PersistedWorkspaceFolderRecord[];
};

type WorkspaceFoldersResponse = {
	data?: {
		folders?: PersistedWorkspaceFolderRecord[];
		renamedFolder?: PersistedWorkspaceFolderRecord;
		movedFolder?: PersistedWorkspaceFolderRecord;
		previousFolderId?: string;
	};
	error?: string;
	message?: string;
};

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

const LONG_PRESS_MS = 320;

const getWorkspaceTreeNodeId = (node: WorkspaceTreeNode): string => node.id;

const buildPersistedWorkspaceFolderNodes = (
	folders: readonly PersistedWorkspaceFolderRecord[],
): WorkspaceTreeNode[] =>
	folders.map((folder) => ({
		id: folder.id,
		label: folder.label,
		kind: "folder",
		icon: Folder,
		children: buildPersistedWorkspaceFolderNodes(folder.children ?? []),
	}));

const readPersistedWorkspaceFolders = (body: WorkspaceFoldersResponse): PersistedWorkspaceFolderRecord[] =>
	Array.isArray(body.data?.folders) ? body.data.folders : [];

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

const FolderDraftRow = (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 isContextMenuKeyboardTrigger = (event: KeyboardEvent): boolean => event.key === "ContextMenu" || (event.shiftKey && event.key === "F10");

const WorkspaceHomeEntry = (props: {
	item: WorkspaceStaticItem;
	onOpenContextMenu: (event: MouseEvent, target: WorkspaceContextMenuTarget) => void;
	onOpenContextMenuFromKeyboard: (element: HTMLElement, target: WorkspaceContextMenuTarget) => void;
}): JSX.Element => {
	const Icon = props.item.icon;
	const target = createWorkspaceStaticTarget(props.item);

	return (
		<li>
			<button
				type="button"
				classList={{
					[styles.navItem]: true,
					[styles.navItemActive]: !!props.item.active,
				}}
				aria-current={props.item.active ? "page" : undefined}
				aria-label={props.item.label}
				title={props.item.label}
				data-slot="workspace-static-item"
				data-target-kind={target.kind}
				data-active={props.item.active ? "true" : "false"}
				onContextMenu={(event): void => {
					event.stopPropagation();
					props.onOpenContextMenu(event, target);
				}}
				onKeyDown={(event): void => {
					if (!isContextMenuKeyboardTrigger(event)) {
						return;
					}

					event.preventDefault();
					props.onOpenContextMenuFromKeyboard(event.currentTarget, target);
				}}
			>
				<Icon class={styles.icon} size={18} strokeWidth={2} />
				<span class={styles.label}>{props.item.label}</span>
				<Show when={props.item.meta}>
					<span class={styles.itemMeta}>{props.item.meta}</span>
				</Show>
			</button>
		</li>
	);
};

const WorkspaceTreeBranch = (props: {
	nodes: readonly WorkspaceTreeNode[];
	parentId?: string | null;
	depth?: number;
	isFolderCollapsed: (folderId: string) => boolean;
	onToggleFolder: (folderId: string) => void;
	pendingFolderDraft: PendingWorkspaceFolderDraft | null;
	pendingFolderName: string;
	onPendingFolderNameChange: (value: string) => void;
	onSubmitPendingFolder: () => void;
	onCancelPendingFolder: () => void;
	pendingFolderRename: PendingWorkspaceFolderRename | null;
	pendingFolderRenameName: string;
	onPendingFolderRenameChange: (value: string) => void;
	onSubmitPendingFolderRename: () => void;
	onCancelPendingFolderRename: () => void;
	onOpenContextMenu: (event: MouseEvent, target: WorkspaceContextMenuTarget) => void;
	onOpenContextMenuFromKeyboard: (element: HTMLElement, target: WorkspaceContextMenuTarget) => void;
	onNodePointerDown: (event: PointerEvent, nodeId: string) => void;
	onNodePointerMove: (event: PointerEvent, parentId: string | null, index: number, node: WorkspaceTreeNode) => void;
	dragState: WorkspaceDragState | null;
	isTreeClickSuppressed: () => boolean;
}): JSX.Element => {
	const depth = () => props.depth ?? 0;
	const parentId = () => props.parentId ?? null;

	return (
		<ul class={styles.treeList} role="list">
			<Show when={props.nodes.length === 0 && props.pendingFolderDraft?.parentId !== parentId()}>
				<li>
					<div class={styles.treeEmptySlot} style={{ "--tree-depth": String(depth()) }} />
				</li>
			</Show>
			<For each={props.nodes}>
				{(node, indexAccessor): JSX.Element => {
					const Icon = getWorkspaceNodeIcon(node);
					const target = createWorkspaceTreeTarget(node);
					const isCollapsed = (): boolean => (node.kind === "folder" ? props.isFolderCollapsed(node.id) : false);
					const isRenaming = (): boolean => props.pendingFolderRename?.folderId === node.id;
					const isDraggedNode = (): boolean => props.dragState?.draggedNodeId === node.id;
					const dropIntent = (): WorkspaceDragTarget["intent"] | null => {
						if (props.dragState?.dropTarget?.targetNodeId !== node.id) {
							return null;
						}

						return props.dragState.dropTarget.intent;
					};

					return (
						<li>
							<Show
								when={node.kind === "folder" && isRenaming()}
								fallback={
									<button
										type="button"
										classList={{
											[styles.treeItem]: true,
											[styles.treeItemActive]: !!node.active,
											[styles.treeItemFolder]: node.kind === "folder",
											[styles.treeItemDragging]: isDraggedNode(),
											[styles.treeItemDropBefore]: dropIntent() === "before",
											[styles.treeItemDropAfter]: dropIntent() === "after",
											[styles.treeItemDropInside]: dropIntent() === "inside",
										}}
										style={{ "--tree-depth": String(depth()) }}
										aria-expanded={node.kind === "folder" ? !isCollapsed() : undefined}
										aria-current={node.active ? "page" : undefined}
										aria-label={node.label}
										title={node.label}
										data-slot="workspace-tree-item"
										data-kind={node.kind}
										data-item-type={node.kind === "item" ? node.itemType : undefined}
										data-active={node.active ? "true" : "false"}
										onClick={(): void => {
											if (props.dragState || props.isTreeClickSuppressed()) {
												return;
											}

											if (node.kind !== "folder") {
												return;
											}

											props.onToggleFolder(node.id);
										}}
										onContextMenu={(event): void => {
											event.stopPropagation();
											props.onOpenContextMenu(event, target);
										}}
										onPointerDown={(event): void => props.onNodePointerDown(event, node.id)}
										onPointerMove={(event): void =>
											props.onNodePointerMove(event, parentId(), indexAccessor(), node)
										}
										onPointerEnter={(event): void =>
											props.onNodePointerMove(event, parentId(), indexAccessor(), node)
										}
										onKeyDown={(event): void => {
											if (!isContextMenuKeyboardTrigger(event)) {
												return;
											}

											event.preventDefault();
											props.onOpenContextMenuFromKeyboard(event.currentTarget, target);
										}}
									>
										<Show when={node.kind === "folder"}>
											<ChevronRight
												classList={{
													[styles.folderChevron]: true,
													[styles.folderChevronOpen]: !isCollapsed(),
												}}
												size={16}
												strokeWidth={2}
											/>
										</Show>
										<Icon 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>
								}
							>
								<FolderDraftRow
									depth={props.pendingFolderRename?.depth ?? depth()}
									value={props.pendingFolderRenameName}
									onInput={props.onPendingFolderRenameChange}
									onSubmit={props.onSubmitPendingFolderRename}
									onCancel={props.onCancelPendingFolderRename}
								/>
							</Show>

							<Show when={node.kind === "folder" && !isCollapsed() && (((node.children?.length ?? 0) > 0) || props.pendingFolderDraft?.parentId === node.id)}>
								<WorkspaceTreeBranch
									nodes={node.children ?? []}
									parentId={node.id}
									depth={depth() + 1}
									isFolderCollapsed={props.isFolderCollapsed}
									onToggleFolder={props.onToggleFolder}
									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}
									onOpenContextMenu={props.onOpenContextMenu}
									onOpenContextMenuFromKeyboard={props.onOpenContextMenuFromKeyboard}
									onNodePointerDown={props.onNodePointerDown}
									onNodePointerMove={props.onNodePointerMove}
									dragState={props.dragState}
									isTreeClickSuppressed={props.isTreeClickSuppressed}
								/>
							</Show>
						</li>
					);
				}}
			</For>

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

export const WorkspaceSidebar = (props: WorkspaceSidebarProps): JSX.Element => {
	const appShellData = useAppShellData();
	const activeProject = () => appShellData.activeProject();
	const [isProjectDrawerOpen, setIsProjectDrawerOpen] = createSignal(false);
	const [workspaceTreeNodes, setWorkspaceTreeNodes] = createSignal<readonly WorkspaceTreeNode[]>(appShellData.workspaceTree());
	const [persistedFolders, setPersistedFolders] = createSignal<readonly PersistedWorkspaceFolderRecord[]>([]);
	const [collapsedFolderIds, setCollapsedFolderIds] = createSignal<readonly string[]>([]);
	const [pendingFolderDraft, setPendingFolderDraft] = createSignal<PendingWorkspaceFolderDraft | null>(null);
	const [pendingFolderName, setPendingFolderName] = createSignal("");
	const [pendingFolderRename, setPendingFolderRename] = createSignal<PendingWorkspaceFolderRename | null>(null);
	const [pendingFolderRenameName, setPendingFolderRenameName] = createSignal("");
	const [dragState, setDragState] = createSignal<WorkspaceDragState | null>(null);
	const [suppressNextTreeClick, setSuppressNextTreeClick] = createSignal(false);
	let lastSelectedProjectId: string | null = null;
	let latestPersistedFoldersRequest = 0;
	const contextMenu = createWorkspaceContextMenuController();
	let longPressTimer: number | undefined;
	let suppressClickTimer: number | undefined;
	const railToggleLabel = (): string => (props.railCollapsed ? "Expand server rail" : "Collapse server rail");
	const sidebarContextMenuTarget = createWorkspaceSurfaceTarget(appShellData.activeProject());
	const isFolderCollapsed = (folderId: string): boolean => collapsedFolderIds().includes(folderId);
	const folderIds = (): string[] => collectBranchNodeIds(workspaceTreeNodes(), workspaceTreeAdapter);
	const totalFolderCount = (): number => folderIds().length;
	const areAllFoldersCollapsed = (): boolean => {
		const count = totalFolderCount();
		return count > 0 && collapsedFolderIds().length >= count;
	};
	const workspaceFolderToggleLabel = (): string =>
		areAllFoldersCollapsed() ? "Expand all folders" : "Collapse all folders";
	const toggleFolder = (folderId: string): void => {
		setCollapsedFolderIds((current) =>
			current.includes(folderId) ? current.filter((id) => id !== folderId) : [...current, folderId],
		);
	};
	const expandAllFolders = (): void => {
		setCollapsedFolderIds([]);
	};
	const collapseAllFolders = (): void => {
		setCollapsedFolderIds(folderIds());
	};
	const toggleAllFolders = (): void => {
		if (areAllFoldersCollapsed()) {
			expandAllFolders();
			return;
		}

		collapseAllFolders();
	};
	const resetWorkspaceTreeInteractionState = (): void => {
		setCollapsedFolderIds([]);
		setPendingFolderDraft(null);
		setPendingFolderName("");
		setPendingFolderRename(null);
		setPendingFolderRenameName("");
		setDragState(null);
	};
	const syncWorkspaceTree = (): void => {
		const nextTree = activeProject()?.id
			? buildPersistedWorkspaceFolderNodes(persistedFolders())
			: appShellData.workspaceTree();
		const availableFolderIds = new Set(collectBranchNodeIds(nextTree, workspaceTreeAdapter));

		setWorkspaceTreeNodes(nextTree);
		setCollapsedFolderIds((current) => current.filter((id) => availableFolderIds.has(id)));
	};
	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}/tree/folders`, {
				headers: {
					Accept: "application/json",
				},
			});
			const body = (await response.json()) as WorkspaceFoldersResponse;

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

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

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

			console.error(error);
			setPersistedFolders([]);
		}
	};
	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);
	};

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

	createEffect(() => {
		const projectId = activeProject()?.id ?? null;

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

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

		lastSelectedProjectId = projectId;
		resetWorkspaceTreeInteractionState();
	});

	createEffect(() => {
		void loadPersistedFolders(activeProject()?.id ?? "");
	});

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

			const nextDragState = dragState();

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

			suppressTreeClickTemporarily();

			const currentNodes = workspaceTreeNodes();
			const draggedLocation = findTreeNodeLocation(currentNodes, nextDragState.draggedNodeId, workspaceTreeAdapter);
			const persistedParentId = nextDragState.dropTarget.parentId;
			const canPersistMove = isUuidString(activeProject()?.id ?? "");
			const persistedParentLocation = persistedParentId
				? findTreeNodeLocation(currentNodes, persistedParentId, workspaceTreeAdapter)
				: null;

			if (
				canPersistMove &&
				draggedLocation?.node.kind === "folder" &&
				(persistedParentId === null || persistedParentLocation?.node.kind === "folder")
			) {
				void movePersistedFolder(draggedLocation.node.id, persistedParentId);
			} else {
				setWorkspaceTreeNodes((current) =>
					moveTreeNode(current, nextDragState.draggedNodeId, nextDragState.dropTarget as WorkspaceDragTarget, workspaceTreeAdapter),
				);
			}

			setDragState(null);
		};

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

			clearLongPressTimer();

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

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

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

	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 submitPendingFolder = async (): Promise<void> => {
		const name = pendingFolderName().trim();
		const draft = pendingFolderDraft();
		const projectId = activeProject()?.id ?? "";

		if (!draft) {
			return;
		}

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

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

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

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

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

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

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

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

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

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

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

	const movePersistedFolder = async (folderId: string, parentFolderId: string | null): Promise<void> => {
		const projectId = activeProject()?.id ?? "";
		if (!folderId || !projectId || !isUuidString(projectId)) {
			return;
		}

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

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

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

			setPersistedFolders(readPersistedWorkspaceFolders(body));

			const previousFolderId = body.data?.previousFolderId;
			const movedFolderId = body.data?.movedFolder?.id;
			if (previousFolderId && movedFolderId && previousFolderId !== movedFolderId) {
				setCollapsedFolderIds((current) =>
					current.map((id) => (id === previousFolderId ? movedFolderId : id)),
				);
			}
		} catch (error) {
			console.error(error);
		}
	};

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

		if (!draft) {
			return;
		}

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

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

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

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

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

			setPersistedFolders(readPersistedWorkspaceFolders(body));
			setPendingFolderRename(null);
			setPendingFolderRenameName("");

			const previousFolderId = body.data?.previousFolderId;
			const renamedFolderId = body.data?.renamedFolder?.id;
			if (previousFolderId && renamedFolderId && previousFolderId !== renamedFolderId) {
				setCollapsedFolderIds((current) =>
					current.map((id) => (id === previousFolderId ? renamedFolderId : id)),
				);
			}
		} catch (error) {
			console.error(error);
		}
	};

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

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

	const handleHeaderActionClick = (actionId: string): void => {
		switch (actionId) {
			case "toggle-workspace-folders":
				toggleAllFolders();
				return;
			case "search-workspace":
			default:
				return;
		}
	};

	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: WorkspaceTreeNode,
	): void => {
		const nextDragState = dragState();

		if (!nextDragState || nextDragState.draggedNodeId === getWorkspaceTreeNodeId(node)) {
			return;
		}

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

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

	const handleContextActionSelect = (action: WorkspaceContextMenuAction, target: WorkspaceContextMenuTarget): void => {
		switch (action.id) {
			case "new-folder":
				switch (target.kind) {
					case "workspace":
					case "home":
						beginFolderDraft(null, 0);
						return;
					case "folder":
						beginFolderDraft(target.id, (findTreeNodeDepth(workspaceTreeNodes(), target.id, workspaceTreeAdapter) ?? 0) + 1);
						return;
					case "settings":
					case "item":
						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(workspaceTreeNodes(), target.id, workspaceTreeAdapter) ?? 0);
				}
				return;
			default:
				return;
		}
	};

	return (
		<>
			<aside
				classList={{
					[styles.sidebar]: true,
					[styles.sidebarCollapsed]: props.collapsed,
					[styles.sidebarDragMode]: !!dragState(),
				}}
				aria-label="Left workspace sidebar"
				data-ui="workspace-sidebar"
				data-collapsed={props.collapsed ? "true" : "false"}
				onContextMenu={(event): void => {
					contextMenu.openMenu(event, sidebarContextMenuTarget);
				}}
			>
				<div
					classList={{
						[styles.header]: true,
						[styles.headerDrawerOpen]: isProjectDrawerOpen(),
					}}
					data-slot="workspace-sidebar-header"
					data-drawer-open={isProjectDrawerOpen() ? "true" : "false"}
				>
					<div class={styles.headerActions} data-slot="workspace-sidebar-header-actions">
						<button
							type="button"
							classList={{
								[styles.headerActionButton]: true,
								[styles.headerCollapseButton]: true,
							}}
							aria-label={railToggleLabel()}
							title={railToggleLabel()}
							data-slot="workspace-sidebar-rail-toggle"
							onClick={props.onToggleRailCollapse}
						>
							{props.railCollapsed ? <ChevronRight size={16} strokeWidth={2} /> : <ChevronLeft size={16} strokeWidth={2} />}
						</button>

					<For each={workspaceSidebarHeaderActions}>
						{(action): JSX.Element => {
							const label =
								action.id === "toggle-workspace-folders"
										? workspaceFolderToggleLabel()
										: action.label;
							const Icon =
								action.id === "toggle-workspace-folders"
										? areAllFoldersCollapsed()
											? UnfoldVertical
											: ListCollapse
										: action.icon;

							return (
								<button
									type="button"
									class={styles.headerActionButton}
									aria-label={label}
									title={label}
									data-slot="workspace-sidebar-header-action"
									data-action-id={action.id}
									disabled={action.id === "toggle-workspace-folders" && totalFolderCount() === 0}
									onClick={(): void => handleHeaderActionClick(action.id)}
									>
										<Icon size={16} strokeWidth={2} />
									</button>
								);
							}}
						</For>
					</div>

					<div class={styles.headerControls} data-slot="workspace-sidebar-header-controls">
						<ProjectSelector
							compact={props.collapsed}
							isOpen={isProjectDrawerOpen()}
							onToggle={(): void => {
								setIsProjectDrawerOpen(true);
							}}
							onClose={(): void => {
								setIsProjectDrawerOpen(false);
							}}
						/>
					</div>
				</div>

				<div
					classList={{
						[styles.section]: true,
						[styles.sectionHidden]: isProjectDrawerOpen(),
					}}
					data-slot="workspace-sidebar-section"
				>
					<Show when={!props.collapsed}>
						<span class={styles.sectionLabel}>Workspace</span>
					</Show>
					<div class={styles.navScroller} data-slot="workspace-sidebar-nav-scroller">
						<ul class={styles.navList} role="list" data-slot="workspace-static-list">
							<For each={workspaceStaticItems}>
								{(item): JSX.Element => (
									<WorkspaceHomeEntry
										item={item}
										onOpenContextMenu={contextMenu.openMenu}
										onOpenContextMenuFromKeyboard={contextMenu.openMenuFromElement}
									/>
								)}
							</For>
						</ul>

						<Show when={!props.collapsed}>
							<div class={styles.treeSectionLabel}>Items</div>
						</Show>

						<div data-slot="workspace-tree-root">
							<WorkspaceTreeBranch
								nodes={workspaceTreeNodes()}
								parentId={null}
								isFolderCollapsed={isFolderCollapsed}
								onToggleFolder={toggleFolder}
								pendingFolderDraft={pendingFolderDraft()}
								pendingFolderName={pendingFolderName()}
								onPendingFolderNameChange={setPendingFolderName}
								onSubmitPendingFolder={submitPendingFolder}
								onCancelPendingFolder={cancelPendingFolder}
								pendingFolderRename={pendingFolderRename()}
								pendingFolderRenameName={pendingFolderRenameName()}
								onPendingFolderRenameChange={setPendingFolderRenameName}
								onSubmitPendingFolderRename={submitPendingFolderRename}
								onCancelPendingFolderRename={cancelPendingFolderRename}
								onOpenContextMenu={contextMenu.openMenu}
								onOpenContextMenuFromKeyboard={contextMenu.openMenuFromElement}
								onNodePointerDown={handleNodePointerDown}
								onNodePointerMove={handleNodePointerMove}
								dragState={dragState()}
								isTreeClickSuppressed={suppressNextTreeClick}
							/>
						</div>
					</div>
				</div>
			</aside>

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