<script lang="ts">
import { untrack } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import { m } from '$lib/paraglide/messages.js';
import { clickOutside } from '$lib/shared/actions/clickOutside';

export type Tag = { id?: string; name: string; color?: string; parentId?: string };

type SuggestionEntry = { tag: Tag; depth: number; isDirectMatch: boolean };

interface Props {
	tags?: Tag[];
	allowCreation?: boolean;
	onTextInput?: (text: string) => void;
}

let { tags = $bindable([]), allowCreation = true, onTextInput }: Props = $props();

let inputVal = $state('');
let suggestions: Tag[] = $state([]);
let fetchedForQuery = $state('');
let activeIndex = $state(-1);
let showSuggestions = $state(false);

const orderedSuggestions = $derived.by((): SuggestionEntry[] => {
	// SvelteMap satisfies svelte/prefer-svelte-reactivity; reactivity comes from $derived.by() re-evaluation
	const byId = new SvelteMap(suggestions.filter((s) => s.id).map((s) => [s.id!, s]));
	const childrenOf = new SvelteMap<string, Tag[]>();
	const roots: Tag[] = [];

	for (const s of suggestions) {
		if (s.parentId && byId.has(s.parentId)) {
			const arr = childrenOf.get(s.parentId) ?? [];
			arr.push(s);
			childrenOf.set(s.parentId, arr);
		} else {
			roots.push(s);
		}
	}

	const query = fetchedForQuery;
	const result: SuggestionEntry[] = [];

	function dfs(tag: Tag, depth: number) {
		result.push({ tag, depth, isDirectMatch: tag.name.toLowerCase().includes(query) });
		for (const child of childrenOf.get(tag.id!) ?? []) {
			dfs(child, depth + 1);
		}
	}

	for (const root of roots) {
		dfs(root, 0);
	}

	return result;
});

async function fetchSuggestions(query: string) {
	if (query.length < 2) {
		suggestions = [];
		return;
	}
	try {
		const res = await fetch(`/api/tags?query=${encodeURIComponent(query)}`);
		if (res.ok) {
			const data: Tag[] = await res.json();
			const currentTags = untrack(() => tags);
			const currentNames = new Set(currentTags.map((t) => t.name));
			suggestions = data.filter((t) => !currentNames.has(t.name));
			// fetchedForQuery set after suggestions: $derived fires twice but second run is correct
			fetchedForQuery = query.toLowerCase();
			showSuggestions = true;
		}
	} catch (e) {
		console.error('Tag fetch error', e);
	}
}

function addTag(tag: Tag | string) {
	const newTag: Tag = typeof tag === 'string' ? { name: tag.trim() } : tag;
	if (!newTag.name) return;
	const currentTags = untrack(() => tags);
	if (currentTags.some((t) => t.name === newTag.name)) {
		inputVal = '';
		suggestions = [];
		showSuggestions = false;
		activeIndex = -1;
		onTextInput?.('');
		return;
	}
	tags = [...tags, newTag];
	inputVal = '';
	suggestions = [];
	showSuggestions = false;
	activeIndex = -1;
	onTextInput?.('');
}

function removeTag(index: number) {
	tags = tags.filter((_, i) => i !== index);
}

function handleKeydown(e: KeyboardEvent) {
	if (e.key === 'Enter') {
		e.preventDefault();
		if (activeIndex >= 0 && orderedSuggestions[activeIndex]) {
			addTag(orderedSuggestions[activeIndex].tag);
		} else if (allowCreation) {
			addTag(inputVal);
		}
	} else if (e.key === 'Backspace' && inputVal === '' && tags.length > 0) {
		removeTag(tags.length - 1);
	} else if (e.key === 'ArrowDown') {
		e.preventDefault();
		activeIndex = (activeIndex + 1) % orderedSuggestions.length;
	} else if (e.key === 'ArrowUp') {
		e.preventDefault();
		activeIndex = (activeIndex - 1 + orderedSuggestions.length) % orderedSuggestions.length;
	}
}
</script>

<div class="w-full" use:clickOutside onclickoutside={() => (showSuggestions = false)}>
	<!-- Tag Container -->
	<div
		class="flex min-h-[42px] flex-wrap gap-2 rounded border border-line bg-surface p-2 focus-within:border-ink focus-within:ring-1 focus-within:ring-ink"
	>
		<!-- Render Selected Tags -->
		{#each tags as tag, i (tag.id ?? tag.name)}
			<span class="flex items-center gap-1 rounded bg-muted px-2 py-1 text-sm font-medium text-ink">
				{#if tag.color}
					<span
						data-testid="tag-color-dot"
						data-color={tag.color}
						style="background-color: var(--c-tag-{tag.color})"
						class="inline-block h-2 w-2 flex-shrink-0 rounded-full"
					></span>
				{/if}
				{tag.name}
				<button
					type="button"
					onclick={() => removeTag(i)}
					aria-label={m.comp_taginput_remove()}
					class="text-ink/50 hover:text-red-500 focus:outline-none"
				>
					<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
						><path
							stroke-linecap="round"
							stroke-linejoin="round"
							stroke-width="2"
							d="M6 18L18 6M6 6l12 12"
						/></svg
					>
				</button>
			</span>
		{/each}

		<!-- Input Field -->
		<div class="relative min-w-[120px] flex-1">
			<input
				type="text"
				bind:value={inputVal}
				oninput={() => {
					fetchSuggestions(inputVal);
					onTextInput?.(inputVal);
				}}
				onkeydown={handleKeydown}
				onfocus={() => fetchSuggestions(inputVal)}
				placeholder={tags.length === 0
					? allowCreation
						? m.comp_taginput_placeholder_create()
						: m.comp_taginput_placeholder_filter()
					: ''}
				class="h-full w-full border-none bg-transparent p-1 text-sm outline-none focus:ring-0"
			/>

			<!-- Typeahead Dropdown -->
			{#if showSuggestions && orderedSuggestions.length > 0}
				<ul
					role="listbox"
					class="absolute top-full left-0 z-50 mt-1 max-h-48 w-full overflow-y-auto rounded border border-line bg-surface shadow-lg"
				>
					{#each orderedSuggestions as s, i (s.tag.id ?? s.tag.name)}
						<li
							role="option"
							aria-selected={i === activeIndex}
							tabindex="0"
							style="padding-left: {s.depth * 16 + 12}px"
							class="cursor-pointer py-3 pr-3 text-sm hover:bg-muted {i === activeIndex
								? 'bg-muted font-bold text-ink'
								: s.isDirectMatch
									? 'font-medium text-ink-2'
									: 'text-ink-3'}"
							onclick={() => addTag(s.tag)}
							onkeydown={(e) => e.key === 'Enter' && addTag(s.tag)}
						>
							<!-- › only on root context ancestors; depth>0 nodes rely on indentation instead -->
							{#if !s.isDirectMatch && s.depth === 0}
								<span class="mr-1" aria-hidden="true">›</span>
							{/if}
							{#if s.tag.color}
								<span
									style="background-color: var(--c-tag-{s.tag.color})"
									class="mr-1 inline-block h-2 w-2 flex-shrink-0 rounded-full {s.isDirectMatch
										? 'opacity-100'
										: 'opacity-50'}"
								></span>
							{/if}
							{s.tag.name}
						</li>
					{/each}
				</ul>
			{/if}
		</div>
	</div>
	{#if allowCreation}
		<p class="mt-1 text-xs text-ink-3">{m.comp_taginput_create_hint()}</p>
	{/if}
</div>
