<!--
	General app preferences. The UI tab owns the SvelteKit shell's
	light/dark/system theme (mode-watcher) plus the per-user UI knobs
	PhotoPrism's /settings exposes. Search and Maps follow the same
	pattern — server prefs round-trip via /api/v1/settings.

	The Library admin dialog and this one share the ['settings'] cache,
	so saves from either invalidate the other.
-->
<script lang="ts">
	import { Dialog, Tabs } from 'bits-ui';
	import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query';
	import { mode, setMode } from 'mode-watcher';
	import { toast } from 'svelte-sonner';
	import { Loader2, Monitor, Moon, Settings as SettingsIcon, Sun, X } from 'lucide-svelte';
	import {
		getSettings,
		saveSettings,
		type PpSettings
	} from '$lib/services/photoprism';

	interface Props {
		open: boolean;
		onClose: () => void;
	}
	let { open, onClose }: Props = $props();

	const qc = useQueryClient();

	let activeTab = $state<'ui' | 'search' | 'maps'>('ui');

	const themeOptions = [
		{ value: 'light', label: 'Light', Icon: Sun },
		{ value: 'dark', label: 'Dark', Icon: Moon },
		{ value: 'system', label: 'System', Icon: Monitor }
	] as const;

	// PhotoPrism palette names from its built-in themes. Any value
	// outside this list is preserved verbatim (see `withCurrent`).
	const ppThemes = [
		'default',
		'abyss',
		'gemstone',
		'grayscale',
		'lavender',
		'legacy',
		'neon',
		'onyx',
		'raspberry',
		'shadow',
		'yellowstone'
	];

	// IETF subtags PhotoPrism ships translations for. Extend without
	// fear — `withCurrent` keeps unknown values visible.
	const ppLanguages = [
		'en', 'de', 'es', 'fr', 'it', 'pt', 'nl', 'pl', 'cs', 'sk',
		'sv', 'no', 'da', 'fi', 'hu', 'ro', 'bg', 'el', 'ru', 'uk',
		'tr', 'ar', 'he', 'hi', 'vi', 'th', 'ja', 'ko', 'zh'
	];

	const ppStartPages = [
		'default',
		'browse',
		'albums',
		'favorites',
		'calendar',
		'moments',
		'people',
		'places',
		'labels',
		'states',
		'library'
	];

	const ppMapStyles = ['default', 'streets', 'hybrid', 'topographique', 'offline'];

	// Returns `opts` with `current` prepended if it's set and not
	// already in the list — so e.g. an experimental theme name in the
	// server response shows up selected and editable instead of
	// silently being overwritten by the dropdown's default.
	function withCurrent(opts: string[], current?: string): string[] {
		if (!current) return opts;
		return opts.includes(current) ? opts : [current, ...opts];
	}

	const settingsQuery = createQuery<PpSettings>(() => ({
		queryKey: ['settings'],
		queryFn: getSettings,
		enabled: open
	}));

	let draft = $state<PpSettings | null>(null);
	$effect(() => {
		if (settingsQuery.data && draft === null) {
			draft = structuredClone(settingsQuery.data);
		}
	});
	$effect(() => {
		if (!open) draft = null;
	});

	const saveMut = createMutation(() => ({
		mutationFn: (patch: PpSettings) => saveSettings(patch),
		onSuccess: (next) => {
			qc.setQueryData(['settings'], next);
			draft = structuredClone(next);
			toast.success('Settings saved');
		},
		onError: (err) =>
			toast.error(err instanceof Error ? err.message : 'Could not save settings')
	}));

	function resetDraft() {
		if (settingsQuery.data) draft = structuredClone(settingsQuery.data);
	}

	const selectClass =
		'rounded border border-input bg-background px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring';
</script>

<Dialog.Root
	{open}
	onOpenChange={(o) => {
		if (!o) onClose();
	}}
>
	<Dialog.Portal>
		<Dialog.Overlay
			class="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
		/>
		<Dialog.Content
			class="fixed left-1/2 top-1/2 z-50 grid w-full max-w-[560px] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border border-border bg-card p-5 text-card-foreground shadow-lg outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95"
		>
			<div class="flex items-start gap-2">
				<SettingsIcon class="mt-0.5 h-4 w-4 text-muted-foreground" />
				<div class="flex-1">
					<Dialog.Title class="text-sm font-semibold leading-tight">
						General settings
					</Dialog.Title>
					<Dialog.Description class="mt-1 text-xs text-muted-foreground">
						Preferences for this app and your PhotoPrism account.
						Library admin lives under Folders → ⚙.
					</Dialog.Description>
				</div>
				<Dialog.Close
					class="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
					aria-label="Close"
				>
					<X class="h-3.5 w-3.5" />
				</Dialog.Close>
			</div>

			<Tabs.Root bind:value={activeTab}>
				<Tabs.List class="mb-3 flex gap-1 border-b border-border">
					{#each ['ui', 'search', 'maps'] as const as t (t)}
						<Tabs.Trigger
							value={t}
							class="-mb-px border-b-2 border-transparent px-3 py-1.5 text-[12px] capitalize text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground"
						>
							{t}
						</Tabs.Trigger>
					{/each}
				</Tabs.List>

				<!-- UI — local app theme (mode-watcher) on top, then the
				     PhotoPrism per-user UI knobs that go to /settings. -->
				<Tabs.Content value="ui" class="space-y-4 text-[12px] outline-none">
					<section class="space-y-2">
						<h3 class="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
							App theme
						</h3>
						<div
							class="flex items-center overflow-hidden rounded-md border border-border"
							role="group"
							aria-label="Theme"
						>
							{#each themeOptions as opt (opt.value)}
								{@const active = mode.current === opt.value}
								<button
									type="button"
									class="flex flex-1 items-center justify-center gap-1.5 px-3 py-1.5 hover:bg-accent"
									class:bg-primary={active}
									class:text-primary-foreground={active}
									class:hover:bg-primary={active}
									onclick={() => setMode(opt.value)}
								>
									<opt.Icon class="h-3.5 w-3.5" />
									{opt.label}
								</button>
							{/each}
						</div>
						<p class="text-[11px] text-muted-foreground">
							Light/dark for this app. Persists locally; no Save needed.
						</p>
					</section>

					{#if settingsQuery.isPending}
						<p class="px-1 text-muted-foreground">Loading PhotoPrism settings…</p>
					{:else if settingsQuery.isError}
						<p class="px-1 text-destructive">Could not load PhotoPrism settings.</p>
					{:else if draft}
						<section class="space-y-3">
							<h3 class="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
								PhotoPrism UI
							</h3>
							<label class="flex flex-col gap-1">
								<span class="text-muted-foreground">Theme</span>
								<select bind:value={draft.ui!.theme} class={selectClass}>
									{#each withCurrent(ppThemes, draft.ui!.theme) as v (v)}
										<option value={v}>{v}</option>
									{/each}
								</select>
							</label>
							<label class="flex flex-col gap-1">
								<span class="text-muted-foreground">Language</span>
								<select bind:value={draft.ui!.language} class={selectClass}>
									{#each withCurrent(ppLanguages, draft.ui!.language) as v (v)}
										<option value={v}>{v}</option>
									{/each}
								</select>
							</label>
							<label class="flex flex-col gap-1">
								<span class="text-muted-foreground">Time zone</span>
								<!-- IANA tz list is ~400 entries, browser support varies; use
								     a datalist so we get autocomplete without spamming a
								     gigantic <select>. "Local" is PhotoPrism's special
								     "follow system" sentinel. -->
								<input
									type="text"
									list="general-tz-list"
									placeholder="Local"
									bind:value={draft.ui!.timeZone}
									class={selectClass}
								/>
							</label>
							<label class="flex flex-col gap-1">
								<span class="text-muted-foreground">Start page</span>
								<select bind:value={draft.ui!.startPage} class={selectClass}>
									{#each withCurrent(ppStartPages, draft.ui!.startPage) as v (v)}
										<option value={v}>{v}</option>
									{/each}
								</select>
							</label>
							<label class="flex items-center gap-2">
								<input type="checkbox" bind:checked={draft.ui!.scrollbar} />
								Always show scrollbars
							</label>
							<label class="flex items-center gap-2">
								<input type="checkbox" bind:checked={draft.ui!.zoom} />
								Allow image zoom
							</label>
						</section>
					{/if}
				</Tabs.Content>

				{#if settingsQuery.isPending && activeTab !== 'ui'}
					<Tabs.Content value={activeTab} class="outline-none">
						<p class="px-1 text-[12px] text-muted-foreground">Loading settings…</p>
					</Tabs.Content>
				{:else if settingsQuery.isError && activeTab !== 'ui'}
					<Tabs.Content value={activeTab} class="outline-none">
						<p class="px-1 text-[12px] text-destructive">
							Could not load settings.
						</p>
					</Tabs.Content>
				{:else if draft}
					<Tabs.Content value="search" class="space-y-3 text-[12px] outline-none">
						<label class="flex items-center gap-2">
							<input type="checkbox" bind:checked={draft.search!.listView} />
							Default to list view
						</label>
						<label class="flex items-center gap-2">
							<input type="checkbox" bind:checked={draft.search!.showTitles} />
							Show titles
						</label>
						<label class="flex items-center gap-2">
							<input type="checkbox" bind:checked={draft.search!.showCaptions} />
							Show captions
						</label>
						<label class="flex flex-col gap-1">
							<span class="text-muted-foreground">
								Batch size (-1 = server default)
							</span>
							<input
								type="number"
								bind:value={draft.search!.batchSize}
								class={selectClass}
							/>
						</label>
					</Tabs.Content>

					<Tabs.Content value="maps" class="space-y-3 text-[12px] outline-none">
						<label class="flex flex-col gap-1">
							<span class="text-muted-foreground">Style</span>
							<select bind:value={draft.maps!.style} class={selectClass}>
								{#each withCurrent(ppMapStyles, draft.maps!.style) as v (v)}
									<option value={v}>{v}</option>
								{/each}
							</select>
						</label>
						<label class="flex flex-col gap-1">
							<span class="text-muted-foreground">
								Animation duration (ms, 0 = off)
							</span>
							<input
								type="number"
								bind:value={draft.maps!.animate}
								class={selectClass}
							/>
						</label>
					</Tabs.Content>
				{/if}
			</Tabs.Root>

			<!-- Datalist for time-zone autocomplete. Falls back to the
			     "Local" sentinel when the browser can't enumerate the
			     IANA list (older Safari, etc.). -->
			<datalist id="general-tz-list">
				<option value="Local"></option>
				{#each tzOptions() as tz (tz)}<option value={tz}></option>{/each}
			</datalist>

			<!-- Save/Revert apply to draft (the PhotoPrism /settings round
			     trip). The App theme group above persists itself, so we
			     only show the action row when there's something to save. -->
			{#if draft}
				<div class="flex items-center justify-end gap-2 border-t border-border pt-3">
					<button
						type="button"
						class="rounded border border-border px-3 py-1 text-[12px] hover:bg-accent"
						onclick={resetDraft}
						disabled={saveMut.isPending}
					>
						Revert
					</button>
					<button
						type="button"
						class="flex items-center gap-1.5 rounded bg-primary px-3 py-1 text-[12px] text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
						onclick={() => draft && saveMut.mutate(draft)}
						disabled={saveMut.isPending}
					>
						{#if saveMut.isPending}
							<Loader2 class="h-3 w-3 animate-spin" />
						{/if}
						Save
					</button>
				</div>
			{/if}
		</Dialog.Content>
	</Dialog.Portal>
</Dialog.Root>

<script lang="ts" module>
	// `Intl.supportedValuesOf` is a 2022+ API; older browsers (Safari
	// 15.3 and below) return undefined here. The component handles that
	// by simply showing only the "Local" sentinel in the datalist.
	export function tzOptions(): string[] {
		const fn = (Intl as unknown as {
			supportedValuesOf?: (k: string) => string[];
		}).supportedValuesOf;
		if (typeof fn !== 'function') return [];
		try {
			return fn('timeZone');
		} catch {
			return [];
		}
	}
</script>
