<script lang="ts">
import { untrack, tick, onMount } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
import { page } from '$app/state';
import { replaceState } from '$app/navigation';
import StammbaumTree from '$lib/person/genealogy/StammbaumTree.svelte';
import StammbaumSidePanel from '$lib/person/genealogy/StammbaumSidePanel.svelte';
import StammbaumBottomSheet from '$lib/person/genealogy/StammbaumBottomSheet.svelte';
import StammbaumControls from '$lib/person/genealogy/StammbaumControls.svelte';
import StammbaumAffordance from '$lib/person/genealogy/StammbaumAffordance.svelte';
import {
	type PanZoomState,
	DEFAULT_VIEW,
	clampZoom,
	serializePanZoomParams,
	ZOOM_STEP_KB
} from '$lib/person/genealogy/panZoom';
import { animateView, prefersReducedMotion } from '$lib/person/genealogy/animateView';
import type { components } from '$lib/generated/api';

type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
type RelationshipDTO = components['schemas']['RelationshipDTO'];

interface Props {
	data: { nodes: PersonNodeDTO[]; edges: RelationshipDTO[]; initialView: PanZoomState };
}

let { data }: Props = $props();

const canWrite = $derived<boolean>(page.data.canWrite ?? false);

const focusId = page.url.searchParams.get('focus');
let selectedId = $state<string | null>(
	focusId && data.nodes.some((n) => n.id === focusId) ? focusId : null
);

const selectedNode = $derived(data.nodes.find((n) => n.id === selectedId) ?? null);

let view = $state<PanZoomState>(data.initialView);
let canvasActivity = $state(false);
function zoomIn() {
	view = { ...view, z: clampZoom(view.z + ZOOM_STEP_KB) };
}
function zoomOut() {
	view = { ...view, z: clampZoom(view.z - ZOOM_STEP_KB) };
}
// One-shot recentre trigger: set the focal id, let StammbaumTree's effect read
// it and emit the recentred view, then clear so the same person can be
// re-centred on a later click (US-PAN-005).
let centreOnId = $state<string | null>(null);
async function centreOnSelected() {
	centreOnId = selectedId;
	await tick();
	centreOnId = null;
}

// Below the md breakpoint the side panel is replaced by a bottom sheet that
// overlays the lower ~60dvh of the canvas. On a phone we therefore auto-centre
// the tapped person into the band above the sheet (#703 AC8); on desktop the
// panel is a flex sibling that never covers the tree, so no centring is needed.
const MOBILE_QUERY = '(max-width: 767px)';
/** How far above the viewBox centre to lift the tapped anchor on mobile. */
const MOBILE_CENTRE_BIAS = 0.3;
let isMobile = $state(
	typeof window !== 'undefined' && typeof window.matchMedia === 'function'
		? window.matchMedia(MOBILE_QUERY).matches
		: false
);
$effect(() => {
	if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
	const mq = window.matchMedia(MOBILE_QUERY);
	const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
	mq.addEventListener('change', handler);
	return () => mq.removeEventListener('change', handler);
});

async function selectPerson(id: string) {
	selectedId = id;
	if (isMobile) await centreOnSelected();
}

let cancelAnimation = () => {};
function fitToScreen() {
	cancelAnimation();
	cancelAnimation = animateView(view, DEFAULT_VIEW, (v) => (view = v), {
		reducedMotion: prefersReducedMotion()
	});
}

// SvelteKit's replaceState throws "before the router is initialized" if called
// during hydration (the router sets `started = true` only after onMount + the
// first effect tick). Gate the URL sync on a flag flipped after the first
// post-mount tick() — which resolves once hydration is complete — so the write
// only ever runs against a ready router.
let routerReady = $state(false);
onMount(() => {
	tick().then(() => (routerReady = true));
});

// Mirror the view into shareable ?cx&cy&z params (OQ-003). Only `view` and
// `routerReady` are tracked; the current URL is read untracked so the
// replaceState write does not retrigger the effect. The state thus survives
// panel open/close (US-PANEL-002 AC1) and a shared link reproduces it (AC2).
$effect(() => {
	const { cx, cy, z } = serializePanZoomParams(view);
	if (!routerReady) return;
	untrack(() => {
		const url = new URL(window.location.href);
		url.searchParams.set('cx', cx);
		url.searchParams.set('cy', cy);
		url.searchParams.set('z', z);
		try {
			replaceState(url, page.state);
		} catch {
			// Router not ready yet — the next view change retries.
		}
	});
});
</script>

<!-- 4.25rem = 4rem navbar (h-16) + 0.25rem accent strip (h-1).
     -my-6 cancels <main>'s py-6 so the canvas sits flush against the navbar
     on top and the viewport edge on the bottom. -->
<div class="-my-6 flex h-[calc(100dvh-4.25rem)] flex-col">
	<header
		class="flex shrink-0 items-center justify-between border-b border-line bg-surface px-6 py-4"
	>
		<h1 class="font-serif text-2xl text-ink">{m.nav_stammbaum()}</h1>
	</header>

	{#if data.nodes.length === 0}
		<div class="flex flex-1 items-center justify-center p-8">
			<div
				class="mx-auto max-w-md rounded-sm border border-line bg-surface p-10 text-center shadow-sm"
			>
				<svg
					class="mx-auto mb-4 h-12 w-12 text-ink-3"
					viewBox="0 0 24 24"
					fill="none"
					stroke="currentColor"
					stroke-width="1.5"
					aria-hidden="true"
				>
					<circle cx="12" cy="5" r="2.5" />
					<circle cx="6" cy="14" r="2.5" />
					<circle cx="18" cy="14" r="2.5" />
					<path stroke-linecap="round" d="M12 7.5v3M9.5 12.5L9 14M14.5 12.5l.5 1.5" />
				</svg>
				<h2 class="mb-2 font-serif text-xl text-ink">{m.stammbaum_empty_heading()}</h2>
				<p class="mb-4 font-serif text-sm text-ink-2">{m.stammbaum_empty_body()}</p>
				<a
					href="/persons"
					class="inline-block font-sans text-sm font-medium text-primary hover:underline"
				>
					{m.stammbaum_empty_link()}
				</a>
			</div>
		</div>
	{:else}
		<div class="flex flex-1 overflow-hidden">
			<div class="relative flex-1 overflow-hidden bg-muted/20">
				<StammbaumTree
					nodes={data.nodes}
					edges={data.edges}
					selectedId={selectedId}
					panZoom={view}
					centreOnId={centreOnId}
					centreBiasFraction={isMobile ? MOBILE_CENTRE_BIAS : 0}
					anchorTopLeft={!page.url.searchParams.has('z')}
					onPanZoom={(v) => (view = v)}
					onActivity={() => (canvasActivity = true)}
					onSelect={selectPerson}
				/>
				<StammbaumAffordance dismissed={canvasActivity} />
				<StammbaumControls onZoomIn={zoomIn} onZoomOut={zoomOut} onFit={fitToScreen} />
			</div>
			{#if selectedNode}
				<!-- Desktop: side panel on the right -->
				<aside
					class="hidden w-[320px] shrink-0 overflow-y-auto border-l border-line bg-surface md:block"
				>
					<StammbaumSidePanel
						node={selectedNode}
						canWrite={canWrite}
						onClose={() => (selectedId = null)}
						onCentre={centreOnSelected}
					/>
				</aside>
				<!-- Mobile: dismissible bottom sheet (overlay, preserves pan/zoom) -->
				<StammbaumBottomSheet
					node={selectedNode}
					canWrite={canWrite}
					onClose={() => (selectedId = null)}
					onCentre={centreOnSelected}
				/>
			{/if}
		</div>
	{/if}
</div>
