<script lang="ts">
import { onDestroy, tick } from 'svelte';
import { detectMention } from '$lib/utils/mention';
import type { MentionDTO } from '$lib/types';
import { m } from '$lib/paraglide/messages.js';

type Props = {
	value: string;
	mentionCandidates: MentionDTO[];
	placeholder?: string;
	rows?: number;
	disabled?: boolean;
	onsubmit?: () => void;
};

let {
	value = $bindable(''),
	mentionCandidates = $bindable([]),
	placeholder = '',
	rows = 3,
	disabled = false,
	onsubmit
}: Props = $props();

let query: string | null = $state(null);
let results: MentionDTO[] = $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;
	return () => {
		textarea = null;
	};
}

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

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

	// Calculate where the @ starts
	const before = value.slice(0, cursorPos);
	const atIndex = before.lastIndexOf('@');
	mentionStart = atIndex;

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

function scheduleSearch(q: string) {
	clearTimeout(debounceTimer);
	if (!q) {
		results = [];
		return;
	}
	debounceTimer = setTimeout(async () => {
		try {
			const res = await fetch(`/api/users/search?q=${encodeURIComponent(q)}`);
			if (res.ok) {
				const data: MentionDTO[] = await res.json();
				results = data.slice(0, 5);
			} else {
				results = [];
			}
		} catch {
			results = [];
		}
	}, 200);
}

async function selectUser(user: MentionDTO) {
	if (!textarea) return;

	const displayName = `${user.firstName} ${user.lastName}`;
	// Replace @partialQuery with @FirstName LastName (plus trailing space)
	const replacement = `@${displayName} `;
	const cursorPos = textarea.selectionStart;
	const before = value.slice(0, mentionStart);
	const after = value.slice(cursorPos);
	value = before + replacement + after;

	// Deduplicate and add to candidates
	if (!mentionCandidates.some((c) => c.id === user.id)) {
		mentionCandidates = [...mentionCandidates, user];
	}

	closePopup();

	// Reposition cursor after the inserted mention
	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 (e.ctrlKey && e.key === 'Enter') {
		e.preventDefault();
		onsubmit?.();
		return;
	}

	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();
		selectUser(results[highlightedIndex]);
		return;
	}
}

async function handleAtButtonClick() {
	if (!textarea) return;
	const pos = textarea.selectionStart;
	const before = value.slice(0, pos);
	const after = value.slice(pos);
	// Ensure @ is preceded by whitespace or is at the start
	const needsSpace = before.length > 0 && !/\s$/.test(before);
	const insertion = needsSpace ? ' @' : '@';
	value = before + insertion + after;

	await tick();
	if (!textarea) return;
	const newPos = pos + insertion.length;
	textarea.selectionStart = newPos;
	textarea.selectionEnd = newPos;
	textarea.focus();

	// Trigger mention detection after inserting @
	const detected = detectMention(value, newPos);
	if (detected !== null) {
		mentionStart = newPos - 1;
		query = detected;
		highlightedIndex = 0;
		scheduleSearch(detected);
	}
}

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

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

<div class="relative">
	<textarea
		{@attach attachTextarea}
		class="w-full resize-none rounded border border-line px-3 py-2 font-serif text-sm text-ink focus:ring-1 focus:ring-accent focus:outline-none"
		rows={rows}
		placeholder={placeholder}
		disabled={disabled}
		bind:value={value}
		oninput={handleInput}
		onkeydown={handleKeydown}
	></textarea>

	{#if popupOpen}
		<div
			class="absolute z-20 mt-1 w-64 overflow-hidden rounded-sm border border-line bg-surface shadow-lg"
			role="listbox"
			aria-label={m.mention_btn_label()}
		>
			{#if results.length === 0}
				<p class="px-3 py-2 font-sans text-sm text-ink-3">{m.mention_popup_empty()}</p>
			{:else}
				{#each results as user, i (user.id)}
					<div
						class="w-full px-3 py-2 text-left font-sans text-sm text-ink hover:bg-canvas {i === highlightedIndex ? 'bg-canvas' : ''}"
						role="option"
						aria-selected={i === highlightedIndex}
						tabindex="-1"
						onmousedown={(e) => {
							// Use mousedown to fire before textarea blur
							e.preventDefault();
							selectUser(user);
						}}
					>
						{user.firstName}
						{user.lastName}
					</div>
				{/each}
			{/if}
		</div>
	{/if}

	<button
		type="button"
		aria-label={m.mention_btn_label()}
		disabled={disabled}
		class="mt-1 rounded border border-line px-2 py-0.5 font-sans text-xs font-medium text-ink-3 transition-colors hover:border-ink hover:text-ink disabled:opacity-40"
		onclick={handleAtButtonClick}
	>
		@
	</button>
</div>
