<script lang="ts">
import { onDestroy, tick } from 'svelte';
import { detectPersonMention } from '$lib/utils/personMention';
import { formatLifeDateRange } from '$lib/utils/personLifeDates';
import { m } from '$lib/paraglide/messages.js';
import type { components } from '$lib/generated/api';

type Person = components['schemas']['Person'];
type PersonMention = components['schemas']['PersonMention'];

type Props = {
	value: string;
	mentionedPersons: PersonMention[];
	placeholder?: string;
	rows?: number;
	disabled?: boolean;
	onfocus?: () => void;
	onblur?: () => void;
	// Optional escape hatch: lets the parent observe the underlying textarea node
	// (e.g. to read selection bounds for quote-selection features). Returning a
	// cleanup function from the parent is not required.
	captureTextarea?: (node: HTMLTextAreaElement) => void | (() => void);
};

let {
	value = $bindable(''),
	mentionedPersons = $bindable([]),
	placeholder = '',
	rows = 1,
	disabled = false,
	onfocus,
	onblur,
	captureTextarea
}: Props = $props();

let query: string | null = $state(null);
let results: Person[] = $state([]);
let highlightedIndex = $state(0);
let mentionStart = $state(0);

let textarea: HTMLTextAreaElement | null = null;
let debounceTimer: ReturnType<typeof setTimeout> | undefined;

function attachTextarea(node: HTMLTextAreaElement) {
	textarea = node;
	const parentCleanup = captureTextarea?.(node);
	return () => {
		parentCleanup?.();
		textarea = null;
	};
}

function handleInput() {
	if (!textarea) return;
	const cursorPos = textarea.selectionStart;
	const detected = detectPersonMention(value, cursorPos);

	if (detected === null) {
		closePopup();
		return;
	}

	const before = value.slice(0, cursorPos);
	mentionStart = before.lastIndexOf('@');

	if (query !== detected) {
		query = detected;
		highlightedIndex = 0;
		scheduleSearch(detected);
	}
}

function scheduleSearch(q: string) {
	clearTimeout(debounceTimer);
	if (!q.trim()) {
		// Empty query: keep popup open with last results so the user can browse,
		// but don't fire a backend call until they actually type something.
		results = [];
		return;
	}
	debounceTimer = setTimeout(async () => {
		try {
			const res = await fetch(`/api/persons?q=${encodeURIComponent(q)}`);
			if (res.ok) {
				const data: Person[] = await res.json();
				results = data.slice(0, 5);
			} else {
				results = [];
			}
		} catch {
			results = [];
		}
	}, 200);
}

async function selectPerson(person: Person) {
	if (!textarea) return;

	const displayName = person.displayName ?? '';
	const replacement = `@${displayName} `;
	const cursorPos = textarea.selectionStart;
	const before = value.slice(0, mentionStart);
	const after = value.slice(cursorPos);
	value = before + replacement + after;

	if (!mentionedPersons.some((existing) => existing.personId === person.id)) {
		mentionedPersons = [...mentionedPersons, { personId: person.id!, displayName }];
	}

	closePopup();

	await tick();
	if (!textarea) return;
	const pos = mentionStart + replacement.length;
	textarea.selectionStart = pos;
	textarea.selectionEnd = pos;
	textarea.focus();
}

function closePopup() {
	query = null;
	results = [];
	highlightedIndex = 0;
	clearTimeout(debounceTimer);
}

function handleKeydown(e: KeyboardEvent) {
	if (query === null) return;

	if (e.key === 'Escape') {
		e.preventDefault();
		closePopup();
		return;
	}

	if (e.key === 'ArrowDown') {
		e.preventDefault();
		if (results.length > 0) {
			highlightedIndex = (highlightedIndex + 1) % results.length;
		}
		return;
	}

	if (e.key === 'ArrowUp') {
		e.preventDefault();
		if (results.length > 0) {
			highlightedIndex = (highlightedIndex - 1 + results.length) % results.length;
		}
		return;
	}

	if (e.key === 'Enter' && results.length > 0) {
		e.preventDefault();
		selectPerson(results[highlightedIndex]);
		return;
	}
}

onDestroy(() => clearTimeout(debounceTimer));

const popupOpen = $derived(query !== null);
</script>

<div class="relative">
	<textarea
		{@attach attachTextarea}
		class="w-full resize-none border-none bg-transparent font-serif text-base leading-relaxed text-ink outline-none placeholder:text-ink-3"
		rows={rows}
		placeholder={placeholder}
		disabled={disabled}
		bind:value={value}
		oninput={handleInput}
		onkeydown={handleKeydown}
		onfocus={onfocus}
		onblur={onblur}
	></textarea>

	{#if popupOpen}
		<div
			class="absolute z-20 mt-1 w-72 overflow-hidden rounded-sm border border-line bg-surface shadow-lg"
			role="listbox"
			aria-label={m.person_mention_btn_label()}
		>
			{#if results.length === 0}
				<p class="px-3 py-2 font-sans text-sm text-ink-3">{m.person_mention_popup_empty()}</p>
			{:else}
				{#each results as person, i (person.id)}
					<div
						class={[
							'flex min-h-[44px] cursor-pointer flex-col gap-1 px-3 py-2.5 text-left hover:bg-canvas',
							i === highlightedIndex && 'bg-canvas'
						]}
						role="option"
						aria-selected={i === highlightedIndex}
						data-test-person-id={person.id}
						tabindex="-1"
						onmousedown={(e) => {
							e.preventDefault();
							selectPerson(person);
						}}
					>
						<span class="truncate font-serif text-base text-ink">{person.displayName}</span>
						{#if formatLifeDateRange(person.birthYear, person.deathYear)}
							<span class="truncate font-sans text-xs text-ink-3">
								{formatLifeDateRange(person.birthYear, person.deathYear)}
							</span>
						{/if}
					</div>
				{/each}
			{/if}
		</div>
	{/if}
</div>
