<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import { SvelteSet } from 'svelte/reactivity';
	import { page } from '$app/stores';
	import { goto } from '$app/navigation';
	import { flip } from 'svelte/animate';
	import { dndzone } from 'svelte-dnd-action';
	import { currentCollective } from '$lib/stores/collective';
	import { currentUser } from '$lib/stores/auth';
	import {
		loadItems,
		addItem,
		updateItem,
		checkItem,
		deleteItem,
		reorderItems,
		renameList,
		fetchSuggestions,
		bulkDeleteItems,
		bulkMoveItems,
		bulkCheckItems,
		loadLists,
		createList,
		lists as listsStore
	} from '$lib/stores/lists';
	import {
		subscribeToList,
		applyItemEvent,
		type RealtimeSubscription
	} from '$lib/stores/realtimeSync';
	import { enqueueOp, hydrateSyncState } from '$lib/sync';
	import { scheduleUndoable } from '$lib/sync/undoQueue';
	import SyncBanner from '$lib/components/SyncBanner.svelte';
	import { getSupabase } from '$lib/supabase';
	import { generateId } from '$lib/utils/id';
	import type { ShoppingItem, ShoppingList } from '@colectivo/types';
	import ItemSuggestions from '$lib/components/ItemSuggestions.svelte';
	import {
		ArrowLeft,
		GripVertical,
		Minus,
		Plus,
		Trash2,
		MoreHorizontal,
		RotateCcw,
		Check,
		CheckSquare,
		FolderInput,
		X
	} from 'lucide-svelte';
	import * as m from '$lib/paraglide/messages';

	const listId = $page.params.id!;

	// ── State ──────────────────────────────────────────────────────────────────

	let list = $state<ShoppingList | null>(null);
	let items = $state<ShoppingItem[]>([]);
	let loading = $state(true);

	// Inline rename of the list (replaces the old /lists sticky input flow).
	let listName = $state('');
	let nameSaveTimer: ReturnType<typeof setTimeout> | null = null;
	const NAME_SAVE_DEBOUNCE_MS = 500;

	// Add-item form
	let newName = $state('');
	let newQty = $state<number | null>(null);
	let nameInput: HTMLInputElement | undefined = $state();

	// Suggestions
	let suggestions = $state<Awaited<ReturnType<typeof fetchSuggestions>>>([]);
	let suggestionsTimer: ReturnType<typeof setTimeout>;

	// Double-tap edit overlay (replaces inline edit)
	let overlayItem = $state<ShoppingItem | null>(null);
	let overlayName = $state('');
	let lastTapById = new Map<string, number>();
	const DOUBLE_TAP_WINDOW_MS = 300;

	// Symmetric toggle swipe — replaces the old swipe-to-delete.
	// Unchecked + swipe right → check. Checked + swipe left → uncheck.
	// The opposite direction for each state is a no-op (snaps back).
	let swipeOffsets = $state<Record<string, number>>({});
	let swipeStartX = 0;
	let swipeStartY = 0;
	let swipeHorizontal = false;
	const SWIPE_REVEAL_WIDTH = 96;
	const SWIPE_COMMIT_THRESHOLD = 120;

	// Drag handles: the dndzone is disabled by default; pressing the handle
	// flips it to enabled for the duration of that interaction so swipes on
	// the rest of the row don't trigger drag, and vice versa.
	let dragEnabled = $state(false);

	function enableDragFromHandle(e: PointerEvent) {
		e.stopPropagation();
		dragEnabled = true;
	}

	function onDragEnd() {
		dragEnabled = false;
	}

	// ── Selection mode ────────────────────────────────────────────────────────
	// Entered via long-press on mobile or toggle button on desktop. While
	// active: single tap toggles selection, swipes + double-tap are disabled.

	let selectionMode = $state(false);
	let selectedIds = $state(new SvelteSet<string>());
	let longPressTimer: ReturnType<typeof setTimeout> | null = null;
	const LONG_PRESS_MS = 500;
	let showMovePicker = $state(false);

	const selectedCount = $derived(selectedIds.size);

	function enterSelection(initialId?: string) {
		selectionMode = true;
		if (initialId) selectedIds.add(initialId);
	}

	function exitSelection() {
		selectionMode = false;
		selectedIds.clear();
		showMovePicker = false;
	}

	function toggleSelection(id: string) {
		if (selectedIds.has(id)) selectedIds.delete(id);
		else selectedIds.add(id);
	}

	function startLongPress(item: ShoppingItem) {
		if (selectionMode) return;
		if (longPressTimer) clearTimeout(longPressTimer);
		longPressTimer = setTimeout(() => {
			longPressTimer = null;
			enterSelection(item.id);
		}, LONG_PRESS_MS);
	}

	function cancelLongPress() {
		if (longPressTimer) {
			clearTimeout(longPressTimer);
			longPressTimer = null;
		}
	}

	// ── Bulk actions ──────────────────────────────────────────────────────────

	async function handleBulkDelete() {
		if (!selectedIds.size) return;
		const ids = [...selectedIds];
		const snapshots = items.filter((i) => ids.includes(i.id)).map((i) => ({ ...i }));
		items = items.filter((i) => !ids.includes(i.id));
		exitSelection();
		scheduleUndoable({
			label: m.list_undo_bulk_delete({ n: String(ids.length) }),
			restore: () => {
				items = [...items, ...snapshots].sort((a, b) => a.sort_order - b.sort_order);
			},
			commit: async () => {
				await bulkDeleteItems(ids);
			}
		});
	}

	async function handleBulkCheck() {
		if (!$currentUser || !selectedIds.size) return;
		const ids = [...selectedIds];
		const now = new Date().toISOString();
		items = items.map((i) =>
			ids.includes(i.id) && !i.is_checked
				? { ...i, is_checked: true, checked_by: $currentUser!.id, checked_at: now }
				: i
		);
		exitSelection();
		await bulkCheckItems(ids, $currentUser.id);
	}

	async function handleBulkMove(targetListId: string) {
		if (!selectedIds.size) return;
		const ids = [...selectedIds];
		items = items.filter((i) => !ids.includes(i.id));
		exitSelection();
		await bulkMoveItems(ids, targetListId);
	}

	async function openMovePicker() {
		if (!$currentCollective) return;
		showMovePicker = true;
		await loadLists($currentCollective.id);
	}

	async function handleMoveToNewList() {
		if (!$currentCollective || !$currentUser) return;
		const created = await createList($currentCollective.id, '', $currentUser.id);
		if (created) await handleBulkMove(created.id);
	}

	// List action menu
	let showMenu = $state(false);

	// ── Derived ────────────────────────────────────────────────────────────────

	const uncheckedItems = $derived(items.filter((i) => !i.is_checked));
	const checkedItems = $derived(items.filter((i) => i.is_checked));

	// ── Load + Realtime ────────────────────────────────────────────────────────

	let realtimeSub: RealtimeSubscription | null = null;
	// Optimistic rows use client-side UUIDs ("tempId") until the server
	// confirms the real id. We track them so a Realtime INSERT echo can
	// atomically replace the optimistic row instead of appending a duplicate.
	const pendingTempIds = new Set<string>();

	onMount(async () => {
		const [listRes, itemsRes] = await Promise.all([
			getSupabase().from('shopping_lists').select('*').eq('id', listId).single(),
			loadItems(listId)
		]);

		if (listRes.error || !listRes.data) {
			goto('/lists');
			return;
		}

		list = listRes.data as ShoppingList;
		listName = list.name ?? '';
		items = itemsRes;
		loading = false;

		if ($currentCollective) {
			suggestions = await fetchSuggestions($currentCollective.id, '');
		}

		await hydrateSyncState();

		realtimeSub = await subscribeToList(listId, (evt) => {
			if (evt.type === 'INSERT') {
				const optimistic = items.find(
					(i) =>
						pendingTempIds.has(i.id) &&
						i.name === evt.row.name &&
						i.created_by === evt.row.created_by &&
						i.sort_order === evt.row.sort_order
				);
				if (optimistic) {
					items = items.map((i) => (i.id === optimistic.id ? evt.row : i));
					pendingTempIds.delete(optimistic.id);
					return;
				}
			}
			items = applyItemEvent(items, evt);
		});
	});

	onDestroy(async () => {
		await realtimeSub?.unsubscribe();
		if (nameSaveTimer) {
			clearTimeout(nameSaveTimer);
			await flushNameSave();
		}
	});

	// ── Suggestions ────────────────────────────────────────────────────────────

	async function updateSuggestions(prefix: string) {
		clearTimeout(suggestionsTimer);
		if (!$currentCollective) return;
		suggestionsTimer = setTimeout(async () => {
			suggestions = await fetchSuggestions($currentCollective!.id, prefix);
		}, 150);
	}

	$effect(() => {
		updateSuggestions(newName);
	});

	function selectSuggestion(name: string) {
		newName = name;
		nameInput?.focus();
	}

	// ── Inline rename ──────────────────────────────────────────────────────────

	function scheduleNameSave() {
		if (nameSaveTimer) clearTimeout(nameSaveTimer);
		nameSaveTimer = setTimeout(() => {
			nameSaveTimer = null;
			void flushNameSave();
		}, NAME_SAVE_DEBOUNCE_MS);
	}

	async function flushNameSave() {
		if (!list) return;
		const next = listName.trim();
		if (next === (list.name ?? '')) return;
		list = { ...list, name: next };
		await renameList(list.id, next);
	}

	// ── Add item ───────────────────────────────────────────────────────────────

	async function handleAdd() {
		const name = newName.trim();
		if (!name || !$currentUser) return;

		const sortOrder = items.length;
		const tempId = generateId();
		const optimistic: ShoppingItem = {
			id: tempId,
			list_id: listId,
			name,
			quantity: newQty,
			is_checked: false,
			checked_by: null,
			checked_at: null,
			sort_order: sortOrder,
			created_by: $currentUser.id,
			created_at: new Date().toISOString()
		};

		// Use tempId as the REAL server id. The server accepts a client-provided
		// UUID for `id` (see supabase/migrations/005 — `id uuid PRIMARY KEY
		// DEFAULT gen_random_uuid()` lets us override it). This makes the
		// insert idempotent: if the response is lost and we retry via the
		// offline queue, the second POST gets a 409 PK conflict (still desired
		// end-state), and the Realtime echo only fires once.
		pendingTempIds.add(tempId);
		items = [...items, optimistic];
		newName = '';
		newQty = null;
		nameInput?.focus();

		const real = await addItem(
			listId,
			name,
			newQty,
			$currentUser.id,
			sortOrder,
			tempId
		);
		if (real) {
			// tempId === real.id, so the map is a no-op; still dedupe in case
			// a Realtime echo beat us.
			const seen = new Set<string>();
			items = items.filter((i) => {
				if (seen.has(i.id)) return false;
				seen.add(i.id);
				return true;
			});
			pendingTempIds.delete(tempId);
		} else {
			// Network failure — keep optimistic row, enqueue the insert with
			// the same id so the retry is idempotent.
			await enqueueOp({
				kind: 'insert',
				table: 'shopping_items',
				payload: {
					id: tempId,
					list_id: listId,
					name,
					quantity: newQty,
					sort_order: sortOrder,
					created_by: $currentUser.id
				}
			});
		}
	}

	function handleAddKeydown(e: KeyboardEvent) {
		if (e.key === 'Enter') handleAdd();
	}

	// ── Check / uncheck ────────────────────────────────────────────────────────

	async function handleCheck(item: ShoppingItem) {
		if (!$currentUser) return;
		const checked = !item.is_checked;
		items = items.map((i) =>
			i.id === item.id
				? {
						...i,
						is_checked: checked,
						checked_by: checked ? $currentUser!.id : null,
						checked_at: checked ? new Date().toISOString() : null
					}
				: i
		);
		await checkItem(item.id, $currentUser.id, checked);
	}

	// ── Quantity stepper ───────────────────────────────────────────────────────

	async function adjustQty(item: ShoppingItem, delta: number) {
		const current = item.quantity ?? 1;
		const next = Math.max(1, current + delta);
		items = items.map((i) => (i.id === item.id ? { ...i, quantity: next } : i));
		await updateItem(item.id, { quantity: next });
	}

	// ── Edit overlay (double-tap) ──────────────────────────────────────────────

	function onRowTap(item: ShoppingItem) {
		// Single tap: no-op. Double-tap (two presses within DOUBLE_TAP_WINDOW_MS)
		// opens the edit overlay.
		const now = Date.now();
		const last = lastTapById.get(item.id) ?? 0;
		if (now - last <= DOUBLE_TAP_WINDOW_MS) {
			lastTapById.delete(item.id);
			openOverlay(item);
		} else {
			lastTapById.set(item.id, now);
		}
	}

	function openOverlay(item: ShoppingItem) {
		overlayItem = item;
		overlayName = item.name;
	}

	function closeOverlay() {
		overlayItem = null;
		overlayName = '';
	}

	async function overlayConfirm() {
		if (!overlayItem) return;
		const name = overlayName.trim();
		const target = overlayItem;
		closeOverlay();
		if (!name || name === target.name) return;
		items = items.map((i) => (i.id === target.id ? { ...i, name } : i));
		await updateItem(target.id, { name });
	}

	function overlayDelete() {
		if (!overlayItem) return;
		const id = overlayItem.id;
		closeOverlay();
		void handleDelete(id);
	}

	function handleOverlayKeydown(e: KeyboardEvent) {
		if (e.key === 'Enter') void overlayConfirm();
		if (e.key === 'Escape') closeOverlay();
	}

	// ── Delete (called from overlay + bulk actions) ────────────────────────────

	async function handleDelete(id: string) {
		const item = items.find((i) => i.id === id);
		if (!item) return;
		const snapshot = { ...item };
		items = items.filter((i) => i.id !== id);
		scheduleUndoable({
			label: m.undo_deleted_item({ name: snapshot.name }),
			restore: () => {
				items = [...items, snapshot].sort((a, b) => a.sort_order - b.sort_order);
			},
			commit: async () => {
				await deleteItem(id);
			}
		});
	}

	// ── Swipe-toggle (symmetric check / uncheck) ───────────────────────────────
	//
	// Unchecked item + swipe RIGHT → mark checked (reveals success zone).
	// Checked   item + swipe LEFT  → mark unchecked (reveals neutral zone).
	// Opposite direction for each state is a no-op; row snaps back.

	function swipeDirectionFor(item: ShoppingItem): 'right' | 'left' {
		return item.is_checked ? 'left' : 'right';
	}

	function onSwipeStart(e: PointerEvent, _id: string) {
		if (e.pointerType === 'mouse') return;
		swipeStartX = e.clientX;
		swipeStartY = e.clientY;
		swipeHorizontal = false;
	}

	function onSwipeMove(e: PointerEvent, item: ShoppingItem) {
		if (e.pointerType === 'mouse') return;
		const dx = e.clientX - swipeStartX;
		const dy = e.clientY - swipeStartY;

		if (!swipeHorizontal) {
			if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
			if (Math.abs(dy) >= Math.abs(dx)) return;
			swipeHorizontal = true;
		}

		// Clamp to the valid direction for this item's state.
		const dir = swipeDirectionFor(item);
		const clamped = dir === 'right' ? Math.max(0, dx) : Math.min(0, dx);
		swipeOffsets[item.id] = clamped;
	}

	async function onSwipeEnd(e: PointerEvent, item: ShoppingItem) {
		if (e.pointerType === 'mouse') return;
		if (!swipeHorizontal) {
			swipeOffsets[item.id] = 0;
			return;
		}

		const offset = swipeOffsets[item.id] ?? 0;
		const dir = swipeDirectionFor(item);
		const crossed =
			dir === 'right'
				? offset >= SWIPE_COMMIT_THRESHOLD
				: offset <= -SWIPE_COMMIT_THRESHOLD;
		swipeOffsets[item.id] = 0;
		swipeHorizontal = false;
		if (crossed && $currentUser) {
			await handleCheck(item);
		}
	}

	// ── Drag & drop reorder ────────────────────────────────────────────────────

	const flipDurationMs = 200;

	function handleDndConsider(e: CustomEvent<{ items: ShoppingItem[] }>) {
		const checked = items.filter((i) => i.is_checked);
		items = [...e.detail.items, ...checked];
	}

	async function handleDndFinalize(e: CustomEvent<{ items: ShoppingItem[] }>) {
		const checked = items.filter((i) => i.is_checked);
		const reordered = e.detail.items;
		items = [...reordered, ...checked];
		await reorderItems(reordered);
	}

	// ── List actions ───────────────────────────────────────────────────────────

	async function handleReset() {
		if (!list) return;
		showMenu = false;
		items = items.map((i) => ({ ...i, is_checked: false, checked_by: null, checked_at: null }));
		await getSupabase().from('shopping_lists').update({ status: 'active', completed_at: null }).eq('id', listId);
		await getSupabase().from('shopping_items').update({ is_checked: false, checked_by: null, checked_at: null }).eq('list_id', listId);
		if (list) list = { ...list, status: 'active', completed_at: null };
	}
</script>

{#if loading}
	<div class="flex flex-1 items-center justify-center">
		<span class="text-sm text-text-muted">{m.loading()}</span>
	</div>
{:else}
	<div class="flex flex-1 flex-col overflow-hidden">
		<!-- Header — sticky so it stays in view when the list is long.
		     (app)/+layout.svelte hides the global MobileTopBar on detail routes,
		     so this is the ONLY top chrome on mobile.
		     In selection mode the header transforms into a dark action bar
		     showing the selection count + Cancel (bulk action buttons live in
		     a bottom bar on mobile / inline on desktop). -->
		{#if selectionMode}
			<header
				data-testid="selection-bar"
				class="sticky top-0 z-30 flex items-center gap-3 bg-slate-900 px-4 py-3 text-white md:px-8"
			>
				<button
					type="button"
					onclick={exitSelection}
					class="rounded-md px-2 py-1 text-sm font-medium hover:bg-white/10"
				>
					{m.list_cancel_selection()}
				</button>
				<span class="flex-1 text-sm font-medium">
					{m.list_selection_count({ n: String(selectedCount) })}
				</span>
				<!-- Desktop inline actions (mobile gets a bottom bar further down) -->
				<div class="hidden shrink-0 items-center gap-1 md:flex">
					<button
						type="button"
						onclick={handleBulkCheck}
						disabled={selectedCount === 0}
						aria-label={m.list_bulk_mark_checked()}
						class="rounded-md p-1.5 hover:bg-white/10 disabled:opacity-40"
					>
						<CheckSquare size={18} strokeWidth={1.5} />
					</button>
					<button
						type="button"
						onclick={openMovePicker}
						disabled={selectedCount === 0}
						aria-label={m.list_bulk_move()}
						class="rounded-md p-1.5 hover:bg-white/10 disabled:opacity-40"
					>
						<FolderInput size={18} strokeWidth={1.5} />
					</button>
					<button
						type="button"
						onclick={handleBulkDelete}
						disabled={selectedCount === 0}
						aria-label={m.list_bulk_delete()}
						class="rounded-md p-1.5 text-red-300 hover:bg-white/10 disabled:opacity-40"
					>
						<Trash2 size={18} strokeWidth={1.5} />
					</button>
				</div>
			</header>
		{:else}
			<header class="sticky top-0 z-30 flex items-center gap-3 border-b border-black/5 bg-surface/80 px-4 py-3 backdrop-blur-xl md:border-b-0 md:bg-transparent md:backdrop-blur-0 md:px-8 md:pb-4 md:pt-8 dark:border-white/5">
				<a
					href="/lists"
					class="rounded-md p-1.5 text-text-muted hover:bg-black/5 dark:hover:bg-white/5"
					aria-label="Back"
				>
					<ArrowLeft size={16} strokeWidth={1.5} />
				</a>
				<div class="flex-1 min-w-0">
					<!-- Title moved to the content area for inline rename (see <input> below). -->
					<h1 class="truncate text-[15px] font-medium text-text-secondary md:hidden">
						{list?.name || m.list_name_placeholder()}
					</h1>
				</div>
				<!-- Start shopping session -->
				<a
					href="/lists/{listId}/session"
					data-testid="start-session"
					class="shrink-0 rounded-md bg-slate-900 px-3 py-1.5 text-sm font-medium text-slate-50
						hover:bg-slate-800 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-200"
				>
					{m.list_start_session()}
				</a>
				<!-- Selection toggle (desktop, next to 3-dots) -->
				<button
					type="button"
					onclick={() => enterSelection()}
					aria-label={m.list_select()}
					class="hidden shrink-0 rounded-md p-1.5 text-text-muted hover:bg-black/5 md:flex dark:hover:bg-white/5"
					data-testid="selection-toggle"
				>
					<CheckSquare size={16} strokeWidth={1.5} />
				</button>
				<!-- Actions menu -->
				<div class="relative shrink-0">
					<button
						onclick={() => (showMenu = !showMenu)}
						class="rounded-md p-1.5 text-text-muted hover:bg-black/5 dark:hover:bg-white/5"
						aria-label={m.list_actions()}
					>
						<MoreHorizontal size={16} strokeWidth={1.5} />
					</button>
					{#if showMenu}
						<div
							class="absolute right-0 top-full z-10 mt-1 w-44 rounded-lg bg-surface
								shadow-[0px_20px_40px_rgba(15,23,42,0.1)] dark:shadow-[0px_20px_40px_rgba(0,0,0,0.3)]"
						>
							<button
								onclick={handleReset}
								class="flex w-full items-center gap-2 px-3 py-2.5 text-sm text-slate-700
									hover:bg-black/5 dark:text-slate-300 dark:hover:bg-white/5 rounded-lg"
							>
								<RotateCcw size={14} strokeWidth={1.5} />
								{m.list_reset()}
							</button>
						</div>
					{/if}
				</div>
			</header>
		{/if}

		<SyncBanner />

		<!-- Items -->
		<div class="flex-1 overflow-y-auto pb-16 md:pb-6">
			<!-- Inline rename title — matches /notes editor pattern. Autosaves after
			     NAME_SAVE_DEBOUNCE_MS of inactivity. Status signals: strikethrough
			     on completed lists, muted pill next to the title for archived. -->
			<div class="flex items-center gap-2 px-4 pt-2 pb-2 md:px-8 md:pt-3 md:pb-4">
				<input
					bind:value={listName}
					oninput={scheduleNameSave}
					onblur={flushNameSave}
					placeholder={m.list_name_placeholder()}
					class="min-w-0 flex-1 bg-transparent text-[26px] md:text-[32px] font-semibold tracking-[-0.02em] placeholder:text-text-secondary focus:outline-none {list?.status === 'completed' ? 'text-text-secondary line-through' : 'text-text-primary'}"
					aria-label={m.list_name_placeholder()}
				/>
				{#if list?.status === 'archived'}
					<span
						data-testid="list-status-pill"
						class="shrink-0 rounded-full bg-slate-200 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-text-secondary dark:bg-slate-800"
					>
						{m.list_status_archived()}
					</span>
				{/if}
			</div>

			{#if uncheckedItems.length === 0 && checkedItems.length === 0}
				<div class="flex flex-col items-center justify-center py-16 text-center">
					<p class="mb-1 text-sm font-medium text-text-primary">{m.list_items_empty()}</p>
					<p class="text-sm text-text-muted">{m.list_items_empty_hint()}</p>
				</div>
				{@render addItemForm()}
			{:else}
				<!-- Unchecked items (drag & droppable via left handle) -->
				{#if uncheckedItems.length > 0}
					<div
						use:dndzone={{
							items: uncheckedItems,
							flipDurationMs,
							type: 'items',
							dragDisabled: !dragEnabled
						}}
						onconsider={handleDndConsider}
						onfinalize={(e) => {
							void handleDndFinalize(e);
							onDragEnd();
						}}
						class="px-4 md:px-8"
					>
						{#each uncheckedItems as item (item.id)}
							{@const offset = swipeOffsets[item.id] ?? 0}
							{@const isSelected = selectedIds.has(item.id)}
							<div
								animate:flip={{ duration: flipDurationMs }}
								class="group relative overflow-hidden"
							>
								<!-- Reveal zone (behind row) — hidden in selection mode -->
								{#if !selectionMode}
									<div
										class="absolute inset-y-0 left-0 flex items-center bg-emerald-500 pl-4 rounded-l-lg text-white"
										style="width: {Math.min(SWIPE_REVEAL_WIDTH, Math.abs(offset))}px; opacity: {offset > 0 ? 1 : 0};"
										aria-hidden="true"
									>
										<Check size={18} strokeWidth={2} />
									</div>
								{/if}

								<!-- Item row — button, no checkbox (except in selection mode) -->
								<div
									role="listitem"
									data-testid="item-row"
									data-selected={isSelected ? 'true' : undefined}
									class="relative flex items-center gap-2 py-3 select-none transition-transform duration-150 {isSelected ? 'bg-slate-100 dark:bg-slate-800' : 'bg-background'} {selectionMode ? '' : 'touch-pan-y'}"
									style={selectionMode ? '' : `transform: translateX(${offset}px)`}
									onpointerdown={(e) => {
										if (selectionMode) return;
										onSwipeStart(e, item.id);
										startLongPress(item);
									}}
									onpointermove={(e) => {
										if (selectionMode) return;
										onSwipeMove(e, item);
										// Any move cancels the long-press intent
										cancelLongPress();
									}}
									onpointerup={(e) => {
										if (selectionMode) return;
										cancelLongPress();
										void onSwipeEnd(e, item);
									}}
									onpointerleave={cancelLongPress}
									onpointercancel={cancelLongPress}
								>
									{#if selectionMode}
										<!-- Selection checkbox replaces the drag handle -->
										<button
											type="button"
											onclick={() => toggleSelection(item.id)}
											aria-label={item.name}
											aria-pressed={isSelected}
											class="shrink-0 flex h-5 w-5 items-center justify-center rounded border-2 {isSelected ? 'bg-slate-900 border-slate-900 dark:bg-slate-100 dark:border-slate-100' : 'border-slate-300 dark:border-slate-600'}"
										>
											{#if isSelected}
												<Check size={12} strokeWidth={2.5} class="text-white dark:text-slate-900" />
											{/if}
										</button>
									{:else}
										<!-- Drag handle (always visible on mobile, reveal on hover on desktop). -->
										<button
											type="button"
											aria-label={m.list_reorder_handle()}
											onpointerdown={enableDragFromHandle}
											class="drag-handle shrink-0 cursor-grab text-text-muted touch-none active:cursor-grabbing md:opacity-0 md:group-hover:opacity-100 transition-opacity"
										>
											<GripVertical size={16} strokeWidth={1.5} />
										</button>
									{/if}

									<!-- Name as button; tap=nothing/toggle-selection, dbltap=overlay -->
									<button
										type="button"
										onclick={() => {
											if (selectionMode) toggleSelection(item.id);
											else onRowTap(item);
										}}
										class="flex-1 min-w-0 text-left"
										aria-label={item.name}
									>
										<span class="block truncate text-sm text-slate-800 dark:text-slate-200">
											{item.name}
										</span>
									</button>

									<!-- Quantity stepper — hidden in selection mode -->
									{#if !selectionMode}
									<div class="flex shrink-0 items-center gap-1">
										<button
											type="button"
											onclick={() => adjustQty(item, -1)}
											class="flex h-7 w-7 items-center justify-center rounded text-text-muted hover:bg-black/5 dark:hover:bg-white/5"
											aria-label={m.list_qty_decrease()}
										>
											<Minus size={14} strokeWidth={2} />
										</button>
										<span class="w-7 text-center text-sm tabular-nums text-slate-700 dark:text-slate-300">
											{item.quantity ?? 1}
										</span>
										<button
											type="button"
											onclick={() => adjustQty(item, 1)}
											class="flex h-7 w-7 items-center justify-center rounded text-text-muted hover:bg-black/5 dark:hover:bg-white/5"
											aria-label={m.list_qty_increase()}
										>
											<Plus size={14} strokeWidth={2} />
										</button>
									</div>
									{/if}
								</div>
							</div>
						{/each}
					</div>
				{/if}

				<!-- Inline add-item form. Always rendered between unchecked and checked
				     sections (option `a` from the UX review); hidden in selection mode. -->
				{@render addItemForm()}

				<!-- Checked items section — separate zone, symmetric swipe-to-uncheck -->
				{#if checkedItems.length > 0}
					<div class="px-4 md:px-8 mt-4">
						<p class="mb-2 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-muted">
							{m.list_checked()} ({checkedItems.length})
						</p>
						{#each checkedItems as item (item.id)}
							{@const offset = swipeOffsets[item.id] ?? 0}
							{@const isSelected = selectedIds.has(item.id)}
							<div class="relative overflow-hidden">
								{#if !selectionMode}
									<div
										class="absolute inset-y-0 right-0 flex items-center justify-end bg-slate-400 pr-4 rounded-r-lg text-white dark:bg-slate-600"
										style="width: {Math.min(SWIPE_REVEAL_WIDTH, Math.abs(offset))}px; opacity: {offset < 0 ? 1 : 0};"
										aria-hidden="true"
									>
										<RotateCcw size={18} strokeWidth={2} />
									</div>
								{/if}

								<div
									role="listitem"
									data-testid="item-row"
									data-selected={isSelected ? 'true' : undefined}
									class="relative flex items-center gap-2 py-3 select-none transition-transform duration-150 {isSelected ? 'bg-slate-100 dark:bg-slate-800' : 'bg-background opacity-70'} {selectionMode ? '' : 'touch-pan-y'}"
									style={selectionMode ? '' : `transform: translateX(${offset}px)`}
									onpointerdown={(e) => {
										if (selectionMode) return;
										onSwipeStart(e, item.id);
										startLongPress(item);
									}}
									onpointermove={(e) => {
										if (selectionMode) return;
										onSwipeMove(e, item);
										cancelLongPress();
									}}
									onpointerup={(e) => {
										if (selectionMode) return;
										cancelLongPress();
										void onSwipeEnd(e, item);
									}}
									onpointerleave={cancelLongPress}
									onpointercancel={cancelLongPress}
								>
									{#if selectionMode}
										<button
											type="button"
											onclick={() => toggleSelection(item.id)}
											aria-label={item.name}
											aria-pressed={isSelected}
											class="shrink-0 flex h-5 w-5 items-center justify-center rounded border-2 {isSelected ? 'bg-slate-900 border-slate-900 dark:bg-slate-100 dark:border-slate-100' : 'border-slate-300 dark:border-slate-600'}"
										>
											{#if isSelected}
												<Check size={12} strokeWidth={2.5} class="text-white dark:text-slate-900" />
											{/if}
										</button>
									{:else}
										<div class="shrink-0 w-4"></div>
									{/if}
									<button
										type="button"
										onclick={() => {
											if (selectionMode) toggleSelection(item.id);
											else onRowTap(item);
										}}
										class="flex-1 min-w-0 text-left"
										aria-label={item.name}
									>
										<span class="block truncate text-sm text-text-muted line-through">
											{item.name}
										</span>
									</button>
									{#if !selectionMode}
										<span class="shrink-0 text-xs tabular-nums text-text-muted">
											{item.quantity ?? 1}
										</span>
									{/if}
								</div>
							</div>
						{/each}
					</div>
				{/if}
			{/if}
		</div>

		<!-- Mobile bottom action bar (selection mode only) — replaces the
		     add-item row and sits above the BottomTabBar safe-area. -->
		{#if selectionMode}
			<div
				data-testid="selection-action-bar"
				class="absolute inset-x-0 bottom-0 z-30 flex items-center justify-around border-t border-black/5 bg-surface/95 px-4 py-3 backdrop-blur-xl mb-16 md:hidden dark:border-white/5"
			>
				<button
					type="button"
					onclick={handleBulkCheck}
					disabled={selectedCount === 0}
					aria-label={m.list_bulk_mark_checked()}
					class="flex flex-col items-center gap-0.5 p-2 text-text-secondary disabled:opacity-40"
				>
					<CheckSquare size={20} strokeWidth={1.5} />
				</button>
				<button
					type="button"
					onclick={openMovePicker}
					disabled={selectedCount === 0}
					aria-label={m.list_bulk_move()}
					class="flex flex-col items-center gap-0.5 p-2 text-text-secondary disabled:opacity-40"
				>
					<FolderInput size={20} strokeWidth={1.5} />
				</button>
				<button
					type="button"
					onclick={handleBulkDelete}
					disabled={selectedCount === 0}
					aria-label={m.list_bulk_delete()}
					class="flex flex-col items-center gap-0.5 p-2 text-red-500 disabled:opacity-40"
				>
					<Trash2 size={20} strokeWidth={1.5} />
				</button>
			</div>
		{/if}

	</div>

	<!-- Inline add-item form: input row on top, frequency chips below.
	     Hidden in selection mode per UX decision 6. -->
	{#snippet addItemForm()}
		{#if !selectionMode}
			<div class="px-4 pt-2 pb-6 md:px-8" data-testid="add-item-form">
				<div class="flex items-center gap-2">
					<div class="flex flex-1 items-center gap-2 rounded-lg bg-surface px-4 py-2.5 shadow-[0px_2px_8px_rgba(15,23,42,0.06)]">
						<input
							bind:this={nameInput}
							bind:value={newName}
							onkeydown={handleAddKeydown}
							type="text"
							placeholder={m.list_item_placeholder()}
							class="min-w-0 flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-muted outline-none"
						/>
						<input
							bind:value={newQty}
							type="number"
							min="1"
							placeholder={m.list_qty_label()}
							onkeydown={handleAddKeydown}
							class="w-12 bg-transparent text-sm text-text-secondary placeholder:text-text-muted outline-none text-right"
						/>
					</div>
					<button
						onclick={handleAdd}
						disabled={!newName.trim()}
						class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-900 text-white hover:opacity-90 disabled:opacity-40 dark:bg-white dark:text-slate-900 transition-opacity"
						aria-label={m.add()}
					>
						<Plus size={18} strokeWidth={2} />
					</button>
				</div>
				<!-- Suggestions — second line, right below the input. -->
				<ItemSuggestions
					{suggestions}
					activePrefix={newName}
					onselect={selectSuggestion}
				/>
			</div>
		{/if}
	{/snippet}

	<!-- Double-tap edit overlay -->
	{#if overlayItem}
		<div
			class="fixed inset-0 z-50 flex items-end justify-center bg-black/40 md:items-center"
			role="dialog"
			aria-modal="true"
			aria-label={m.list_edit_item()}
			onclick={(e) => {
				if (e.target === e.currentTarget) closeOverlay();
			}}
			onkeydown={(e) => e.key === 'Escape' && closeOverlay()}
			tabindex="-1"
		>
			<div
				class="w-full max-w-md rounded-t-2xl bg-surface p-5 shadow-[0px_20px_40px_rgba(15,23,42,0.15)] md:rounded-2xl"
				data-testid="edit-overlay"
			>
				<div class="mb-4 flex items-center justify-between">
					<h2 class="text-base font-semibold text-text-primary">{m.list_edit_item()}</h2>
					<button
						type="button"
						onclick={closeOverlay}
						aria-label={m.list_close()}
						class="rounded p-1 text-text-secondary hover:bg-black/5 dark:hover:bg-white/5"
					>
						<X size={18} strokeWidth={1.5} />
					</button>
				</div>
				<!-- svelte-ignore a11y_autofocus -->
				<input
					bind:value={overlayName}
					onkeydown={handleOverlayKeydown}
					placeholder={m.list_item_placeholder()}
					autofocus
					class="mb-4 w-full rounded-md bg-background px-3 py-2 text-base text-text-primary outline-none ring-1 ring-black/10 focus:ring-slate-900 dark:ring-white/10 dark:focus:ring-slate-100"
				/>
				<div class="flex items-center justify-between gap-2">
					<button
						type="button"
						onclick={overlayDelete}
						class="rounded-md bg-red-500 px-4 py-2 text-sm font-medium text-white hover:bg-red-600"
					>
						{m.action_delete()}
					</button>
					<button
						type="button"
						onclick={overlayConfirm}
						class="rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900"
					>
						{m.list_confirm()}
					</button>
				</div>
			</div>
		</div>
	{/if}

	<!-- Move-to-other-list picker (selection mode) -->
	{#if showMovePicker}
		<div
			class="fixed inset-0 z-50 flex items-end justify-center bg-black/40 md:items-center"
			role="dialog"
			aria-modal="true"
			aria-label={m.list_move_title()}
			onclick={(e) => {
				if (e.target === e.currentTarget) showMovePicker = false;
			}}
			onkeydown={(e) => e.key === 'Escape' && (showMovePicker = false)}
			tabindex="-1"
		>
			<div
				class="w-full max-w-md rounded-t-2xl bg-surface p-5 shadow-[0px_20px_40px_rgba(15,23,42,0.15)] md:rounded-2xl"
				data-testid="move-picker"
			>
				<div class="mb-3 flex items-center justify-between">
					<h2 class="text-base font-semibold text-text-primary">{m.list_move_title()}</h2>
					<button
						type="button"
						onclick={() => (showMovePicker = false)}
						aria-label={m.list_close()}
						class="rounded p-1 text-text-secondary hover:bg-black/5 dark:hover:bg-white/5"
					>
						<X size={18} strokeWidth={1.5} />
					</button>
				</div>
				<div class="max-h-72 overflow-y-auto">
					{#each $listsStore.filter((l) => l.id !== listId && l.status === 'active' && !l.deleted_at) as other (other.id)}
						<button
							type="button"
							onclick={() => handleBulkMove(other.id)}
							class="flex w-full items-center gap-2 rounded-md px-3 py-2.5 text-left text-sm hover:bg-black/5 dark:hover:bg-white/5"
						>
							<Check size={14} strokeWidth={1.5} class="text-text-muted" />
							<span class="truncate">{other.name || m.list_name_placeholder()}</span>
						</button>
					{/each}
				</div>
				<button
					type="button"
					onclick={handleMoveToNewList}
					class="mt-2 flex w-full items-center gap-2 rounded-md border border-dashed border-black/10 px-3 py-2.5 text-sm text-text-secondary hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"
				>
					<Plus size={14} strokeWidth={1.5} />
					{m.list_move_create_new()}
				</button>
			</div>
		</div>
	{/if}
{/if}
