<script lang="ts">
import { untrack } from 'svelte';
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
import { clickOutside } from '$lib/shared/actions/clickOutside';
import { createTypeahead } from '$lib/shared/hooks/useTypeahead.svelte';
import FieldLabelBadge from '$lib/shared/primitives/FieldLabelBadge.svelte';
type Person = components['schemas']['Person'];

interface Props {
	name: string;
	label: string;
	value?: string;
	initialName?: string;
	suggestedName?: string;
	placeholder?: string;
	compact?: boolean;
	large?: boolean;
	autofocus?: boolean;
	required?: boolean;
	restrictToCorrespondentsOf?: string;
	excludePersonId?: string;
	badge?: 'additive' | 'replace';
	resetKey?: number;
	onchange?: (value: string) => void;
	onfocused?: () => void;
}

let {
	name,
	label,
	value = $bindable(''),
	initialName = '',
	suggestedName = '',
	placeholder,
	compact = false,
	large = false,
	autofocus = false,
	required = false,
	restrictToCorrespondentsOf,
	excludePersonId,
	badge,
	resetKey = 0,
	onchange,
	onfocused
}: Props = $props();

// searchTerm must be both prop-derived AND locally writable (user typing), so $state +
// $effect is the correct pattern here — writable $derived is read-only and won't work.
let searchTerm = $state(initialName);

// Sync display text when initialName changes OR when resetKey increments (navigation reset).
// resetKey is incremented by the page on every SvelteKit navigation so that a manually-typed
// term that was never committed (no person selected) gets cleared even if initialName stays ''.
$effect(() => {
	void resetKey;
	searchTerm = initialName;
});

$effect(() => {
	const suggested = suggestedName;
	if (suggested && !untrack(() => value)) {
		searchTerm = suggested;
	}
});

const typeahead = createTypeahead<Person>({
	fetchUrl: async (term) => {
		const personId = restrictToCorrespondentsOf;
		const excludeId = excludePersonId;
		const filter = (results: Person[]) =>
			excludeId ? results.filter((p) => p.id !== excludeId) : results;
		if (personId) {
			const url =
				term.length >= 1
					? `/api/persons/${personId}/correspondents?q=${encodeURIComponent(term)}`
					: `/api/persons/${personId}/correspondents`;
			const res = await fetch(url);
			return res.ok ? filter(await res.json()) : [];
		}
		if (term.length < 1) return [];
		// review=true so the typeahead searches the whole directory (incl. provisional /
		// zero-document persons), not just the clean reader subset.
		const res = await fetch(`/api/persons?review=true&q=${encodeURIComponent(term)}`);
		if (!res.ok) return [];
		const body = await res.json();
		return filter(body.items ?? []);
	},
	debounceMs: 300
});

// Fixed-position dropdown state — escapes any CSS stacking context that would clip it.
let inputEl: HTMLInputElement;
let dropdownStyle = $state('');
let activeIndex = $state(-1);

// Stable id linking the input's aria-controls to the listbox element.
const listboxId = `${name}-listbox`;

const isOpen = $derived(typeahead.isOpen && (typeahead.results.length > 0 || typeahead.loading));

function updateDropdownPosition() {
	if (!inputEl) return;
	const rect = inputEl.getBoundingClientRect();
	dropdownStyle = `position:fixed;top:${rect.bottom + 4}px;left:${rect.left}px;width:${rect.width}px`;
}

function handleInput() {
	if (value && searchTerm !== initialName) {
		value = '';
		onchange?.('');
	}

	const term = untrack(() => searchTerm);
	typeahead.setQuery(term);
}

function handleFocus() {
	onfocused?.();
	updateDropdownPosition();
	if (restrictToCorrespondentsOf) {
		const personId = untrack(() => restrictToCorrespondentsOf)!;
		(async () => {
			try {
				const res = await fetch(`/api/persons/${personId}/correspondents`);
				const persons: Person[] = res.ok ? await res.json() : [];
				typeahead.openWith(persons);
			} catch (e) {
				console.error('Suche fehlgeschlagen', e);
				typeahead.openWith([]);
			}
		})();
	} else {
		typeahead.openWith(typeahead.results);
	}
}

function selectPerson(person: Person) {
	value = person.id!;
	searchTerm = person.displayName;
	typeahead.close();
	activeIndex = -1;
	onchange?.(person.id!);
}

function closeDropdown() {
	typeahead.close();
	activeIndex = -1;
}

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

	const results = typeahead.results;

	if (e.key === 'ArrowDown') {
		e.preventDefault();
		activeIndex = (activeIndex + 1) % results.length;
	} else if (e.key === 'ArrowUp') {
		e.preventDefault();
		activeIndex = (activeIndex - 1 + results.length) % results.length;
	} else if (e.key === 'Enter') {
		e.preventDefault();
		if (activeIndex >= 0 && results[activeIndex]) {
			selectPerson(results[activeIndex]);
		}
	} else if (e.key === 'Escape') {
		e.preventDefault();
		closeDropdown();
	}
}

// Keep dropdown position current when user scrolls or resizes.
// fixed positioning is intentional — it escapes any CSS stacking context (overflow, transform,
// shadow-sm + z-index combinations) that would clip an absolute-positioned dropdown.
</script>

<svelte:window onscroll={updateDropdownPosition} onresize={updateDropdownPosition} />

<div class="relative" use:clickOutside onclickoutside={closeDropdown}>
	<label
		for="{name}-search"
		class={compact
			? 'block text-xs font-bold tracking-wide text-ink-3 uppercase'
			: 'block text-sm font-medium text-ink-2'}
		>{label}{#if required}*{/if}{#if badge}<FieldLabelBadge variant={badge} />{/if}</label
	>

	<input type="hidden" name={name} bind:value={value} />

	<input
		bind:this={inputEl}
		type="text"
		id="{name}-search"
		role="combobox"
		autocomplete="off"
		autofocus={autofocus}
		bind:value={searchTerm}
		aria-expanded={isOpen}
		aria-haspopup="listbox"
		aria-controls={listboxId}
		aria-activedescendant={activeIndex >= 0 && typeahead.results[activeIndex]
			? `${listboxId}-option-${typeahead.results[activeIndex].id}`
			: undefined}
		oninput={handleInput}
		onfocus={handleFocus}
		onkeydown={handleKeydown}
		placeholder={placeholder ?? m.comp_typeahead_placeholder()}
		class={large
			? 'mt-2 block h-14 w-full rounded-md border border-line bg-surface px-4 text-base text-ink shadow-sm placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'
			: compact
				? 'mt-1 block h-9 w-full rounded border border-line bg-surface px-2 text-sm text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'
				: 'mt-1 block w-full rounded border border-line bg-surface px-2 py-3 text-sm text-ink shadow-sm placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'}
	/>

	{#if isOpen}
		<ul
			id={listboxId}
			role="listbox"
			style={dropdownStyle}
			class="ring-opacity-5 z-50 max-h-60 overflow-auto rounded-md bg-surface py-1 text-base shadow-lg ring-1 ring-black focus:outline-none sm:text-sm"
		>
			{#if typeahead.loading}
				<li class="p-2 text-sm text-ink-2">{m.comp_typeahead_loading()}</li>
			{:else}
				{#each typeahead.results as person, i (person.id)}
					<li
						id="{listboxId}-option-{person.id}"
						role="option"
						aria-selected={i === activeIndex}
						class="relative cursor-pointer py-2 pr-9 pl-3 text-ink select-none hover:bg-accent-bg {i === activeIndex ? 'bg-accent-bg' : ''}"
						onclick={() => selectPerson(person)}
						onkeydown={(e) => e.key === 'Enter' && selectPerson(person)}
						tabindex="-1"
					>
						<div class="flex items-center">
							<span class="block truncate font-medium">
								{person.displayName}
							</span>
						</div>
					</li>
				{/each}
			{/if}
		</ul>
	{/if}
</div>
