<script lang="ts">
	import { tick } from 'svelte';
	import { Combobox as ComboboxPrimitive } from 'bits-ui';
	import { ChevronDown, X } from 'lucide-svelte';
	import { cn } from '$lib/utils.js';

	interface Item {
		value: string;
		label: string;
	}

	let {
		items = [],
		value = $bindable(''),
		onValueChange,
		placeholder = 'Seleccionar...',
		searchPlaceholder = 'Buscar...',
		emptyMessage = 'No se encontraron resultados',
		class: className = '',
		disabled = false
	}: {
		items: Item[];
		value?: string;
		onValueChange?: (value: string) => void;
		placeholder?: string;
		searchPlaceholder?: string;
		emptyMessage?: string;
		class?: string;
		disabled?: boolean;
	} = $props();

	let searchQuery = $state('');
	let open = $state(false);
	let isTyping = $state(false);
	let containerWidth = $state(0);
	let inputEl: HTMLInputElement | null = $state(null);

	// Texto mostrado cuando hay selección real; si value está vacío no usar la etiqueta del ítem
	// «Todos…» (también value ''), para que la X deje el campo vacío y se vea el placeholder.
	const selectedLabel = $derived(
		value ? ((items ?? []).find((item) => item.value === value)?.label ?? '') : ''
	);

	// Filter items based on search query
	// Show all items unless the user is explicitly typing
	const filteredItems = $derived(
		!isTyping || searchQuery.trim() === ''
			? (items ?? [])
			: (items ?? []).filter(
					(item) =>
						(item.label ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
						(item.value ?? '').toLowerCase().includes(searchQuery.toLowerCase())
				)
	);

	async function handleValueChange(v: string | string[]) {
		// bits-ui: sin type="single" el valor es array y rompe enlaces string (filtros, etc.)
		const next = Array.isArray(v) ? (v[0] ?? '') : (v ?? '');
		value = next;
		onValueChange?.(next);
		open = false;
		isTyping = false;
		await tick();
		inputEl?.focus();
	}

	function handleKeydown(e: KeyboardEvent) {
		if (!open) return;
		if (e.key === 'Tab') {
			// Close the dropdown and let Tab move focus naturally
			open = false;
			isTyping = false;
			return;
		}
		if (e.key === 'Enter') {
			// If bits-ui already has a highlighted item, let it handle selection
			const highlighted = document.querySelector('[data-highlighted]');
			if (highlighted) return;
			// No highlighted item — select the first visible item
			if (filteredItems.length > 0) {
				e.preventDefault();
				handleValueChange(filteredItems[0].value);
			}
		}
	}

	function handleClear(e: Event) {
		e.stopPropagation();
		value = '';
		onValueChange?.('');
		searchQuery = '';
		isTyping = false;
	}

	function handleInput() {
		isTyping = true;
	}

	function handleFocus() {
		if (!open) {
			open = true;
			isTyping = false; // Show all items on initial focus
		}
	}

	// Sincronizar el input al cerrar, al cambiar la selección desde fuera o al actualizar items
	$effect(() => {
		if (open || isTyping) return;
		searchQuery = selectedLabel;
	});
</script>

<ComboboxPrimitive.Root
		type="single"
		items={items}
		bind:value={value}
		bind:open
		{disabled}
		onValueChange={handleValueChange}
	>
	<div class="relative {className}" bind:clientWidth={containerWidth}>
		<div class="relative">
			<ComboboxPrimitive.Input
				bind:ref={inputEl}
				bind:value={searchQuery}
				clearOnDeselect={true}
				{placeholder}
				aria-label={placeholder}
				class={cn(
					'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 shadow-xs flex h-9 w-full items-center rounded-md border bg-transparent px-3 py-2 pr-24 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
					!value && !searchQuery && 'text-muted-foreground'
				)}
				oninput={handleInput}
				onfocus={handleFocus}
				onkeydown={handleKeydown}
			/>
			<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
				{#if value && !disabled}
					<button
						type="button"
						tabindex="-1"
						onclick={handleClear}
						aria-label="Limpiar selección"
						class="hover:bg-accent focus-visible:bg-accent rounded-sm p-1 transition-colors outline-none"
					>
						<X class="size-4" />
					</button>
				{/if}
				<ComboboxPrimitive.Trigger
					tabindex="-1"
					aria-label="Abrir opciones"
					class="hover:bg-accent focus-visible:bg-accent rounded-sm p-1 transition-colors outline-none"
				>
					<ChevronDown class="size-4 opacity-50" />
				</ComboboxPrimitive.Trigger>
			</div>
		</div>

		<ComboboxPrimitive.Portal>
			<ComboboxPrimitive.Content
				class={cn(
					'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[300px] overflow-y-auto rounded-md border p-1 shadow-md outline-none'
				)}
				style="width: {containerWidth > 0 ? containerWidth + 'px' : '300px'}"
				sideOffset={4}
			>
				{#if filteredItems.length === 0}
					<div class="text-muted-foreground py-6 text-center text-sm italic">
						{emptyMessage}
					</div>
				{:else}
					{#each filteredItems as item (`${item.value}\u0000${item.label}`)}
						<ComboboxPrimitive.Item
							value={item.value}
							label={item.label}
							onpointerdowncapture={(e) => {
								// No tratar value '' (p. ej. «Todos los módulos») como «reclic para deseleccionar»:
								// si no, al volver a elegir Todos con el filtro ya vacío se cancela el clic.
								if (item.value !== '' && item.value === value) {
									e.preventDefault();
									e.stopImmediatePropagation();
									handleClear(e);
									open = false;
								}
							}}
							class={cn(
								'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
							)}
						>
							<span class="flex-1 truncate">{item.label}</span>
						</ComboboxPrimitive.Item>
					{/each}
				{/if}
			</ComboboxPrimitive.Content>
		</ComboboxPrimitive.Portal>
	</div>
</ComboboxPrimitive.Root>
