<script lang="ts">
	import { onMount } from 'svelte';
	import { page } from '$app/state';
	import { goto } from '$app/navigation';
	import { Dialog, DialogContent, DialogOverlay } from '$lib/components/ui/dialog';
	import { Input } from '$lib/components/ui/input';
	import { Button } from '$lib/components/ui/button';
	import ImageVariant from '$lib/components/ui/ImageVariant.svelte';

	interface ThumbURLs {
		avif: string;
		webp: string;
		jpg: string;
	}

	interface FullURLs {
		avif: string;
		webp: string;
		jpg: string;
		jxl?: string;
	}

	interface PortfolioImage {
		id: string;
		full: FullURLs;
		thumb: ThumbURLs;
		tag_names: string[];
		created_at: string;
	}

	interface FilterCategory {
		category: string;
		values: FilterValue[];
	}

	interface FilterValue {
		value: string;
		count: number;
	}

	let images = $state<PortfolioImage[]>([]);
	let loading = $state(true);
	let loadingMore = $state(false);
	let error = $state(false);
	let selectedTag = $state('');
	let selectedTags = $state<string[]>([]);
	let hasMore = $state(true);
	let cursor = $state('');
	const limit = 20;

	let searchQuery = $state('');
	let filterCategories = $state<FilterCategory[]>([]);
	let selectedFilters = $state<Record<string, string>>({});
	let openDropdowns = $state<Record<string, boolean>>({});
	let dropdownPosition = $state<{ x: number; y: number } | null>(null);
	const DROPDOWN_WIDTH = 192; // w-48 = 12rem = 192px

	function toggleDropdown(category: string, event: MouseEvent) {
		const target = event.currentTarget as HTMLElement;
		const rect = target.getBoundingClientRect();

		if (!openDropdowns[category]) {
			// Clamp dropdown position to stay within viewport
			let x = rect.left;
			const maxX = window.innerWidth - DROPDOWN_WIDTH;
			if (x > maxX) {
				x = maxX;
			}
			if (x < 0) {
				x = 0;
			}
			dropdownPosition = { x, y: rect.bottom };
			openDropdowns = { [category]: true };
		} else {
			openDropdowns = {};
			dropdownPosition = null;
		}
	}

	function closeAllDropdowns() {
		openDropdowns = {};
		dropdownPosition = null;
	}

	function handleWindowClick(e: MouseEvent) {
		const target = e.target as HTMLElement;

		// Don't close if clicking anywhere inside a filter dropdown (button or its children)
		if (target.closest('.filter-dropdown')) {
			return;
		}

		// Close all dropdowns when clicking outside
		closeAllDropdowns();
	}

	function buildFilterUrl(): string {
		const parts: string[] = [];
		if (selectedTags.length > 0) {
			parts.push(`tags=${encodeURIComponent(selectedTags.join(','))}`);
		} else if (selectedTag) {
			parts.push(`tag=${encodeURIComponent(selectedTag)}`);
		}
		for (const [category, value] of Object.entries(selectedFilters)) {
			if (value) {
				parts.push(`filter[${encodeURIComponent(category)}]=${encodeURIComponent(value)}`);
			}
		}
		return parts.length > 0 ? '?' + parts.join('&') : '';
	}

	async function fetchFilters() {
		try {
			const params = buildFilterUrl();
			const url = params ? `/api/portfolio/filters${params}` : '/api/portfolio/filters';
			const response = await fetch(url);
			if (!response.ok) {
				console.error('Filter fetch failed:', response.status, response.statusText);
				return;
			}

			const data = await response.json();
			if (!data || !Array.isArray(data)) return;
			filterCategories = data.sort((a: FilterCategory, b: FilterCategory) => {
				const countA = a.values.reduce((sum: number, v: FilterValue) => sum + v.count, 0);
				const countB = b.values.reduce((sum: number, v: FilterValue) => sum + v.count, 0);
				return countB - countA;
			});
		} catch (e) {
			console.error('Failed to fetch filters:', e);
		}
	}

	function buildImageUrl(): string {
		const parts: string[] = [`limit=${limit}`];
		if (cursor) {
			parts.push(`cursor=${encodeURIComponent(cursor)}`);
		}

		if (selectedTags.length > 0) {
			parts.push(`tags=${encodeURIComponent(selectedTags.join(','))}`);
		} else if (selectedTag) {
			parts.push(`tag=${encodeURIComponent(selectedTag)}`);
		}

		for (const [category, value] of Object.entries(selectedFilters)) {
			if (value) {
				parts.push(`filter[${encodeURIComponent(category)}]=${encodeURIComponent(value)}`);
			}
		}

		return '?' + parts.join('&');
	}

	async function fetchImages(append = false) {
		if (append) {
			loadingMore = true;
		} else {
			loading = true;
			error = false;
		}

		try {
			const url = '/api/portfolio/images' + buildImageUrl();
			const response = await fetch(url);

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

			const data = await response.json();
			const imagesList = data.images ?? data;
			const newImages = imagesList.map(
				(img: {
					id: string;
					url: string;
					thumbnail_url: string;
					full: { avif: string; webp: string; jpg: string; jxl?: string };
					thumb: { avif: string; webp: string; jpg: string };
					tag_names: string[];
					created_at: string;
				}) => ({
					id: img.id,
					full: img.full || {
						avif: img.url,
						webp: img.url,
						jpg: img.url
					},
					thumb: img.thumb || {
						avif: img.thumbnail_url,
						webp: img.thumbnail_url,
						jpg: img.thumbnail_url
					},
					tag_names: img.tag_names || [],
					created_at: img.created_at
				})
			);

			if (append) {
				images = [...images, ...newImages];
			} else {
				images = newImages;
			}

			cursor = data.next_cursor ?? '';
			hasMore = newImages.length === limit;
		} catch (e) {
			error = true;
			console.error('Failed to load portfolio:', e);
		} finally {
			loading = false;
			loadingMore = false;
		}
	}

	async function fetchImageById(id: string): Promise<PortfolioImage | null> {
		try {
			const response = await fetch(`/api/portfolio/images/${id}`);
			if (!response.ok) return null;

			const img = await response.json();
			return {
				id: img.id,
				full: img.full || {
					avif: img.url,
					webp: img.url,
					jpg: img.url
				},
				thumb: img.thumb || {
					avif: img.thumbnail_url,
					webp: img.thumbnail_url,
					jpg: img.thumbnail_url
				},
				tag_names: img.tag_names || [],
				created_at: img.created_at
			};
		} catch (e) {
			console.error('Failed to fetch image by ID:', e);
			return null;
		}
	}

	function loadMore() {
		if (loadingMore || !hasMore || !cursor) return;
		fetchImages(true);
	}

	function applySearch() {
		cursor = '';
		if (searchQuery.includes(',')) {
			selectedTags = searchQuery
				.split(',')
				.map((t) => t.trim().toLowerCase())
				.filter(Boolean);
			selectedTag = '';
		} else if (searchQuery.trim()) {
			selectedTag = searchQuery.trim().toLowerCase();
			selectedTags = [];
		} else {
			selectedTag = '';
			selectedTags = [];
		}

		// Build URL with reactive page state
		const url = new URL(page.url);
		if (selectedTags.length > 0) {
			url.searchParams.set('tags', selectedTags.join(','));
		} else if (selectedTag) {
			url.searchParams.set('tag', selectedTag);
		} else {
			url.searchParams.delete('tag');
			url.searchParams.delete('tags');
		}
		// eslint-disable-next-line svelte/no-navigation-without-resolve
		goto(url.pathname + url.search, { replaceState: true, noScroll: true });

		fetchImages(false);
		fetchFilters();
	}

	function selectFilter(category: string, value: string) {
		cursor = '';
		closeAllDropdowns();

		if (value === '' || value === selectedFilters[category]) {
			delete selectedFilters[category];
			selectedFilters = { ...selectedFilters };
		} else {
			selectedFilters = { ...selectedFilters, [category]: value };
		}

		// Build URL with reactive page state
		const url = new URL(page.url);

		// Remove all existing filter params first to avoid stale params
		const keysToDelete = Array.from(url.searchParams.keys()).filter((k) => k.startsWith('filter['));
		keysToDelete.forEach((k) => url.searchParams.delete(k));

		// Add current filters
		for (const [cat, val] of Object.entries(selectedFilters)) {
			if (val) {
				url.searchParams.set(`filter[${cat}]`, val);
			}
		}
		// eslint-disable-next-line svelte/no-navigation-without-resolve
		goto(url.pathname + url.search, { replaceState: true, noScroll: true });

		fetchImages(false);
		fetchFilters();
	}

	function clearFilters() {
		selectedFilters = {};
		cursor = '';

		// Build URL with reactive page state
		const url = new URL(page.url);
		const keysToDelete = Array.from(url.searchParams.keys()).filter((k) => k.startsWith('filter['));
		keysToDelete.forEach((k) => url.searchParams.delete(k));
		// eslint-disable-next-line svelte/no-navigation-without-resolve
		goto(url.pathname + url.search, { replaceState: true, noScroll: true });

		fetchImages(false);
		fetchFilters();
	}

	function clearSearch() {
		selectedTag = '';
		selectedTags = [];
		searchQuery = '';
		cursor = '';

		// Build URL with reactive page state
		const url = new URL(page.url);
		url.searchParams.delete('tag');
		url.searchParams.delete('tags');
		// eslint-disable-next-line svelte/no-navigation-without-resolve
		goto(url.pathname + url.search, { replaceState: true, noScroll: true });

		fetchImages(false);
	}

	let sentinelRef: HTMLDivElement | undefined = $state(undefined);

	onMount(() => {
		const tagParam = page.url.searchParams.get('tag');
		const tagsParam = page.url.searchParams.get('tags');
		const imgParam = page.url.searchParams.get('img');

		selectedTag = tagParam && tagParam.length <= 100 ? tagParam.slice(0, 100) : '';
		selectedTags =
			tagsParam && tagsParam.length <= 500
				? tagsParam
						.split(',')
						.map((t: string) => t.trim())
						.filter(Boolean)
						.slice(0, 20)
				: [];

		if (selectedTag) {
			searchQuery = selectedTag;
		} else if (selectedTags.length > 0) {
			searchQuery = selectedTags.join(', ');
		}

		for (const [key, value] of page.url.searchParams.entries()) {
			const match = key.match(/^filter\[(.+)\]$/);
			if (match) {
				const filterKey = match[1].slice(0, 50);
				const filterValue = value.slice(0, 200);
				if (/^[a-zA-Z0-9_-]+$/.test(filterKey)) {
					selectedFilters[filterKey] = filterValue;
				}
			}
		}

		fetchFilters();
		fetchImages(false);

		// Always fetch the image by ID - this bypasses filters and pagination
		// ensuring the featured image always loads
		if (imgParam) {
			const targetId = imgParam.trim();
			// Validate: only allow alphanumeric and dash
			if (!/^[a-zA-Z0-9_-]+$/.test(targetId)) {
				console.warn('Invalid img parameter, ignoring');
			} else {
				fetchImageById(targetId).then((img) => {
					if (img) {
						featuredImage = img;
						openModal(img);
					}
				});
			}
		}

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries[0].isIntersecting && hasMore && !loadingMore && !loading) {
					loadMore();
				}
			},
			{ rootMargin: '200px' }
		);

		if (sentinelRef) {
			observer.observe(sentinelRef);
		}

		return () => observer.disconnect();
	});

	let featuredImage = $state<PortfolioImage | null>(null);
	let showModal = $state(false);
	let selectedFullURLs = $state<FullURLs | null>(null);
	let selectedThumbURLs = $state<ThumbURLs | null>(null);
	let currentIndex = $state(0);
	let imageLoading = $state(false);
	let nextButtonRef = $state<HTMLButtonElement | undefined>(undefined);
	let prevButtonRef = $state<HTMLButtonElement | undefined>(undefined);
	let closeBtnRef = $state<HTMLButtonElement | undefined>(undefined);
	let touchStartX = $state(0);
	let swiping = $state(false);
	let trackAnimating = $state(false);
	let modalContentRef = $state<HTMLDivElement | undefined>(undefined);
	let trackOffset = $state('0px');
	let trackTransition = $state('none');

	let prevFullURLs = $state<FullURLs | null>(null);
	let nextFullURLs = $state<FullURLs | null>(null);
	let centerSlideRef = $state<HTMLDivElement | undefined>(undefined);

	const SLIDE_MS = 250;
	const BTN_FLIP_MS = 150;
	const BUTTON_EASE = 'cubic-bezier(0.4, 0, 0.2, 1)';

	/** Old button rect captured before a data swap, used to FLIP the close button
	 *  once the new image has loaded and has its final layout dimensions. */
	let pendingBtnRect: DOMRect | null = null;

	/** Position the close button at the top-right of the current image.
	 *  Uses `getBoundingClientRect` for accuracy (works after the image has loaded
	 *  and the browser has laid out the final constrained size).
	 *  If `pendingBtnRect` is set, it plays a FLIP animation from that old position. */
	function positionCloseBtn() {
		if (!closeBtnRef || !centerSlideRef) return;

		const isSm = window.innerWidth >= 640;
		const offset = isSm ? 16 : 8;

		// Find the image inside the center slide
		const img = centerSlideRef.querySelector('img');
		if (!img) return;

		// Use getBoundingClientRect to get the actual rendered image bounds
		const imgRect = img.getBoundingClientRect();
		const slideRect = centerSlideRef.getBoundingClientRect();
		const imgTopRel = imgRect.top - slideRect.top; // image top relative to the slide

		// Position button so its top edge is `offset` px below the image top edge.
		// Same offset as the right edge (`right-2` / `sm:right-4`), so the circle
		// sits equally inside both edges.
		closeBtnRef.style.top = imgTopRel + offset + 'px';

		// FLIP from the old position if we have one
		if (pendingBtnRect) {
			const newRect = closeBtnRef.getBoundingClientRect();
			const dy = pendingBtnRect.top - newRect.top;
			pendingBtnRect = null;
			if (Math.abs(dy) > 0.5) {
				closeBtnRef.animate(
					[{ transform: `translateY(${dy}px)` }, { transform: 'translateY(0)' }],
					{ duration: BTN_FLIP_MS, easing: BUTTON_EASE }
				);
			}
		}
	}

	function updateUrlForIndex(index: number) {
		const imgUrl = images[index].full.avif || images[index].full.jpg;
		const filename = imgUrl.split('/').pop() || '';
		const timestamp = filename.replace(/_full|_thumb|\.[^.]+$/g, '') || images[index].id;
		const url = new URL(page.url);
		url.searchParams.set('img', timestamp);
		// eslint-disable-next-line svelte/no-navigation-without-resolve
		goto(url.pathname + url.search, { replaceState: true, noScroll: true });
	}

	function finishSlideTransition(index: number) {
		trackAnimating = false;

		// Capture old button position BEFORE swapping data.
		// The FLIP animation will play once the new image has loaded
		// (in handleFullImageLoad) and has correct layout dimensions.
		pendingBtnRect = closeBtnRef?.getBoundingClientRect() || null;

		// Update data — the slide content changes and the track snaps back.
		const target = images[index];
		selectedFullURLs = target.full;
		selectedThumbURLs = target.thumb;
		currentIndex = index;
		updateUrlForIndex(index);
		updateAdjacentSlides(index);

		trackTransition = 'none';
		trackOffset = 'calc(-100% / 3)';
	}

	function navigateNext() {
		slideToNext();
	}

	function navigatePrev() {
		slideToPrev();
	}

	function handleTouchStart(e: TouchEvent) {
		if (trackAnimating) return;
		// Ignore touches on nav buttons — they use onclick handlers
		if ((e.target as HTMLElement).closest('button')) return;
		touchStartX = e.touches[0].clientX;
		swiping = true;
	}

	function handleTouchMove(e: TouchEvent) {
		if (!swiping || trackAnimating) return;
		e.preventDefault();
		const delta = e.touches[0].clientX - touchStartX;
		trackOffset = `calc(-100% / 3 + ${delta}px)`;
		trackTransition = 'none';
	}

	function handleTouchEnd(e: TouchEvent) {
		if (!swiping) return;
		swiping = false;
		const deltaX = e.changedTouches[0].clientX - touchStartX;
		const threshold = 50;
		const canGoNext = currentIndex < images.length - 1;
		const canGoPrev = currentIndex > 0;

		if (Math.abs(deltaX) > threshold && !trackAnimating) {
			if (deltaX < 0 && canGoNext) {
				slideToNext();
			} else if (deltaX > 0 && canGoPrev) {
				slideToPrev();
			} else {
				animateBounceBack();
			}
		} else {
			animateBounceBack();
		}
	}

	function animateBounceBack() {
		trackAnimating = true;
		trackTransition = `transform ${SLIDE_MS}ms ${BUTTON_EASE}`;
		trackOffset = 'calc(-100% / 3)';
		setTimeout(() => {
			trackAnimating = false;
			trackTransition = 'none';
		}, SLIDE_MS + 50);
	}

	function openModal(img: PortfolioImage) {
		const idx = images.findIndex((i) => i.id === img.id);
		if (idx !== -1) {
			openModalByIndex(idx);
		} else {
			selectedFullURLs = img.full;
			selectedThumbURLs = img.thumb;
			imageLoading = true;
			showModal = true;
		}
	}

	function openModalByIndex(index: number) {
		currentIndex = index;
		selectedFullURLs = images[index].full;
		selectedThumbURLs = images[index].thumb;
		imageLoading = true;
		showModal = true;
		trackOffset = 'calc(-100% / 3)';
		trackTransition = 'none';
		updateAdjacentSlides(index);

		const imgUrl = images[index].full.avif || images[index].full.jpg;
		const filename = imgUrl.split('/').pop() || '';
		const timestamp = filename.replace(/_full|_thumb|\.[^.]+$/g, '') || images[index].id;

		const url = new URL(page.url);
		url.searchParams.set('img', timestamp);
		// eslint-disable-next-line svelte/no-navigation-without-resolve
		goto(url.pathname + url.search, { replaceState: true, noScroll: true });

		requestAnimationFrame(() => {
			// Position close button (no pendingBtnRect → no FLIP animation)
			positionCloseBtn();

			const hasNext = currentIndex < images.length - 1;
			const hasPrev = currentIndex > 0;
			if (hasNext && nextButtonRef) {
				nextButtonRef.focus();
			} else if (hasPrev && prevButtonRef) {
				prevButtonRef.focus();
			}
		});
	}

	function handleFullImageLoad() {
		imageLoading = false;
		// Wait a frame so the browser has laid out the image with its final
		// constrained size, then position the button and FLIP from the old position.
		requestAnimationFrame(() => positionCloseBtn());
	}

	function updateAdjacentSlides(index: number) {
		const nextIdx = index + 1;
		nextFullURLs = nextIdx < images.length ? images[nextIdx].full : null;
		const prevIdx = index - 1;
		prevFullURLs = prevIdx >= 0 ? images[prevIdx].full : null;
	}

	// -- carousel -- //

	function slideToNext() {
		if (trackAnimating || currentIndex >= images.length - 1) return;
		trackAnimating = true;
		trackTransition = `transform ${SLIDE_MS}ms ${BUTTON_EASE}`;
		trackOffset = 'calc(-200% / 3)';
		setTimeout(() => {
			finishSlideTransition(currentIndex + 1);
		}, SLIDE_MS + 50);
	}

	function slideToPrev() {
		if (trackAnimating || currentIndex <= 0) return;
		trackAnimating = true;
		trackTransition = `transform ${SLIDE_MS}ms ${BUTTON_EASE}`;
		trackOffset = 'calc(0%)';
		setTimeout(() => {
			finishSlideTransition(currentIndex - 1);
		}, SLIDE_MS + 50);
	}

	function handleKeydown(e: KeyboardEvent) {
		if (!showModal) return;

		if (e.key === 'ArrowRight') {
			e.preventDefault();
			navigateNext();
		} else if (e.key === 'ArrowLeft') {
			e.preventDefault();
			navigatePrev();
		} else if (e.key === 'Escape') {
			e.preventDefault();
			closeModal();
		}
	}

	function handleImageLoad(e: Event) {
		const target = e.target as HTMLImageElement;
		if (target) {
			target.style.opacity = '1';
		}
	}

	function handleImageError(e: Event) {
		const target = e.target as HTMLImageElement;
		if (target) {
			target.style.display = 'none';
		}
	}

	function closeModal() {
		showModal = false;
		if (featuredImage) {
			featuredImage = null;
		}
		// Remove img param from URL
		const url = new URL(page.url);
		url.searchParams.delete('img');
		// eslint-disable-next-line svelte/no-navigation-without-resolve
		goto(url.pathname + url.search, { replaceState: true, noScroll: true });
	}
</script>

<svelte:window onkeydown={handleKeydown} onclick={handleWindowClick} onscroll={closeAllDropdowns} />

{#if loading}
	<div class="flex min-h-screen items-center justify-center p-4">
		<div class="text-center">
			<div
				class="mb-4 inline-block h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-gray-900"
			></div>
			<p class="text-gray-600">Loading portfolio...</p>
		</div>
	</div>
{:else if error}
	<div class="flex min-h-screen items-center justify-center p-4">
		<div class="max-w-md text-center">
			<svg
				class="mx-auto mb-4 h-16 w-16 text-gray-400"
				fill="none"
				stroke="currentColor"
				viewBox="0 0 24 24"
			>
				<path
					stroke-linecap="round"
					stroke-linejoin="round"
					stroke-width="2"
					d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
				/>
			</svg>
			<h2 class="mb-2 text-xl font-semibold text-gray-900">Unable to Load Portfolio</h2>
			<p class="text-gray-600">We couldn't load the portfolio images. Please try again later.</p>
		</div>
	</div>
{:else}
	<div class="p-2">
		<div class="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2">
			<div class="w-full">
				<div
					class="scrollbar-hide flex max-w-full items-center gap-2 overflow-x-auto pb-1"
					onscroll={closeAllDropdowns}
				>
					{#if filterCategories.length > 0}
						{@const hasActiveFilters = Object.values(selectedFilters).some((v) => v && v !== '')}
						{#if hasActiveFilters || selectedTag || selectedTags.length > 0}
							<button
								class="filter-dropdown flex shrink-0 items-center justify-center"
								onclick={clearFilters}
								aria-label="Clear all filters"
							>
								<div
									class="flex h-6 w-6 items-center justify-center rounded-full bg-red-100 text-red-600 hover:bg-red-200"
								>
									<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
										<path
											stroke-linecap="round"
											stroke-linejoin="round"
											stroke-width="2"
											d="M6 18L18 6M6 6l12 12"
										/>
									</svg>
								</div>
							</button>
						{/if}
						{#each filterCategories as filter (filter.category)}
							{@const selectedValue = selectedFilters[filter.category]}
							{@const hasActiveFilter = selectedValue && selectedValue !== ''}
							<div class="filter-dropdown relative shrink-0">
								<button
									class="flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm transition-colors hover:bg-gray-50 {hasActiveFilter
										? 'border-primary bg-primary/10'
										: ''}"
									onclick={(e) => {
										e.stopPropagation();
										toggleDropdown(filter.category, e);
									}}
								>
									<span class="capitalize">{filter.category}:</span>
									<span class="font-medium">{selectedValue || 'Any'}</span>
									<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
										<path
											stroke-linecap="round"
											stroke-linejoin="round"
											stroke-width="2"
											d="M19 9l-7 7-7-7"
										/>
									</svg>
								</button>
							</div>
						{/each}
					{/if}
				</div>
				{#each filterCategories as filter (filter.category)}
					{@const selectedValue = selectedFilters[filter.category]}
					{#if openDropdowns[filter.category]}
						<div
							class="filter-dropdown absolute z-50 mt-1 max-h-60 w-48 overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg"
							style="position: fixed; left: {dropdownPosition?.x}px; top: {dropdownPosition?.y}px;"
						>
							<button
								class="filter-dropdown flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-fuchsia-50 {selectedValue ===
								''
									? 'bg-fuchsia-100 font-medium'
									: ''}"
								onclick={(e) => {
									e.stopPropagation();
									selectFilter(filter.category, '');
								}}
							>
								<span class="text-gray-500">Any</span>
							</button>
							{#each filter.values as value (value.value)}
								<button
									class="filter-dropdown flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-fuchsia-50 {selectedValue ===
									value.value
										? 'bg-fuchsia-100 font-medium'
										: ''}"
									onclick={(e) => {
										e.stopPropagation();
										selectFilter(filter.category, value.value);
									}}
								>
									<span class="capitalize">{value.value}</span>
									<span class="text-xs text-gray-400">({value.count})</span>
								</button>
							{/each}
						</div>
					{/if}
				{/each}
			</div>

			<div class="flex gap-2 lg:border-l lg:pl-4">
				<Input
					placeholder="Search tags"
					maxlength={256}
					bind:value={searchQuery}
					onkeydown={(e) => {
						if ((e as KeyboardEvent).key === 'Enter') applySearch();
					}}
				/>
				<Button onclick={applySearch}>
					{loading ? 'Searching...' : 'Search'}
				</Button>

				{#if selectedTag || selectedTags.length > 0}
					<button
						class="flex shrink-0 items-center justify-center"
						onclick={clearSearch}
						aria-label="Clear search tags"
					>
						<div
							class="flex h-6 w-6 items-center justify-center rounded-full bg-red-100 text-red-600 hover:bg-red-200"
						>
							<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path
									stroke-linecap="round"
									stroke-linejoin="round"
									stroke-width="2"
									d="M6 18L18 6M6 6l12 12"
								/>
							</svg>
						</div>
					</button>
				{/if}
			</div>
		</div>
	</div>

	<div class="grid grid-cols-3 gap-1 p-2 pt-0 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8">
		{#each images as img (img.id)}
			<button
				class="relative aspect-square overflow-hidden rounded-xs transition-opacity hover:opacity-90 active:opacity-75"
				onclick={() => openModal(img)}
				aria-label="View full size portfolio item"
			>
				<ImageVariant
					urls={img.thumb}
					type="thumb"
					alt="Portfolio thumbnail {img.id}"
					class="h-full w-full object-cover opacity-0 transition-opacity duration-300"
					onload={handleImageLoad}
					onerror={handleImageError}
				/>
			</button>
		{/each}
	</div>

	<div bind:this={sentinelRef} class="h-4 w-full">
		{#if loadingMore}
			<div class="flex justify-center py-4">
				<div
					class="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-gray-900"
				></div>
			</div>
		{:else if !hasMore && images.length > 0}
			<p class="text-center text-sm text-gray-500">No more images to load</p>
		{/if}
	</div>
{/if}

{#if showModal}
	<Dialog
		open={showModal}
		onOpenChange={(v) => {
			if (!v) closeModal();
			else showModal = v;
		}}
	>
		<DialogOverlay class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm" />
		<DialogContent
			hideClose={true}
			class="fixed top-1/2 left-1/2 z-50 max-h-[95vh] max-w-[95vw] -translate-x-1/2 -translate-y-1/2 border-0 bg-transparent p-0 shadow-none focus:outline-none sm:max-h-[90vh] sm:max-w-[90vw]"
		>
			<div
				bind:this={modalContentRef}
				class="relative flex max-h-[95vh] max-w-[95vw] items-center justify-center sm:max-h-[90vh] sm:max-w-[90vw]"
				role="presentation"
				ontouchstart={handleTouchStart}
				ontouchmove={handleTouchMove}
				ontouchend={handleTouchEnd}
			>
				<div class="overflow-hidden rounded-sm">
					<div
						class="flex w-[300%]"
						style="transform: translateX({trackOffset}); transition: {trackTransition}"
					>
						<div
							class="flex h-[95vh] shrink-0 items-center justify-center sm:h-[90vh]"
							style="width: calc(100% / 3)"
						>
							{#if prevFullURLs}
								<ImageVariant
									urls={prevFullURLs}
									type="full"
									alt=""
									class="max-h-[95vh] max-w-[95vw] rounded-sm object-contain sm:max-h-[90vh] sm:max-w-[90vw]"
								/>
							{/if}
						</div>

						<div
							bind:this={centerSlideRef}
							class="relative flex h-[95vh] shrink-0 items-center justify-center sm:h-[90vh]"
							style="width: calc(100% / 3)"
						>
							{#if imageLoading && selectedThumbURLs}
								<ImageVariant
									urls={selectedThumbURLs}
									type="thumb"
									alt="Loading preview"
									class="absolute max-h-[95vh] max-w-[95vw] scale-110 rounded-sm object-contain blur-xl sm:max-h-[90vh] sm:max-w-[90vw]"
								/>
								<div class="absolute inset-0 flex items-center justify-center">
									<div
										class="h-12 w-12 animate-spin rounded-full border-4 border-white/30 border-t-white"
									></div>
								</div>
							{/if}

							{#if selectedFullURLs}
								<ImageVariant
									urls={selectedFullURLs}
									type="full"
									alt="Portfolio full size"
									class="max-h-[95vh] max-w-[95vw] rounded-sm object-contain transition-opacity duration-300 sm:max-h-[90vh] sm:max-w-[90vw] {imageLoading
										? 'opacity-0'
										: ''}"
									onload={handleFullImageLoad}
								/>
							{/if}
						</div>

						<div
							class="flex h-[95vh] shrink-0 items-center justify-center sm:h-[90vh]"
							style="width: calc(100% / 3)"
						>
							{#if nextFullURLs}
								<ImageVariant
									urls={nextFullURLs}
									type="full"
									alt=""
									class="max-h-[95vh] max-w-[95vw] rounded-sm object-contain sm:max-h-[90vh] sm:max-w-[90vw]"
								/>
							{/if}
						</div>
					</div>
				</div>

				<button
					bind:this={closeBtnRef}
					class="absolute right-2 z-[60] rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70 sm:right-4"
					onclick={closeModal}
					aria-label="Close modal"
				>
					<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							stroke-width="2"
							d="M6 18L18 6M6 6l12 12"
						/>
					</svg>
				</button>

				{#if currentIndex > 0}
					<button
						bind:this={prevButtonRef}
						class="absolute top-1/2 left-2 z-[60] -translate-y-1/2 rounded-full bg-black/50 p-3 text-white transition-colors hover:bg-black/70 sm:left-4"
						onclick={navigatePrev}
						aria-label="Previous image"
					>
						<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
								stroke-width="2"
								d="M15 19l-7-7 7-7"
							/>
						</svg>
					</button>
				{/if}

				{#if currentIndex < images.length - 1}
					<button
						bind:this={nextButtonRef}
						class="absolute top-1/2 right-2 z-[60] -translate-y-1/2 rounded-full bg-black/50 p-3 text-white transition-colors hover:bg-black/70 sm:right-4"
						onclick={navigateNext}
						aria-label="Next image"
					>
						<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
								stroke-width="2"
								d="M9 5l7 7-7 7"
							/>
						</svg>
					</button>
				{/if}
			</div>
		</DialogContent>
	</Dialog>
{/if}
