<script lang="ts">
	import { goto } from '$app/navigation';
	import { page } from '$app/state';
	import { createQuery } from '@tanstack/svelte-query';
	import {
		aggregateKeywords,
		getAllMarks,
		listLabels,
		listPhotos,
		type AggregatedKeyword,
		type PhotoMarksMap,
		type PpLabel
	} from '$lib/services/photoprism';
	import { isAuthenticated, thumbUrl } from '$lib/stores/session.svelte';
	import { view } from '$lib/stores/view.svelte';
	import { primaryFile, type PpPhoto } from '$lib/types/photoprism';
	import { gridKeyNav } from '$lib/actions/gridKeyNav';
	import BulkActionBar from '$lib/components/timeline/BulkActionBar.svelte';
	import PhotoGrid from '$lib/components/timeline/PhotoGrid.svelte';
	import SkeletonGrid from '$lib/components/timeline/SkeletonGrid.svelte';
	import Toolbar from '$lib/components/layout/Toolbar.svelte';

	// Tag-flavoured surfaces, all under one route so the user can swap
	// between them without losing place. Four tabs:
	//   - labels   classifier-derived (PhotoPrism's /labels)
	//   - keywords user-typed (Details.Keywords aggregated)
	//   - ratings  1-5 star marks from mule-sidecar
	//   - colors   4-swatch marks from mule-sidecar
	// Tab + drill state live in the URL so refresh / share / back work.

	type Tab = 'labels' | 'keywords' | 'ratings' | 'colors';
	const TABS: { id: Tab; label: string }[] = [
		{ id: 'labels', label: 'Labels (auto)' },
		{ id: 'keywords', label: 'Keywords' },
		{ id: 'ratings', label: 'Ratings' },
		{ id: 'colors', label: 'Colors' }
	];

	const PAGE_SIZE = 60;

	const activeTab = $derived<Tab>(parseTab(page.url.searchParams.get('tab')));
	const activePage = $derived(parsePage(page.url.searchParams.get('page')));
	const drillKey = $derived(page.url.searchParams.get('drill'));

	function parseTab(raw: string | null): Tab {
		if (raw === 'keywords' || raw === 'ratings' || raw === 'colors') return raw;
		return 'labels';
	}
	function parsePage(raw: string | null): number {
		const n = raw ? parseInt(raw, 10) : 0;
		return Number.isFinite(n) && n >= 0 ? n : 0;
	}

	function setTab(tab: Tab) {
		const params = new URLSearchParams();
		if (tab !== 'labels') params.set('tab', tab);
		void goto(`/tags${params.size ? '?' + params : ''}`, { keepFocus: true, noScroll: true });
	}
	function setPage(p: number) {
		const params = new URLSearchParams(page.url.searchParams);
		if (p === 0) params.delete('page');
		else params.set('page', String(p));
		params.delete('drill');
		void goto(`/tags${params.size ? '?' + params : ''}`, { keepFocus: true, noScroll: true });
	}
	function drillInto(key: string) {
		const params = new URLSearchParams(page.url.searchParams);
		params.set('drill', key);
		void goto(`/tags?${params}`, { keepFocus: true, noScroll: true });
	}
	function clearDrill() {
		const params = new URLSearchParams(page.url.searchParams);
		params.delete('drill');
		void goto(`/tags${params.size ? '?' + params : ''}`, {
			keepFocus: true,
			noScroll: true
		});
	}

	// ── Data sources ─────────────────────────────────────────────────────────
	const labelsQuery = createQuery<PpLabel[]>(() => ({
		queryKey: ['labels'],
		queryFn: listLabels,
		enabled: isAuthenticated() && activeTab === 'labels'
	}));

	const keywordsQuery = createQuery<AggregatedKeyword[]>(() => ({
		queryKey: ['photos', 'keywords'],
		queryFn: aggregateKeywords,
		enabled: isAuthenticated() && activeTab === 'keywords',
		staleTime: 5 * 60_000
	}));

	const marksQuery = createQuery<PhotoMarksMap>(() => ({
		queryKey: ['marks'],
		queryFn: getAllMarks,
		enabled: isAuthenticated() && (activeTab === 'ratings' || activeTab === 'colors'),
		staleTime: 60_000
	}));

	// Marks → photo resolution. The sidecar's marks map is keyed by UID;
	// we need a thumbnail per UID, so pool the most-recent photo list.
	const marksPoolQuery = createQuery<PpPhoto[]>(() => ({
		queryKey: ['photos', 'marks-pool'],
		queryFn: () => listPhotos({ count: 1000, order: 'newest', merged: true }),
		enabled: isAuthenticated() && (activeTab === 'ratings' || activeTab === 'colors')
	}));

	// ── Group builders ───────────────────────────────────────────────────────
	interface RatingGroup {
		rating: number;
		photos: PpPhoto[];
	}
	const ratingGroups = $derived<RatingGroup[]>(
		buildRatingGroups(marksQuery.data, marksPoolQuery.data)
	);
	function buildRatingGroups(
		marks: PhotoMarksMap | undefined,
		pool: PpPhoto[] | undefined
	): RatingGroup[] {
		if (!marks || !pool) return [];
		const byUid = new Map(pool.map((p) => [p.UID, p]));
		const buckets = new Map<number, PpPhoto[]>();
		for (const [uid, mark] of Object.entries(marks)) {
			const r = mark.rating ?? 0;
			if (r <= 0) continue;
			const photo = byUid.get(uid);
			if (!photo) continue;
			const arr = buckets.get(r) ?? [];
			arr.push(photo);
			buckets.set(r, arr);
		}
		const out: RatingGroup[] = [];
		for (let r = 5; r >= 1; r--) {
			const photos = buckets.get(r);
			if (photos && photos.length > 0) out.push({ rating: r, photos });
		}
		return out;
	}

	// Titles follow the Lightroom culling convention so users see the
	// swatch's *intent* (reject/review/pick/keep), not just its color.
	// Used both as tooltip on swatches and as the visible card label in
	// the colors-tab picker grid below.
	const COLOR_SWATCHES: { key: string; bg: string; title: string }[] = [
		{ key: 'red', bg: 'bg-red-500', title: 'Red — reject' },
		{ key: 'orange', bg: 'bg-orange-500', title: 'Orange — review' },
		{ key: 'yellow', bg: 'bg-yellow-400', title: 'Yellow — pick' },
		{ key: 'green', bg: 'bg-green-500', title: 'Green — keep' }
	];
	interface ColorGroup {
		key: string;
		title: string;
		bg: string;
		photos: PpPhoto[];
	}
	const colorGroups = $derived<ColorGroup[]>(
		buildColorGroups(marksQuery.data, marksPoolQuery.data)
	);
	function buildColorGroups(
		marks: PhotoMarksMap | undefined,
		pool: PpPhoto[] | undefined
	): ColorGroup[] {
		if (!marks || !pool) return [];
		const byUid = new Map(pool.map((p) => [p.UID, p]));
		const buckets = new Map<string, PpPhoto[]>();
		for (const [uid, mark] of Object.entries(marks)) {
			const c = mark.color;
			if (!c) continue;
			const photo = byUid.get(uid);
			if (!photo) continue;
			const arr = buckets.get(c) ?? [];
			arr.push(photo);
			buckets.set(c, arr);
		}
		const out: ColorGroup[] = [];
		for (const swatch of COLOR_SWATCHES) {
			const photos = buckets.get(swatch.key);
			if (photos && photos.length > 0) out.push({ ...swatch, photos });
		}
		return out;
	}

	// ── Sorted full lists per tab (most-common first), then page slice ───────
	const labelsSorted = $derived(
		[...(labelsQuery.data ?? [])].sort((a, b) => (b.PhotoCount ?? 0) - (a.PhotoCount ?? 0))
	);
	const keywordsSorted = $derived(keywordsQuery.data ?? []); // already sorted
	// Ratings/Colors are small fixed-cardinality buckets — no pagination.
	const totalPages = $derived.by(() => {
		if (activeTab === 'labels') return Math.ceil(labelsSorted.length / PAGE_SIZE);
		if (activeTab === 'keywords') return Math.ceil(keywordsSorted.length / PAGE_SIZE);
		return 0;
	});
	const pageSlice = $derived.by(() => {
		const start = activePage * PAGE_SIZE;
		const end = start + PAGE_SIZE;
		if (activeTab === 'labels') return labelsSorted.slice(start, end);
		if (activeTab === 'keywords') return keywordsSorted.slice(start, end);
		return [];
	});

	// ── Drill-down: server-side q-DSL query for the picked group/tile ───────
	const drillQ = $derived.by(() => {
		if (!drillKey) return '';
		if (activeTab === 'labels') return `label:${drillKey}`;
		if (activeTab === 'keywords') return `keywords:${drillKey}`;
		return ''; // ratings/colors resolve locally from the marks pool
	});
	const drillPhotosQuery = createQuery<PpPhoto[]>(() => ({
		queryKey: ['photos', 'tag-drill', activeTab, drillQ],
		queryFn: () =>
			listPhotos({
				q: drillQ,
				count: 1000,
				order: 'newest',
				merged: true
			}),
		enabled: isAuthenticated() && Boolean(drillQ)
	}));

	// Local drill resolution (ratings / colors): the marks pool already
	// carries the photos; just pick the bucket the user clicked.
	const localDrillPhotos = $derived.by<PpPhoto[]>(() => {
		if (!drillKey) return [];
		if (activeTab === 'ratings') {
			const r = parseInt(drillKey, 10);
			return ratingGroups.find((g) => g.rating === r)?.photos ?? [];
		}
		if (activeTab === 'colors') {
			return colorGroups.find((g) => g.key === drillKey)?.photos ?? [];
		}
		return [];
	});

	const drillPhotos = $derived<PpPhoto[]>(
		activeTab === 'ratings' || activeTab === 'colors'
			? localDrillPhotos
			: drillPhotosQuery.data ?? []
	);

	function starLabel(rating: number): string {
		return '★'.repeat(rating);
	}

	// Friendly label for the current drill — used in the toolbar pill.
	const drillTitle = $derived.by(() => {
		if (!drillKey) return '';
		if (activeTab === 'labels') {
			const hit = (labelsQuery.data ?? []).find(
				(l) => (l.CustomSlug ?? l.Slug) === drillKey
			);
			return hit?.Name ?? drillKey;
		}
		if (activeTab === 'keywords') return drillKey;
		if (activeTab === 'ratings') return starLabel(parseInt(drillKey, 10));
		if (activeTab === 'colors') {
			return COLOR_SWATCHES.find((c) => c.key === drillKey)?.title ?? drillKey;
		}
		return drillKey;
	});

	const drillCount = $derived<number>(drillPhotos.length);
</script>

<Toolbar>
	<span class="rounded-md border border-border px-2 py-0.5 text-[11px] text-muted-foreground">
		Tags
	</span>
	{#if drillKey}
		<button
			type="button"
			class="rounded border border-border px-2 py-0.5 text-xs hover:bg-accent"
			onclick={clearDrill}
		>
			← Back
		</button>
		<span class="text-[11px] font-medium">{drillTitle}</span>
		<span class="text-[11px] text-muted-foreground">
			{drillCount} photo{drillCount === 1 ? '' : 's'}
		</span>
	{:else}
		<!-- Pill row of tabs. The active tab uses the same primary-tinted
		     treatment as the sidebar's active rows so the active state
		     reads consistently across the app. -->
		<div class="flex items-center gap-1">
			{#each TABS as t (t.id)}
				<button
					type="button"
					class="rounded border px-2 py-0.5 text-[11px] {activeTab === t.id
						? 'border-primary/40 bg-primary/10 text-primary'
						: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'}"
					onclick={() => setTab(t.id)}
				>
					{t.label}
				</button>
			{/each}
		</div>
	{/if}
	{#snippet trailing()}
		{#if !drillKey && (activeTab === 'labels' || activeTab === 'keywords') && totalPages > 1}
			<!-- Page navigator only renders when there's actually a page to
			     turn — the ratings/colors tabs cap at 5 / 4 buckets which
			     always fits a single screen. -->
			<button
				type="button"
				class="rounded border border-border px-2 py-0.5 text-xs hover:bg-accent disabled:opacity-50"
				disabled={activePage === 0}
				onclick={() => setPage(activePage - 1)}
			>
				‹
			</button>
			<span class="text-[11px] tabular-nums text-muted-foreground">
				{activePage + 1} / {totalPages}
			</span>
			<button
				type="button"
				class="rounded border border-border px-2 py-0.5 text-xs hover:bg-accent disabled:opacity-50"
				disabled={activePage >= totalPages - 1}
				onclick={() => setPage(activePage + 1)}
			>
				›
			</button>
		{/if}
	{/snippet}
</Toolbar>

<main
	class="min-h-0 flex-1 overflow-y-auto p-6 outline-none focus:outline-none"
	use:gridKeyNav={{}}
>
	{#if drillKey}
		<!-- Drill-down photo grid. Reused for all four tabs; the q-DSL drives
		     labels/keywords while ratings/colors resolve locally from the
		     marks pool already in cache. -->
		{#if activeTab !== 'ratings' && activeTab !== 'colors' && drillPhotosQuery.isPending}
			<SkeletonGrid />
		{:else if activeTab !== 'ratings' && activeTab !== 'colors' && drillPhotosQuery.isError}
			<p class="text-sm text-destructive">Failed to load photos.</p>
		{:else if drillPhotos.length === 0}
			<p class="text-sm text-muted-foreground">No photos under this tag.</p>
		{:else}
			<PhotoGrid photos={drillPhotos} />
		{/if}
	{:else if activeTab === 'labels'}
		{#if labelsQuery.isPending}
			<SkeletonGrid />
		{:else if labelsQuery.isError}
			<p class="text-sm text-destructive">Failed to load labels.</p>
		{:else if labelsSorted.length === 0}
			<p class="text-sm text-muted-foreground">
				No labels yet. PhotoPrism's TensorFlow indexer generates these from photo content;
				if the indexer hasn't run on real photos yet, the list will be empty.
			</p>
		{:else}
			<div
				class="grid gap-2"
				style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
			>
				{#each pageSlice as item (item)}
					{@const label = item as PpLabel}
					<button
						type="button"
						class="group relative aspect-square overflow-hidden rounded-md border border-border bg-secondary p-0 text-left outline-none focus:outline-none"
						onclick={() => drillInto(label.CustomSlug ?? label.Slug)}
					>
						{#if label.Thumb}
							<img
								src={thumbUrl(label.Thumb, 'tile_500')}
								alt={label.Name}
								loading="lazy"
								class="h-full w-full object-cover transition group-hover:scale-105"
							/>
						{/if}
						<div
							class="absolute inset-x-0 bottom-0 flex items-center justify-between bg-background/85 px-2 py-1.5 text-xs"
						>
							<span class="truncate font-medium">{label.Name}</span>
							<span class="text-muted-foreground">{label.PhotoCount ?? 0}</span>
						</div>
					</button>
				{/each}
			</div>
		{/if}
	{:else if activeTab === 'keywords'}
		{#if keywordsQuery.isPending}
			<SkeletonGrid />
		{:else if keywordsQuery.isError}
			<p class="text-sm text-destructive">Failed to load keywords.</p>
		{:else if keywordsSorted.length === 0}
			<p class="text-sm text-muted-foreground">
				No user-set keywords yet. Add them from a photo's right-sidebar metadata panel.
			</p>
		{:else}
			<div
				class="grid gap-2"
				style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
			>
				{#each pageSlice as item (item)}
					{@const kw = item as AggregatedKeyword}
					<button
						type="button"
						class="group relative aspect-square overflow-hidden rounded-md border border-border bg-secondary p-0 text-left outline-none focus:outline-none"
						onclick={() => drillInto(kw.keyword)}
					>
						<img
							src={thumbUrl(kw.sampleHash, 'tile_500')}
							alt={kw.keyword}
							loading="lazy"
							class="h-full w-full object-cover transition group-hover:scale-105"
						/>
						<div
							class="absolute inset-x-0 bottom-0 flex items-center justify-between bg-background/85 px-2 py-1.5 text-xs"
						>
							<span class="truncate font-medium">{kw.keyword}</span>
							<span class="text-muted-foreground">{kw.count}</span>
						</div>
					</button>
				{/each}
			</div>
		{/if}
	{:else if activeTab === 'ratings'}
		{#if marksQuery.isPending || marksPoolQuery.isPending}
			<SkeletonGrid count={5} />
		{:else if marksQuery.isError || marksPoolQuery.isError}
			<p class="text-sm text-destructive">Failed to load ratings.</p>
		{:else if ratingGroups.length === 0}
			<p class="text-sm text-muted-foreground">
				No rated photos yet. Open a photo and use the star row in the right sidebar (or
				1–5 in bulk mode) to rate it.
			</p>
		{:else}
			<div
				class="grid gap-2"
				style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
			>
				{#each ratingGroups as group (group.rating)}
					{@const rep = group.photos[0]}
					{@const hash = rep.Hash ?? primaryFile(rep).Hash}
					<button
						type="button"
						class="group relative aspect-square overflow-hidden rounded-md border border-border bg-secondary p-0 text-left outline-none focus:outline-none"
						onclick={() => drillInto(String(group.rating))}
					>
						<img
							src={thumbUrl(hash, 'tile_500')}
							alt={starLabel(group.rating)}
							loading="lazy"
							class="h-full w-full object-cover transition group-hover:scale-105"
						/>
						<div
							class="absolute inset-x-0 bottom-0 flex items-center justify-between bg-background/85 px-2 py-1.5 text-xs"
						>
							<span class="truncate font-medium text-yellow-500">
								{starLabel(group.rating)}
							</span>
							<span class="text-muted-foreground">{group.photos.length}</span>
						</div>
					</button>
				{/each}
			</div>
		{/if}
	{:else if activeTab === 'colors'}
		{#if marksQuery.isPending || marksPoolQuery.isPending}
			<SkeletonGrid count={4} />
		{:else if marksQuery.isError || marksPoolQuery.isError}
			<p class="text-sm text-destructive">Failed to load colors.</p>
		{:else if colorGroups.length === 0}
			<p class="text-sm text-muted-foreground">
				No color labels yet. Open a photo and use the four-swatch row in the right
				sidebar to tag it.
			</p>
		{:else}
			<div
				class="grid gap-2"
				style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
			>
				{#each colorGroups as group (group.key)}
					{@const rep = group.photos[0]}
					{@const hash = rep.Hash ?? primaryFile(rep).Hash}
					<button
						type="button"
						class="group relative aspect-square overflow-hidden rounded-md border border-border bg-secondary p-0 text-left outline-none focus:outline-none"
						onclick={() => drillInto(group.key)}
					>
						<img
							src={thumbUrl(hash, 'tile_500')}
							alt={group.title}
							loading="lazy"
							class="h-full w-full object-cover transition group-hover:scale-105"
						/>
						<div
							class="absolute inset-x-0 bottom-0 flex items-center justify-between bg-background/85 px-2 py-1.5 text-xs"
						>
							<span class="flex items-center gap-1.5 truncate font-medium">
								<span class="h-2.5 w-2.5 rounded-full {group.bg}"></span>
								{group.title}
							</span>
							<span class="text-muted-foreground">{group.photos.length}</span>
						</div>
					</button>
				{/each}
			</div>
		{/if}
	{/if}
</main>

<BulkActionBar />
