<script lang="ts">
import { goto } from '$app/navigation';
import { navigating } from '$app/state';
import { untrack } from 'svelte';
import { SvelteURLSearchParams } from 'svelte/reactivity';
import SearchFilterBar from '../SearchFilterBar.svelte';
import DocumentList from '../DocumentList.svelte';
import Pagination from '$lib/shared/primitives/Pagination.svelte';
import BulkSelectionBar from '$lib/document/BulkSelectionBar.svelte';
import TimelineDensityFilter from '$lib/document/TimelineDensityFilter.svelte';
import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte';
import { getErrorMessage, parseBackendError } from '$lib/shared/errors';
import * as m from '$lib/paraglide/messages.js';

let { data } = $props();

// Local state initialised from server-returned filter values.
// untrack() prevents infinite reactive loops during initialisation.
let q = $state(untrack(() => data.q || ''));
let qFocused = $state(false);
let from = $state(untrack(() => data.from || ''));
let to = $state(untrack(() => data.to || ''));
let senderId = $state(untrack(() => data.senderId || ''));
let receiverId = $state(untrack(() => data.receiverId || ''));
let initialSenderName = $state(untrack(() => data.initialSenderName ?? ''));
let initialReceiverName = $state(untrack(() => data.initialReceiverName ?? ''));
let navKey = $state(0);
let tagNames = $state<{ name: string; id?: string; color?: string; parentId?: string }[]>(
	untrack(() => (data.tags || []).map((name: string) => ({ name })))
);
let sort = $state(untrack(() => data.sort || 'DATE'));
let dir = $state(untrack(() => data.dir || 'desc'));
let tagQ = $state(untrack(() => data.tagQ || ''));
let tagOperator = $state<'AND' | 'OR'>(untrack(() => (data.tagOp as 'AND' | 'OR') || 'AND'));
let undated = $state(untrack(() => data.undated ?? false));

function hasAdvancedFilters() {
	return (
		(data.tags?.length ?? 0) > 0 ||
		!!data.senderId ||
		!!data.receiverId ||
		!!data.from ||
		!!data.to ||
		!!data.undated
	);
}

let showAdvanced = $state(untrack(hasAdvancedFilters));

let searchTimer: ReturnType<typeof setTimeout>;

type FilterSnapshot = {
	q: string;
	from: string;
	to: string;
	senderId: string;
	receiverId: string;
	tags: string[];
	sort: string;
	dir: string;
	tagQ: string;
	tagOp: 'AND' | 'OR';
	undated: boolean;
	zoomFrom?: string | null;
	zoomTo?: string | null;
};

/**
 * Builds a URLSearchParams from a filter snapshot. Single source of truth for
 * which params the `/documents` URL understands — add a filter here and both
 * filter-change nav (triggerSearch) and page nav (buildPageHref) will pick it
 * up. `page` is appended only when > 0 so the default page 0 stays out of the
 * URL, keeping the filter-change-resets-to-page-0 behaviour implicit.
 */
function buildSearchParams(filters: FilterSnapshot, targetPage?: number): SvelteURLSearchParams {
	const params = new SvelteURLSearchParams();
	if (filters.q) params.set('q', filters.q);
	if (filters.from) params.set('from', filters.from);
	if (filters.to) params.set('to', filters.to);
	if (filters.senderId) params.set('senderId', filters.senderId);
	if (filters.receiverId) params.set('receiverId', filters.receiverId);
	filters.tags.forEach((tag) => params.append('tag', tag));
	if (filters.sort) params.set('sort', filters.sort);
	if (filters.dir) params.set('dir', filters.dir);
	if (filters.tagQ) params.set('tagQ', filters.tagQ);
	if (filters.tagOp === 'OR') params.set('tagOp', 'OR');
	if (filters.undated) params.set('undated', 'true');
	if (filters.zoomFrom) params.set('zoomFrom', filters.zoomFrom);
	if (filters.zoomTo) params.set('zoomTo', filters.zoomTo);
	if (targetPage !== undefined && targetPage > 0) params.set('page', String(targetPage));
	return params;
}

/**
 * Rebuilds the URL from the CURRENT local filter state, preserving the zoom
 * range carried in `data.zoom{From,To}`. `page` is intentionally not carried
 * over — any filter change implicitly resets back to page 0.
 */
function triggerSearchKeepZoom() {
	navigateWithZoom(data.zoomFrom ?? null, data.zoomTo ?? null);
}

/**
 * Rebuilds the URL from the CURRENT local filter state and replaces the zoom
 * range with the provided values (or clears it if both are null).
 */
function triggerSearchWithZoom(zoomFrom: string | null, zoomTo: string | null) {
	navigateWithZoom(zoomFrom, zoomTo);
}

function navigateWithZoom(zoomFrom: string | null, zoomTo: string | null) {
	const params = buildSearchParams({
		q,
		from,
		to,
		senderId,
		receiverId,
		tags: tagNames.map((t) => t.name),
		sort,
		dir,
		tagQ,
		tagOp: tagOperator,
		undated,
		zoomFrom,
		zoomTo
	});
	goto(`/documents?${params.toString()}`, { keepFocus: true, noScroll: true });
}

/**
 * Builds the href for a Pagination prev/next link. Preserves every filter
 * param from server `data` and updates `page`. Uses a normal <a href> (not
 * goto) so SvelteKit's default scroll restoration brings the user to the top
 * of the new slice — the expected behaviour for page navigation.
 */
function buildPageHref(targetPage: number): string {
	const params = buildSearchParams(
		{
			q: data.q || '',
			from: data.from || '',
			to: data.to || '',
			senderId: data.senderId || '',
			receiverId: data.receiverId || '',
			tags: data.tags || [],
			sort: data.sort || '',
			dir: data.dir || '',
			tagQ: data.tagQ || '',
			tagOp: (data.tagOp as 'AND' | 'OR') || 'AND',
			undated: data.undated ?? false
		},
		targetPage
	);
	const qs = params.toString();
	return qs ? `/documents?${qs}` : '/documents';
}

function handleTextSearch() {
	clearTimeout(searchTimer);
	searchTimer = setTimeout(() => triggerSearchKeepZoom(), 500);
}

function handleImmediateSearch() {
	clearTimeout(searchTimer);
	triggerSearchKeepZoom();
}

// Trigger search reactively when the tag list changes.
let prevTagStr = untrack(() => tagNames.map((t) => t.name).join(','));
$effect(() => {
	const cur = tagNames.map((t) => t.name).join(',');
	if (cur !== prevTagStr) {
		prevTagStr = cur;
		triggerSearchKeepZoom();
	}
});

let editingAll = $state(false);
let editAllError = $state<string | null>(null);

/**
 * Fast path: replace the current selection with every document matching the
 * active filter (across all pages) and jump to the bulk-edit screen. The
 * /api/documents/ids endpoint is hard-capped (5000 results); on cap overflow
 * the backend returns BULK_EDIT_TOO_MANY_IDS, which we surface inline.
 */
async function editAllMatching() {
	if (editingAll) return;
	editingAll = true;
	editAllError = null;
	try {
		const params = buildSearchParams({
			q: data.q || '',
			from: data.from || '',
			to: data.to || '',
			senderId: data.senderId || '',
			receiverId: data.receiverId || '',
			tags: data.tags || [],
			sort: '',
			dir: '',
			tagQ: data.tagQ || '',
			tagOp: (data.tagOp as 'AND' | 'OR') || 'AND',
			undated: data.undated ?? false
		});
		params.delete('sort');
		params.delete('dir');
		const res = await fetch(`/api/documents/ids?${params.toString()}`);
		if (!res.ok) {
			const backend = await parseBackendError(res);
			editAllError = getErrorMessage(backend?.code);
			return;
		}
		const ids: string[] = await res.json();
		bulkSelectionStore.setAll(ids);
		await goto('/documents/bulk-edit');
	} catch {
		editAllError = m.bulk_edit_all_x_failed();
	} finally {
		editingAll = false;
	}
}

// Keep local filter state in sync with server data after navigation completes.
// Guard q: skip overwrite while the user is actively typing.
// navKey increments on every navigation so PersonTypeahead clears manually-typed
// terms even when initialSenderName/initialReceiverName stays '' across navigations.
$effect(() => {
	if (!qFocused) q = data.q || '';
	from = data.from || '';
	to = data.to || '';
	senderId = data.senderId || '';
	receiverId = data.receiverId || '';
	initialSenderName = data.initialSenderName ?? '';
	initialReceiverName = data.initialReceiverName ?? '';
	untrack(() => navKey++);
	tagNames = (data.tags || []).map((name: string) => ({ name }));
	sort = data.sort || 'DATE';
	dir = data.dir || 'desc';
	tagQ = data.tagQ || '';
	tagOperator = (data.tagOp as 'AND' | 'OR') || 'AND';
	undated = data.undated ?? false;
	if (hasAdvancedFilters()) showAdvanced = true;
});
</script>

<svelte:head>
	<title>{m.nav_documents()} – Familienarchiv</title>
</svelte:head>

<!-- Reserve bottom padding when the bulk-selection bar is visible so the
	 sticky bar does not occlude the last document row or the pagination
	 controls (WCAG 1.4.10 / 2.4.7). -->
<main
	class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8"
	class:pb-32={bulkSelectionStore.size > 0 && data.canWrite}
>
	<h1 class="sr-only">{m.nav_documents()}</h1>

	<SearchFilterBar
		bind:q={q}
		bind:from={from}
		bind:to={to}
		bind:senderId={senderId}
		bind:receiverId={receiverId}
		bind:tagNames={tagNames}
		bind:showAdvanced={showAdvanced}
		bind:sort={sort}
		bind:dir={dir}
		bind:tagQ={tagQ}
		bind:tagOperator={tagOperator}
		bind:undated={undated}
		undatedCount={data.undatedCount ?? 0}
		initialSenderName={initialSenderName}
		initialReceiverName={initialReceiverName}
		navKey={navKey}
		isLoading={navigating.to !== null}
		onSearch={handleTextSearch}
		onSearchImmediate={handleImmediateSearch}
		onfocus={() => (qFocused = true)}
		onblur={() => (qFocused = false)}
	/>

	<div class="mt-3 mb-4 hidden lg:block">
		<TimelineDensityFilter
			density={data.density}
			minDate={data.minDate}
			maxDate={data.maxDate}
			zoomFrom={data.zoomFrom}
			zoomTo={data.zoomTo}
			from={from}
			to={to}
			onchange={(event) => {
				from = event.from;
				to = event.to;
				// Drag commits filter + zoom atomically (Graylog-style range selector).
				// Single click and clear omit zoomFrom/zoomTo so existing zoom is preserved.
				if ('zoomFrom' in event) {
					triggerSearchWithZoom(event.zoomFrom ?? null, event.zoomTo ?? null);
				} else {
					triggerSearchKeepZoom();
				}
			}}
			onzoomchange={(event) => {
				triggerSearchWithZoom(event?.zoomFrom ?? null, event?.zoomTo ?? null);
			}}
		/>
	</div>

	<div class="mb-3 flex items-center justify-between gap-4">
		<p class="font-sans text-base text-ink-2">
			{#if data.totalElements > 0}{m.docs_result_count({ count: data.totalElements })}{/if}
		</p>
		{#if data.canWrite}
			<div class="flex flex-col items-end gap-1">
				<div class="flex items-center gap-4">
					{#if data.totalElements > 0}
						<button
							type="button"
							onclick={editAllMatching}
							disabled={editingAll}
							class="inline-flex cursor-pointer items-center gap-1 text-sm font-medium text-ink-2 transition-colors hover:text-ink disabled:opacity-50"
							data-testid="bulk-edit-all-x"
						>
							<img
								src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg"
								alt=""
								aria-hidden="true"
								class="h-4 w-4"
							/>
							{m.bulk_edit_all_x({ count: data.totalElements })}
						</button>
					{/if}
					<a
						href="/documents/new"
						class="inline-flex items-center gap-1 text-sm font-medium text-ink-2 transition-colors hover:text-ink"
					>
						<img
							src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg"
							alt=""
							aria-hidden="true"
							class="h-4 w-4"
						/>
						{m.docs_btn_new()}
					</a>
				</div>
				{#if editAllError}
					<p role="alert" class="text-xs text-danger" data-testid="bulk-edit-all-x-error">
						{editAllError}
					</p>
				{/if}
			</div>
		{/if}
	</div>

	<DocumentList
		items={data.items}
		q={data.q}
		canWrite={data.canWrite}
		error={data.error}
		sort={sort}
		from={data.from}
		to={data.to}
	/>

	<Pagination page={data.pageNumber} totalPages={data.totalPages} makeHref={buildPageHref} />
</main>

<BulkSelectionBar canWrite={data.canWrite} />
