<script lang="ts">
    import { onMount, onDestroy, getContext } from "svelte"
    import type { HTMLInputAttributes } from "svelte/elements"
    import { FORM_CONTEXT, type FormContext, type ValidatableField } from "../lib/formContext"

    let {
        value = $bindable(""),
        options = [] as { value: string; label: string; searchText?: string }[],
        name = "",
        required = false,
        id = "",
        label = "",
        hideLabel = true,
        error = $bindable(""),
        disabled = false,
        placeholder = "",
        messages = {},
        validator,
        ...rest
    }: {
        value?: string
        options?: { value: string; label: string; searchText?: string }[]
        name?: string
        required?: boolean
        id?: string
        label?: string
        hideLabel?: boolean
        error?: string
        disabled?: boolean
        placeholder?: string
        messages?: Partial<Record<"valueMissing", string>>
        validator?: (value: string) => Promise<string | null | undefined> | string | null | undefined
    } & HTMLInputAttributes = $props()

    const fallbackId = `searchable-select-${Math.random().toString(36).substr(2, 9)}`
    const inputId = $derived(id || fallbackId)
    const listboxId = $derived(`${inputId}-listbox`)
    const errorId = $derived(`${inputId}-error`)

    let inputElement = $state<HTMLInputElement>()
    let inputValue = $state("")
    let isOpen = $state(false)
    let highlightedIndex = $state(-1)
    let hasTypedSinceOpen = $state(false)

    const formContext = getContext<FormContext>(FORM_CONTEXT)

    let selectedOption = $derived.by(() => options.find((option) => option.value === value) || null)

    let filteredOptions = $derived.by(() => {
        const query = isOpen && !hasTypedSinceOpen ? "" : inputValue.trim().toLowerCase()
        if (!query) {
            return options
        }
        return options.filter((option) => {
            const searchable = `${option.label} ${option.searchText || ""}`.toLowerCase()
            return searchable.includes(query)
        })
    })

    $effect(() => {
        if (!isOpen) {
            inputValue = selectedOption?.label || ""
        }
    })

    export const validate = async () => {
        if (required && !value) {
            error = messages.valueMissing || "Please select an option"
            return false
        }

        if (value && !options.some((option) => option.value === value)) {
            error = messages.valueMissing || "Please select a valid option"
            return false
        }

        if (validator) {
            const customError = await validator(value)
            if (customError) {
                error = customError
                return false
            }
        }

        error = ""
        return true
    }

    const reset = () => {
        error = ""
    }

    const focus = () => {
        inputElement?.focus()
    }

    const field: ValidatableField = { validate, reset, focus }

    onMount(() => {
        if (formContext) {
            formContext.register(field)
        }
    })

    onDestroy(() => {
        if (formContext) {
            formContext.unregister(field)
        }
    })

    function openDropdown() {
        if (disabled) return

        if (!isOpen) {
            hasTypedSinceOpen = false
        }

        isOpen = true
        inputElement?.focus()

        const cursorPosition = inputValue.length
        setTimeout(() => {
            inputElement?.setSelectionRange(cursorPosition, cursorPosition)
        }, 0)

        highlightedIndex = filteredOptions.length > 0 ? 0 : -1
    }

    function closeDropdown() {
        isOpen = false
        highlightedIndex = -1
        hasTypedSinceOpen = false
    }

    function selectOption(option: { value: string; label: string; searchText?: string }) {
        value = option.value
        inputValue = option.label
        closeDropdown()
        validate()
    }

    function handleInput(event: Event) {
        const target = event.target as HTMLInputElement
        inputValue = target.value
        openDropdown()
        hasTypedSinceOpen = true

        const normalizedInput = inputValue.trim().toLowerCase()
        const exactMatch = options.find((option) => {
            const labelMatch = option.label.toLowerCase() === normalizedInput
            const searchTextMatch = option.searchText
                ?.toLowerCase()
                .split("|")
                .map((entry) => entry.trim())
                .includes(normalizedInput)
            return labelMatch || !!searchTextMatch
        })
        value = exactMatch?.value || ""

        validate()
    }

    function handleBlur() {
        setTimeout(() => {
            const normalizedInput = inputValue.trim().toLowerCase()
            const exactMatch = options.find((option) => {
                const labelMatch = option.label.toLowerCase() === normalizedInput
                const searchTextMatch = option.searchText
                    ?.toLowerCase()
                    .split("|")
                    .map((entry) => entry.trim())
                    .includes(normalizedInput)
                return labelMatch || !!searchTextMatch
            })
            if (exactMatch) {
                value = exactMatch.value
                inputValue = exactMatch.label
            } else if (selectedOption) {
                inputValue = selectedOption.label
            }
            closeDropdown()
            validate()
        }, 120)
    }

    function handleKeyDown(event: KeyboardEvent) {
        if (disabled) return

        if (event.key === "ArrowDown") {
            event.preventDefault()
            if (!isOpen) {
                openDropdown()
                return
            }
            if (filteredOptions.length > 0) {
                highlightedIndex = Math.min(highlightedIndex + 1, filteredOptions.length - 1)
            }
            return
        }

        if (event.key === "ArrowUp") {
            event.preventDefault()
            if (!isOpen) {
                openDropdown()
                return
            }
            if (filteredOptions.length > 0) {
                highlightedIndex = Math.max(highlightedIndex - 1, 0)
            }
            return
        }

        if (event.key === "Enter" && isOpen && highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
            event.preventDefault()
            selectOption(filteredOptions[highlightedIndex])
            return
        }

        if (event.key === "Escape") {
            closeDropdown()
        }
    }
</script>

<div class="w-full">
    {#if label}
        <label
            for={inputId}
            class="block text-base mb-1 {hideLabel ? 'sr-only' : ''} {error ? 'text-red-600' : 'text-gray-900'}"
        >
            {label}
            {#if required}<span class="text-red-600">*</span>{/if}
        </label>
    {/if}

    <div class="relative w-full">
        <input
            bind:this={inputElement}
            id={inputId}
            type="text"
            {name}
            value={inputValue}
            {disabled}
            {placeholder}
            autocomplete="off"
            role="combobox"
            aria-expanded={isOpen}
            aria-controls={isOpen ? listboxId : undefined}
            aria-autocomplete="list"
            aria-invalid={!!error}
            aria-describedby={error ? errorId : undefined}
            class="w-full border rounded-lg px-4 py-3 bg-white text-base transition-colors focus:outline-none focus:ring-1 focus:ring-blue-600 {error
                ? 'border-red-500 focus:border-red-500'
                : 'border-gray-300 hover:border-blue-600 focus:border-blue-600'} {disabled ? 'bg-gray-50' : 'bg-white'}"
            onfocus={openDropdown}
            oninput={handleInput}
            onblur={handleBlur}
            onkeydown={handleKeyDown}
            {...rest}
        />

        <button
            type="button"
            class="absolute right-4 top-1/2 -translate-y-1/2 cursor-pointer"
            aria-label="Open dropdown"
            tabindex="-1"
            onclick={() => {
                openDropdown()
            }}
        >
            <svg
                xmlns="http://www.w3.org/2000/svg"
                class="h-5 w-5 {error ? 'text-red-500' : 'text-gray-400'}"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
            >
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
            </svg>
        </button>

        {#if isOpen && filteredOptions.length > 0}
            <ul
                id={listboxId}
                role="listbox"
                class="absolute z-30 mt-1 w-full max-h-64 overflow-auto rounded-lg border border-gray-300 bg-white shadow-lg"
            >
                {#each filteredOptions as option, index}
                    <li
                        role="option"
                        aria-selected={option.value === value}
                        class="px-4 py-2 cursor-pointer text-sm {index === highlightedIndex
                            ? 'bg-blue-600/10 text-blue-600'
                            : 'text-gray-900 hover:bg-gray-50'}"
                        onmousedown={(event) => {
                            event.preventDefault()
                            selectOption(option)
                        }}
                    >
                        {option.label}
                    </li>
                {/each}
            </ul>
        {/if}
    </div>

    <div class="min-h-5.5 mt-1">
        {#if error}
            <p id={errorId} class="text-sm text-red-600">{error}</p>
        {/if}
    </div>
</div>
