"use client";

import {
	AlertCircle,
	ArrowRight,
	ArrowUpCircle,
	CheckCircle2,
	ChevronRight,
	CornerLeftUp,
	Folder,
	FolderOpen,
	FolderRoot,
	GitBranch,
	Layers,
	Loader2,
	Plus,
	Search,
	Sparkles,
	X,
} from "lucide-react";
import Image from "next/image";
import {
	useCallback,
	useEffect,
	useRef,
	useState,
	useSyncExternalStore,
} from "react";
import { Button } from "@/components/ui/button";
import {
	Dialog,
	DialogContent,
	DialogDescription,
	DialogFooter,
	DialogHeader,
	DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
	Sheet,
	SheetContent,
	SheetDescription,
	SheetHeader,
	SheetTitle,
} from "@/components/ui/sheet";
import { authFetch } from "@/lib/auth";
import { useProjectStoreManager } from "@/lib/project-store-manager";
import {
	formatCost,
	getCurrentSlice,
	getLiveAutoDashboard,
	getLiveWorkspaceIndex,
	useSFWorkspaceState,
} from "@/lib/sf-workspace-store";
import { cn } from "@/lib/utils";

// ─── Types (mirroring server-side ProjectMetadata) ─────────────────────────

type ProjectDetectionKind =
	| "active-sf"
	| "empty-sf"
	| "v1-legacy"
	| "brownfield"
	| "blank";

interface ProjectDetectionSignals {
	hasSfFolder: boolean;
	hasPlanningFolder: boolean;
	hasGitRepo: boolean;
	hasPackageJson: boolean;
	fileCount: number;
	hasMilestones?: boolean;
	hasCargo?: boolean;
	hasGoMod?: boolean;
	hasPyproject?: boolean;
	isMonorepo?: boolean;
}

interface ProjectProgressInfo {
	activeMilestone: string | null;
	activeSlice: string | null;
	phase: string | null;
	milestonesCompleted: number;
	milestonesTotal: number;
}

interface ProjectMetadata {
	name: string;
	path: string;
	kind: ProjectDetectionKind;
	signals: ProjectDetectionSignals;
	lastModified: number;
	progress?: ProjectProgressInfo | null;
}

// ─── Kind style config ─────────────────────────────────────────────────

const KIND_STYLE: Record<
	ProjectDetectionKind,
	{ label: string; color: string; bgClass: string; icon: typeof Layers }
> = {
	"active-sf": {
		label: "Active",
		color: "text-success",
		bgClass: "bg-success/10",
		icon: Layers,
	},
	"empty-sf": {
		label: "Initialized",
		color: "text-info",
		bgClass: "bg-info/10",
		icon: FolderOpen,
	},
	brownfield: {
		label: "Existing",
		color: "text-warning",
		bgClass: "bg-warning/10",
		icon: GitBranch,
	},
	"v1-legacy": {
		label: "Legacy",
		color: "text-warning",
		bgClass: "bg-warning/10",
		icon: ArrowUpCircle,
	},
	blank: {
		label: "New",
		color: "text-muted-foreground",
		bgClass: "bg-foreground/[0.04]",
		icon: Sparkles,
	},
};

function techStack(signals: ProjectDetectionSignals): string[] {
	const tags: string[] = [];
	if (signals.isMonorepo) tags.push("Monorepo");
	if (signals.hasGitRepo) tags.push("Git");
	if (signals.hasPackageJson) tags.push("Node.js");
	if (signals.hasCargo) tags.push("Rust");
	if (signals.hasGoMod) tags.push("Go");
	if (signals.hasPyproject) tags.push("Python");
	return tags;
}

function progressLabel(p: ProjectProgressInfo): string | null {
	if (p.milestonesTotal === 0) return null;
	const parts: string[] = [];
	if (p.activeMilestone) parts.push(p.activeMilestone);
	if (p.activeSlice) parts.push(p.activeSlice);
	if (p.phase) parts.push(p.phase);
	return parts.join(" · ") || null;
}

function relativeTime(timestamp: number): string {
	const now = Date.now();
	const diffMs = now - timestamp;
	if (diffMs < 60_000) return "just now";
	const minutes = Math.floor(diffMs / 60_000);
	if (minutes < 60) return `${minutes}m ago`;
	const hours = Math.floor(minutes / 60);
	if (hours < 24) return `${hours}h ago`;
	const days = Math.floor(hours / 24);
	if (days < 30) return `${days}d ago`;
	return new Date(timestamp).toLocaleDateString(undefined, {
		month: "short",
		day: "numeric",
	});
}

// ─── Shared project card component ─────────────────────────────────────

function ProjectCard({
	project,
	isActive = false,
	onClick,
	disabled = false,
}: {
	project: ProjectMetadata;
	isActive?: boolean;
	onClick: () => void;
	disabled?: boolean;
}) {
	const style = KIND_STYLE[project.kind];
	const KindIcon = style.icon;
	const stack = techStack(project.signals);
	const progress = project.progress ? progressLabel(project.progress) : null;
	const milestoneCount = project.progress
		? `${project.progress.milestonesCompleted}/${project.progress.milestonesTotal}`
		: null;

	return (
		<button
			type="button"
			onClick={onClick}
			disabled={disabled}
			className={cn(
				"group flex w-full items-start gap-3.5 rounded-xl border px-4 py-3.5 text-left transition-all duration-200",
				"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
				"active:scale-[0.98]",
				isActive
					? "border-primary/30 bg-primary/[0.08]"
					: "border-border/50 bg-card/50 hover:border-foreground/15 hover:bg-card/50",
				disabled && "opacity-40 pointer-events-none",
			)}
		>
			{/* Icon */}
			<div
				className={cn(
					"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg mt-0.5",
					isActive ? "bg-primary/15" : style.bgClass,
				)}
			>
				{isActive ? (
					<CheckCircle2 className="h-4 w-4 text-primary" />
				) : (
					<KindIcon className={cn("h-4 w-4", style.color)} />
				)}
			</div>

			{/* Content */}
			<div className="min-w-0 flex-1">
				{/* Row 1: name + kind badge */}
				<div className="flex items-center gap-2">
					<span className="text-sm font-semibold text-foreground truncate">
						{project.name}
					</span>
					<span
						className={cn(
							"text-[10px] font-medium shrink-0",
							isActive ? "text-primary" : style.color,
						)}
					>
						{isActive ? "Current" : style.label}
					</span>
				</div>

				{/* Row 2: tech stack tags */}
				{stack.length > 0 && (
					<div className="mt-1 flex items-center gap-1.5">
						{stack.map((tag) => (
							<span
								key={tag}
								className="rounded bg-foreground/[0.06] px-1.5 py-0.5 text-[10px] text-muted-foreground"
							>
								{tag}
							</span>
						))}
					</div>
				)}

				{/* Row 3: progress info */}
				{progress && (
					<div className="mt-1.5 text-[11px] text-muted-foreground">
						{progress}
					</div>
				)}

				{/* Row 4: milestone progress bar */}
				{project.progress && project.progress.milestonesTotal > 0 && (
					<div className="mt-2 flex items-center gap-2">
						<div className="h-1 flex-1 overflow-hidden rounded-full bg-foreground/[0.08]">
							<div
								className="h-full rounded-full bg-success/70 transition-all"
								style={{
									width: `${Math.round(
										(project.progress.milestonesCompleted /
											project.progress.milestonesTotal) *
											100,
									)}%`,
								}}
							/>
						</div>
						<span className="text-[10px] tabular-nums text-muted-foreground">
							{milestoneCount}
						</span>
					</div>
				)}
			</div>

			{/* Arrow */}
			<ArrowRight className="mt-1 h-4 w-4 shrink-0 text-muted-foreground/50 transition-all group-hover:text-muted-foreground group-hover:translate-x-0.5" />
		</button>
	);
}

// ─── ProjectsPanel (slide-out sheet from sidebar) ──────────────────────

export function ProjectsPanel({
	open,
	onOpenChange,
}: {
	open: boolean;
	onOpenChange: (open: boolean) => void;
}) {
	const manager = useProjectStoreManager();
	const activeProjectCwd = useSyncExternalStore(
		manager.subscribe,
		manager.getSnapshot,
		manager.getSnapshot,
	);

	const [projects, setProjects] = useState<ProjectMetadata[]>([]);
	const [devRoot, setDevRoot] = useState<string | null>(null);
	const [loading, setLoading] = useState(true);
	const [error, setError] = useState<string | null>(null);

	const loadProjects = useCallback(async (root: string) => {
		const projRes = await authFetch(
			`/api/projects?root=${encodeURIComponent(root)}&detail=true`,
		);
		if (!projRes.ok)
			throw new Error(`Failed to discover projects: ${projRes.status}`);
		return (await projRes.json()) as ProjectMetadata[];
	}, []);

	// Load projects when panel opens
	useEffect(() => {
		if (!open) return;
		let cancelled = false;

		async function load() {
			setLoading(true);
			setError(null);
			try {
				const prefsRes = await authFetch("/api/preferences");
				if (!prefsRes.ok)
					throw new Error(`Failed to load preferences: ${prefsRes.status}`);
				const prefs = await prefsRes.json();

				if (!prefs.devRoot) {
					setDevRoot(null);
					setProjects([]);
					setLoading(false);
					return;
				}

				setDevRoot(prefs.devRoot);
				const discovered = await loadProjects(prefs.devRoot);
				if (!cancelled) {
					setProjects(discovered);
				}
			} catch (err) {
				if (!cancelled) {
					setError(err instanceof Error ? err.message : "Unknown error");
				}
			} finally {
				if (!cancelled) setLoading(false);
			}
		}

		load();
		return () => {
			cancelled = true;
		};
	}, [open, loadProjects]);

	const handleDevRootSaved = useCallback(async (newRoot: string) => {
		setLoading(true);
		setError(null);
		try {
			// Validate path and persist in a single call
			const res = await authFetch("/api/switch-root", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ devRoot: newRoot }),
			});

			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				throw new Error(
					(body as { error?: string }).error ??
						`Request failed (${res.status})`,
				);
			}

			const data = (await res.json()) as {
				devRoot: string;
				projects: ProjectMetadata[];
			};
			setDevRoot(data.devRoot);
			setProjects(data.projects);
		} catch (err) {
			setError(
				err instanceof Error ? err.message : "Failed to switch project root",
			);
		} finally {
			setLoading(false);
		}
	}, []);

	const [newProjectOpen, setNewProjectOpen] = useState(false);
	const [changeRootOpen, setChangeRootOpen] = useState(false);
	const _workspaceState = useSFWorkspaceState();

	const handleProjectCreated = useCallback(
		(newProject: ProjectMetadata) => {
			setProjects((prev) =>
				[...prev, newProject].sort((a, b) => a.name.localeCompare(b.name)),
			);
			setNewProjectOpen(false);
			handleSelectProject(newProject);
		},
		[handleSelectProject],
	);

	function handleSelectProject(project: ProjectMetadata) {
		// Already active — just close the panel
		if (activeProjectCwd === project.path) {
			onOpenChange(false);
			return;
		}

		// Close panel immediately — boot happens in the background with a
		// loading toast managed by WorkspaceChrome
		onOpenChange(false);
		manager.switchProject(project.path);
	}

	// Sort: active-sf first, then by name
	const sortedProjects = [...projects].sort((a, b) => {
		const kindOrder: Record<ProjectDetectionKind, number> = {
			"active-sf": 0,
			"empty-sf": 1,
			brownfield: 2,
			"v1-legacy": 3,
			blank: 4,
		};
		const ka = kindOrder[a.kind] ?? 5;
		const kb = kindOrder[b.kind] ?? 5;
		if (ka !== kb) return ka - kb;
		return a.name.localeCompare(b.name);
	});

	// ─── Content for the various states ──────────────────────────────

	let content: React.ReactNode;

	if (loading) {
		content = (
			<div className="flex items-center justify-center gap-2 py-16 text-xs text-muted-foreground">
				<Loader2 className="h-4 w-4 animate-spin" />
				Discovering projects…
			</div>
		);
	} else if (error) {
		content = (
			<div className="flex flex-col items-center gap-3 px-5 py-16 text-center">
				<AlertCircle className="h-8 w-8 text-destructive" />
				<p className="text-sm text-destructive">{error}</p>
			</div>
		);
	} else if (!devRoot) {
		content = <DevRootSetup onSaved={handleDevRootSaved} />;
	} else if (sortedProjects.length === 0) {
		content = (
			<div className="flex flex-col items-center gap-4 px-5 py-16 text-center">
				<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-muted">
					<FolderOpen className="h-7 w-7 text-muted-foreground" />
				</div>
				<div className="space-y-2">
					<h3 className="text-base font-semibold text-foreground">
						No projects found
					</h3>
					<p className="text-sm text-muted-foreground leading-relaxed">
						No project directories discovered in{" "}
						<code className="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">
							{devRoot}
						</code>
					</p>
				</div>
			</div>
		);
	} else {
		content = (
			<div className="space-y-2">
				{/* Project cards */}
				{sortedProjects.map((project) => (
					<ProjectCard
						key={project.path}
						project={project}
						isActive={activeProjectCwd === project.path}
						onClick={() => handleSelectProject(project)}
					/>
				))}

				{/* Create new project button */}
				<button
					type="button"
					onClick={() => setNewProjectOpen(true)}
					className={cn(
						"flex w-full items-center gap-3.5 rounded-xl border border-dashed px-4 py-3.5 text-left transition-all duration-200",
						"border-border/50 text-muted-foreground hover:border-foreground/15 hover:text-foreground",
						"active:scale-[0.98]",
					)}
				>
					<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-foreground/[0.04]">
						<Plus className="h-4 w-4" />
					</div>
					<div>
						<span className="text-sm font-medium">Create new project</span>
						<p className="mt-0.5 text-[11px] text-muted-foreground">
							Initialize a new directory with Git
						</p>
					</div>
				</button>

				{/* New project dialog */}
				<NewProjectDialog
					open={newProjectOpen}
					onOpenChange={setNewProjectOpen}
					devRoot={devRoot}
					existingNames={projects.map((p) => p.name)}
					onCreated={handleProjectCreated}
				/>
			</div>
		);
	}

	return (
		<Sheet open={open} onOpenChange={onOpenChange}>
			<SheetContent
				side="left"
				className="flex h-full w-full flex-col p-0 sm:max-w-[420px]"
				data-testid="projects-panel"
			>
				<SheetHeader className="sr-only">
					<SheetTitle>Projects</SheetTitle>
					<SheetDescription>
						Switch between projects or create a new one
					</SheetDescription>
				</SheetHeader>

				{/* Visible header */}
				<div className="flex items-center justify-between border-b border-border/50 px-5 py-4">
					<div>
						<h2 className="text-base font-semibold text-foreground">
							Projects
						</h2>
						{devRoot && !loading && (
							<div className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
								<code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px] truncate max-w-[200px]">
									{devRoot}
								</code>
								<button
									type="button"
									onClick={() => setChangeRootOpen(true)}
									className="shrink-0 text-[10px] text-primary hover:text-primary/80 transition-colors font-medium"
									data-testid="projects-panel-change-root"
								>
									Change
								</button>
								<span className="text-muted-foreground">·</span>
								<span>
									{projects.length} project{projects.length !== 1 ? "s" : ""}
								</span>
							</div>
						)}
					</div>
					<Button
						variant="ghost"
						size="icon"
						className="h-8 w-8 shrink-0"
						onClick={() => onOpenChange(false)}
					>
						<X className="h-4 w-4" />
					</Button>
				</div>

				{/* Scrollable project list */}
				<ScrollArea className="min-h-0 flex-1">
					<div className="px-5 py-4">{content}</div>
				</ScrollArea>

				{/* Folder picker for changing dev root */}
				<FolderPickerDialog
					open={changeRootOpen}
					onOpenChange={setChangeRootOpen}
					onSelect={(path) => void handleDevRootSaved(path)}
					initialPath={devRoot}
				/>
			</SheetContent>
		</Sheet>
	);
}

// ─── Active project inline summary (compact for panel card) ────────────

function _ActiveProjectSummary({
	workspaceState,
}: {
	workspaceState: ReturnType<typeof useSFWorkspaceState>;
}) {
	const workspace = getLiveWorkspaceIndex(workspaceState);
	const dashboard = getLiveAutoDashboard(workspaceState);
	const currentSlice = getCurrentSlice(workspace);

	if (!workspace) return null;

	const activeMilestone = workspace.milestones.find(
		(m) => m.id === workspace.active.milestoneId,
	);
	const cost = dashboard?.totalCost ?? 0;

	const parts: string[] = [];
	if (activeMilestone) parts.push(activeMilestone.id);
	if (currentSlice) parts.push(currentSlice.id);
	if (cost > 0) parts.push(formatCost(cost));

	if (parts.length === 0) return null;

	return (
		<div className="mt-1.5 text-[11px] text-muted-foreground">
			{parts.join(" · ")}
		</div>
	);
}

// ─── New Project Dialog ────────────────────────────────────────────────

function NewProjectDialog({
	open,
	onOpenChange,
	devRoot,
	existingNames,
	onCreated,
}: {
	open: boolean;
	onOpenChange: (open: boolean) => void;
	devRoot: string;
	existingNames: string[];
	onCreated: (project: ProjectMetadata) => void;
}) {
	const [name, setName] = useState("");
	const [creating, setCreating] = useState(false);
	const [error, setError] = useState<string | null>(null);
	const inputRef = useRef<HTMLInputElement>(null);

	useEffect(() => {
		if (open) {
			// eslint-disable-next-line react-hooks/set-state-in-effect -- reset form state on dialog open
			setName("");
			setError(null);
			setCreating(false);
			const t = setTimeout(() => inputRef.current?.focus(), 100);
			return () => clearTimeout(t);
		}
	}, [open]);

	const nameValid = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
	const nameConflict = existingNames.includes(name);
	const canSubmit = name.length > 0 && nameValid && !nameConflict && !creating;

	const validationHint = (() => {
		if (!name) return null;
		if (nameConflict) return "A project with this name already exists";
		if (!nameValid)
			return "Use letters, numbers, hyphens, underscores, dots. Must start with a letter or number.";
		return null;
	})();

	async function handleCreate() {
		if (!canSubmit) return;
		setCreating(true);
		setError(null);
		try {
			const res = await authFetch("/api/projects", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ devRoot, name }),
			});
			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				throw new Error(
					(body as { error?: string }).error ?? `Failed (${res.status})`,
				);
			}
			const project = (await res.json()) as ProjectMetadata;
			onCreated(project);
		} catch (err) {
			setError(err instanceof Error ? err.message : "Failed to create project");
			setCreating(false);
		}
	}

	return (
		<Dialog open={open} onOpenChange={onOpenChange}>
			<DialogContent className="sm:max-w-md">
				<DialogHeader>
					<DialogTitle>New Project</DialogTitle>
					<DialogDescription>
						Create a new project directory in{" "}
						<code className="rounded bg-muted px-1 py-0.5 text-xs font-mono">
							{devRoot}
						</code>
					</DialogDescription>
				</DialogHeader>

				<form
					onSubmit={(e) => {
						e.preventDefault();
						void handleCreate();
					}}
					className="space-y-4 py-2"
				>
					<div className="space-y-2">
						<Label htmlFor="project-name">Project name</Label>
						<Input
							ref={inputRef}
							id="project-name"
							placeholder="my-project"
							value={name}
							onChange={(e) => {
								setName(e.target.value);
								setError(null);
							}}
							autoComplete="off"
							aria-invalid={!!validationHint}
						/>
						{validationHint && (
							<p className="text-xs text-destructive">{validationHint}</p>
						)}
						{error && <p className="text-xs text-destructive">{error}</p>}
						{name && nameValid && !nameConflict && (
							<p className="text-xs text-muted-foreground font-mono">
								{devRoot}/{name}
							</p>
						)}
					</div>
				</form>

				<DialogFooter>
					<Button
						variant="ghost"
						size="sm"
						onClick={() => onOpenChange(false)}
						disabled={creating}
					>
						Cancel
					</Button>
					<Button
						size="sm"
						onClick={() => void handleCreate()}
						disabled={!canSubmit}
						className="gap-1.5"
					>
						{creating ? (
							<Loader2 className="h-3.5 w-3.5 animate-spin" />
						) : (
							<Plus className="h-3.5 w-3.5" />
						)}
						Create
					</Button>
				</DialogFooter>
			</DialogContent>
		</Dialog>
	);
}

// ─── Folder Picker Dialog ───────────────────────────────────────────────

interface BrowseEntry {
	name: string;
	path: string;
}

interface BrowseResult {
	current: string;
	parent: string | null;
	entries: BrowseEntry[];
}

function FolderPickerDialog({
	open,
	onOpenChange,
	onSelect,
	initialPath,
}: {
	open: boolean;
	onOpenChange: (open: boolean) => void;
	onSelect: (path: string) => void;
	initialPath?: string | null;
}) {
	const [currentPath, setCurrentPath] = useState<string>("");
	const [parentPath, setParentPath] = useState<string | null>(null);
	const [entries, setEntries] = useState<BrowseEntry[]>([]);
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState<string | null>(null);

	const browse = useCallback(async (targetPath?: string) => {
		setLoading(true);
		setError(null);
		try {
			const param = targetPath ? `?path=${encodeURIComponent(targetPath)}` : "";
			const res = await authFetch(`/api/browse-directories${param}`);
			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				throw new Error((body as { error?: string }).error ?? `${res.status}`);
			}
			const data: BrowseResult = await res.json();
			setCurrentPath(data.current);
			setParentPath(data.parent);
			setEntries(data.entries);
		} catch (err) {
			setError(err instanceof Error ? err.message : "Failed to browse");
		} finally {
			setLoading(false);
		}
	}, []);

	useEffect(() => {
		if (open) {
			// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
			void browse(initialPath ?? undefined);
		}
	}, [open, initialPath, browse]);

	return (
		<Dialog open={open} onOpenChange={onOpenChange}>
			<DialogContent className="sm:max-w-lg gap-0 p-0 overflow-hidden">
				<DialogHeader className="px-5 pt-5 pb-3">
					<DialogTitle className="text-base">Choose Folder</DialogTitle>
					<DialogDescription className="text-xs">
						Navigate to the folder that contains your project directories.
					</DialogDescription>
				</DialogHeader>

				<div className="border-y border-border/50 bg-muted/50 px-5 py-2">
					<p
						className="font-mono text-xs text-muted-foreground truncate"
						title={currentPath}
					>
						{currentPath}
					</p>
				</div>

				<ScrollArea className="h-[320px]">
					<div className="px-2 py-1">
						{loading && (
							<div className="flex items-center justify-center py-12">
								<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
							</div>
						)}

						{error && (
							<div className="px-3 py-4 text-center text-xs text-destructive">
								{error}
							</div>
						)}

						{!loading && !error && (
							<>
								{parentPath && (
									<button
										type="button"
										onClick={() => void browse(parentPath)}
										className="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent/50"
									>
										<CornerLeftUp className="h-4 w-4 text-muted-foreground shrink-0" />
										<span className="text-muted-foreground">..</span>
									</button>
								)}

								{entries.map((entry) => (
									<button
										type="button"
										key={entry.path}
										onClick={() => void browse(entry.path)}
										className="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent/50 group"
									>
										<Folder className="h-4 w-4 text-muted-foreground shrink-0" />
										<span className="text-foreground truncate flex-1">
											{entry.name}
										</span>
										<ChevronRight className="h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
									</button>
								))}

								{!parentPath && entries.length === 0 && (
									<div className="px-3 py-8 text-center text-xs text-muted-foreground">
										No subdirectories
									</div>
								)}
							</>
						)}
					</div>
				</ScrollArea>

				<DialogFooter className="border-t border-border/50 px-5 py-3">
					<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
						Cancel
					</Button>
					<Button
						size="sm"
						onClick={() => {
							onSelect(currentPath);
							onOpenChange(false);
						}}
						disabled={!currentPath}
						className="gap-1.5"
					>
						<FolderOpen className="h-3.5 w-3.5" />
						Select This Folder
					</Button>
				</DialogFooter>
			</DialogContent>
		</Dialog>
	);
}

// ─── Dev Root Setup Component ───────────────────────────────────────────

function DevRootSetup({
	onSaved,
	currentRoot,
}: {
	onSaved: (root: string) => void;
	currentRoot?: string | null;
}) {
	const [saving, setSaving] = useState(false);
	const [error, setError] = useState<string | null>(null);
	const [success, setSuccess] = useState(false);
	const [pickerOpen, setPickerOpen] = useState(false);

	const handleSave = useCallback(
		async (selectedPath: string) => {
			setSaving(true);
			setError(null);
			setSuccess(false);

			try {
				const res = await authFetch("/api/preferences", {
					method: "PUT",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ devRoot: selectedPath }),
				});

				if (!res.ok) {
					const body = await res.json().catch(() => ({}));
					throw new Error(
						(body as { error?: string }).error ??
							`Request failed (${res.status})`,
					);
				}

				setSuccess(true);
				onSaved(selectedPath);
			} catch (err) {
				setError(
					err instanceof Error ? err.message : "Failed to save preference",
				);
			} finally {
				setSaving(false);
			}
		},
		[onSaved],
	);

	const isCompact = !!currentRoot;

	if (isCompact) {
		return (
			<div className="space-y-3" data-testid="devroot-settings">
				<div className="flex items-center gap-2">
					<code className="flex-1 truncate rounded border border-border/50 bg-muted/50 px-3 py-2 font-mono text-xs text-foreground">
						{currentRoot}
					</code>
					<Button
						size="sm"
						variant="outline"
						onClick={() => setPickerOpen(true)}
						disabled={saving}
						className="h-9 gap-1.5 shrink-0"
					>
						{saving ? (
							<Loader2 className="h-3.5 w-3.5 animate-spin" />
						) : success ? (
							<CheckCircle2 className="h-3.5 w-3.5 text-success" />
						) : (
							<>
								<FolderOpen className="h-3.5 w-3.5" />
								Change
							</>
						)}
					</Button>
				</div>

				{error && <p className="text-xs text-destructive">{error}</p>}
				{success && <p className="text-xs text-success">Dev root updated</p>}

				<FolderPickerDialog
					open={pickerOpen}
					onOpenChange={setPickerOpen}
					onSelect={(path) => void handleSave(path)}
					initialPath={currentRoot}
				/>
			</div>
		);
	}

	// Inline setup for first-time configuration
	return (
		<div className="rounded-md border border-border bg-card p-6">
			<div className="flex items-start gap-4">
				<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-border bg-accent">
					<FolderRoot className="h-5 w-5 text-muted-foreground" />
				</div>
				<div className="min-w-0 flex-1">
					<h3 className="text-sm font-semibold text-foreground">
						Set your development root
					</h3>
					<p className="mt-1 text-xs text-muted-foreground leading-relaxed">
						Point SF at the folder that contains your project directories. It
						scans one level deep.
					</p>
					<Button
						onClick={() => setPickerOpen(true)}
						disabled={saving}
						size="sm"
						className="mt-3 gap-2"
						data-testid="projects-devroot-browse"
					>
						{saving ? (
							<Loader2 className="h-3.5 w-3.5 animate-spin" />
						) : (
							<>
								<FolderOpen className="h-3.5 w-3.5" />
								Browse
							</>
						)}
					</Button>
					{error && <p className="mt-2 text-xs text-destructive">{error}</p>}
				</div>
			</div>

			<FolderPickerDialog
				open={pickerOpen}
				onOpenChange={setPickerOpen}
				onSelect={(path) => void handleSave(path)}
			/>
		</div>
	);
}

// ─── Exported Dev Root Section for Settings ──────────────────────────────

export function DevRootSettingsSection() {
	const [devRoot, setDevRoot] = useState<string | null>(null);
	const [loading, setLoading] = useState(true);

	useEffect(() => {
		authFetch("/api/preferences")
			.then((r) => r.json())
			.then((prefs) => setDevRoot(prefs.devRoot ?? null))
			.catch(() => setDevRoot(null))
			.finally(() => setLoading(false));
	}, []);

	if (loading) {
		return (
			<div className="flex items-center gap-2 py-4 text-xs text-muted-foreground">
				<Loader2 className="h-3.5 w-3.5 animate-spin" />
				Loading preferences…
			</div>
		);
	}

	return (
		<div className="space-y-3" data-testid="settings-devroot">
			<div className="flex items-center gap-2.5">
				<FolderRoot className="h-3.5 w-3.5 text-muted-foreground" />
				<h3 className="text-[13px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
					Development Root
				</h3>
			</div>
			<p className="text-xs text-muted-foreground leading-relaxed">
				The parent folder containing your project directories. SF scans one
				level deep for projects.
			</p>
			<DevRootSetup
				currentRoot={devRoot ?? ""}
				onSaved={(root) => setDevRoot(root)}
			/>
		</div>
	);
}

// ─── Project Selection Gate ─────────────────────────────────────────────
//
// Full-screen IDE-style welcome shown before any project is opened.
// Designed to feel like opening the app — not a wizard or onboarding flow.
// Mirrors the app shell layout: header bar, sidebar-width left column,
// project list as the main content area.

export function ProjectSelectionGate() {
	const manager = useProjectStoreManager();

	const [projects, setProjects] = useState<ProjectMetadata[]>([]);
	const [devRoot, setDevRoot] = useState<string | null>(null);
	const [loading, setLoading] = useState(true);
	const [error, setError] = useState<string | null>(null);
	const [newProjectOpen, setNewProjectOpen] = useState(false);
	const [changeRootOpen, setChangeRootOpen] = useState(false);
	const [filter, setFilter] = useState("");

	const loadProjects = useCallback(async (root: string) => {
		const projRes = await authFetch(
			`/api/projects?root=${encodeURIComponent(root)}&detail=true`,
		);
		if (!projRes.ok)
			throw new Error(`Failed to discover projects: ${projRes.status}`);
		return (await projRes.json()) as ProjectMetadata[];
	}, []);

	useEffect(() => {
		let cancelled = false;

		async function load() {
			setLoading(true);
			setError(null);
			try {
				const prefsRes = await authFetch("/api/preferences");
				if (!prefsRes.ok)
					throw new Error(`Failed to load preferences: ${prefsRes.status}`);
				const prefs = await prefsRes.json();

				if (!prefs.devRoot) {
					setDevRoot(null);
					setProjects([]);
					setLoading(false);
					return;
				}

				setDevRoot(prefs.devRoot);
				const discovered = await loadProjects(prefs.devRoot);
				if (!cancelled) setProjects(discovered);
			} catch (err) {
				if (!cancelled) {
					setError(err instanceof Error ? err.message : "Unknown error");
				}
			} finally {
				if (!cancelled) setLoading(false);
			}
		}

		load();
		return () => {
			cancelled = true;
		};
	}, [loadProjects]);

	useEffect(() => {
		if (loading || projects.length !== 1) return;
		const [onlyProject] = projects;
		if (onlyProject?.path === devRoot) {
			manager.switchProject(onlyProject.path);
		}
	}, [devRoot, loading, manager, projects]);

	const handleDevRootSaved = useCallback(async (newRoot: string) => {
		setLoading(true);
		setError(null);
		try {
			const res = await authFetch("/api/switch-root", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ devRoot: newRoot }),
			});

			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				throw new Error(
					(body as { error?: string }).error ??
						`Request failed (${res.status})`,
				);
			}

			const data = (await res.json()) as {
				devRoot: string;
				projects: ProjectMetadata[];
			};
			setDevRoot(data.devRoot);
			setProjects(data.projects);
		} catch (err) {
			setError(
				err instanceof Error ? err.message : "Failed to switch project root",
			);
		} finally {
			setLoading(false);
		}
	}, []);

	const handleProjectCreated = useCallback(
		(newProject: ProjectMetadata) => {
			setProjects((prev) =>
				[...prev, newProject].sort((a, b) => a.name.localeCompare(b.name)),
			);
			setNewProjectOpen(false);
			manager.switchProject(newProject.path);
		},
		[manager],
	);

	function handleSelectProject(project: ProjectMetadata) {
		manager.switchProject(project.path);
	}

	// Sort: active-sf first, then by name
	const sortedProjects = [...projects].sort((a, b) => {
		const kindOrder: Record<ProjectDetectionKind, number> = {
			"active-sf": 0,
			"empty-sf": 1,
			brownfield: 2,
			"v1-legacy": 3,
			blank: 4,
		};
		const ka = kindOrder[a.kind] ?? 5;
		const kb = kindOrder[b.kind] ?? 5;
		if (ka !== kb) return ka - kb;
		return a.name.localeCompare(b.name);
	});

	// Filter projects by name
	const filteredProjects = filter.trim()
		? sortedProjects.filter((p) =>
				p.name.toLowerCase().includes(filter.toLowerCase()),
			)
		: sortedProjects;

	const hasProjects = !loading && sortedProjects.length > 0;
	const showFilter = sortedProjects.length > 5;

	return (
		<div
			className="flex h-screen flex-col bg-background text-foreground"
			data-testid="project-selection-gate"
		>
			{/* ─── Main content ─── */}
			<div className="flex-1 overflow-y-auto">
				<div className="mx-auto max-w-2xl px-6 pt-16 pb-10 md:px-10 lg:pt-24">
					{/* ─── Logo + subtitle ─── */}
					<div className="flex flex-col items-center text-center mb-10">
						<Image
							src="/logo-black.svg"
							alt="SF"
							width={100}
							height={28}
							className="h-7 w-auto dark:hidden"
						/>
						<Image
							src="/logo-white.svg"
							alt="SF"
							width={100}
							height={28}
							className="h-7 w-auto hidden dark:block"
						/>
						<p className="mt-3 text-sm text-muted-foreground">
							Select a project to get started
						</p>
					</div>

					{/* Loading */}
					{loading && (
						<div className="flex items-center gap-3 py-20 justify-center text-sm text-muted-foreground">
							<Loader2 className="h-4 w-4 animate-spin" />
							Scanning for projects…
						</div>
					)}

					{/* Error */}
					{error && !loading && (
						<div className="rounded-md border border-destructive/20 bg-destructive/[0.06] px-4 py-3 text-sm text-destructive">
							{error}
						</div>
					)}

					{/* No dev root — show setup */}
					{!devRoot && !loading && !error && (
						<div className="space-y-6">
							<div>
								<h2 className="text-lg font-semibold tracking-tight text-foreground">
									Welcome to SF
								</h2>
								<p className="mt-1 text-sm text-muted-foreground">
									Set a development root to get started. SF will discover
									projects inside it.
								</p>
							</div>
							<DevRootSetup onSaved={handleDevRootSaved} />
						</div>
					)}

					{/* No projects found */}
					{devRoot && !loading && sortedProjects.length === 0 && !error && (
						<div className="space-y-6">
							<div>
								<h2 className="text-lg font-semibold tracking-tight text-foreground">
									No projects found
								</h2>
								<p className="mt-1 text-sm text-muted-foreground">
									No project directories were discovered. Create one to get
									started.
								</p>
							</div>
							<button
								type="button"
								onClick={() => setNewProjectOpen(true)}
								className="flex items-center gap-3 rounded-md border border-dashed border-border px-4 py-3 text-sm text-muted-foreground transition-colors hover:border-foreground/20 hover:text-foreground"
							>
								<Plus className="h-4 w-4" />
								Create a new project
							</button>
						</div>
					)}

					{/* ─── Project list ─── */}
					{hasProjects && (
						<div className="space-y-5">
							{/* Dev root + change button */}
							{devRoot && (
								<div className="flex items-center gap-2 text-xs text-muted-foreground">
									<FolderRoot className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
									<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground truncate">
										{devRoot}
									</code>
									<button
										type="button"
										onClick={() => setChangeRootOpen(true)}
										className="shrink-0 text-[11px] text-primary hover:text-primary/80 transition-colors font-medium"
										data-testid="gate-change-root"
									>
										Change
									</button>
								</div>
							)}

							{/* Filter + count */}
							<div className="flex items-center justify-between gap-4">
								<p className="text-xs text-muted-foreground tabular-nums">
									{sortedProjects.length} project
									{sortedProjects.length !== 1 ? "s" : ""}
								</p>
								{showFilter && (
									<div className="relative w-48">
										<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
										<input
											type="text"
											placeholder="Filter…"
											value={filter}
											onChange={(e) => setFilter(e.target.value)}
											className="h-8 w-full rounded-md border border-border bg-background pl-8 pr-3 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
										/>
									</div>
								)}
							</div>

							{/* Project rows — table-like, dense */}
							<div className="rounded-md border border-border bg-card overflow-hidden divide-y divide-border">
								{filteredProjects.map((project) => {
									const style = KIND_STYLE[project.kind] ?? KIND_STYLE.blank;
									const KindIcon = style.icon;
									const stack = techStack(project.signals);
									const progress = project.progress
										? progressLabel(project.progress)
										: null;
									const hasBar =
										project.progress && project.progress.milestonesTotal > 0;
									const pct = hasBar
										? Math.round(
												(project.progress!.milestonesCompleted /
													project.progress!.milestonesTotal) *
													100,
											)
										: 0;

									return (
										<button
											key={project.path}
											type="button"
											onClick={() => handleSelectProject(project)}
											className="group flex w-full items-center gap-4 px-4 py-3 text-left transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:bg-accent/50"
										>
											{/* Icon */}
											<div
												className={cn(
													"flex h-8 w-8 shrink-0 items-center justify-center rounded-md",
													style.bgClass,
												)}
											>
												<KindIcon className={cn("h-3.5 w-3.5", style.color)} />
											</div>

											{/* Name + metadata */}
											<div className="min-w-0 flex-1">
												<div className="flex items-center gap-2">
													<span className="text-sm font-medium text-foreground truncate">
														{project.name}
													</span>
													<span
														className={cn(
															"text-[10px] font-medium shrink-0",
															style.color,
														)}
													>
														{style.label}
													</span>
												</div>
												{/* Stack tags + progress on one line */}
												<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground">
													{stack.length > 0 && <span>{stack.join(" · ")}</span>}
													{stack.length > 0 && progress && (
														<span className="text-muted-foreground/50">—</span>
													)}
													{progress && (
														<span className="truncate">{progress}</span>
													)}
												</div>
											</div>

											{/* Progress bar (compact) */}
											{hasBar && (
												<div className="hidden sm:flex items-center gap-2 shrink-0 w-24">
													<div className="h-1 flex-1 overflow-hidden rounded-full bg-foreground/[0.08]">
														<div
															className="h-full rounded-full bg-success/70 transition-all"
															style={{ width: `${pct}%` }}
														/>
													</div>
													<span className="text-[10px] tabular-nums text-muted-foreground w-6 text-right">
														{project.progress!.milestonesCompleted}/
														{project.progress!.milestonesTotal}
													</span>
												</div>
											)}

											{/* Modified time */}
											{project.lastModified > 0 && (
												<span className="hidden lg:inline text-[10px] text-muted-foreground shrink-0 w-16 text-right tabular-nums">
													{relativeTime(project.lastModified)}
												</span>
											)}

											{/* Arrow */}
											<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground/50 transition-colors group-hover:text-muted-foreground" />
										</button>
									);
								})}

								{/* Empty filter state */}
								{filteredProjects.length === 0 && filter.trim() && (
									<div className="px-4 py-8 text-center text-xs text-muted-foreground">
										No projects matching &quot;{filter}&quot;
									</div>
								)}
							</div>

							{/* Create new row */}
							<button
								type="button"
								onClick={() => setNewProjectOpen(true)}
								className="flex items-center gap-3 rounded-md border border-dashed border-border px-4 py-2.5 text-sm text-muted-foreground transition-colors hover:border-foreground/20 hover:text-foreground w-full"
							>
								<Plus className="h-3.5 w-3.5" />
								New project
							</button>

							{devRoot && (
								<NewProjectDialog
									open={newProjectOpen}
									onOpenChange={setNewProjectOpen}
									devRoot={devRoot}
									existingNames={projects.map((p) => p.name)}
									onCreated={handleProjectCreated}
								/>
							)}
						</div>
					)}

					{/* Change root for "no projects" and "no devRoot" states */}
					{devRoot && !loading && sortedProjects.length === 0 && !error && (
						<div className="mt-4">
							<button
								type="button"
								onClick={() => setChangeRootOpen(true)}
								className="flex items-center gap-2 text-xs text-primary hover:text-primary/80 transition-colors font-medium"
								data-testid="gate-change-root-empty"
							>
								<FolderOpen className="h-3.5 w-3.5" />
								Change project root
							</button>
						</div>
					)}
				</div>
			</div>

			{/* Folder picker for changing dev root */}
			<FolderPickerDialog
				open={changeRootOpen}
				onOpenChange={setChangeRootOpen}
				onSelect={(path) => void handleDevRootSaved(path)}
				initialPath={devRoot}
			/>
		</div>
	);
}
