<template>
  <div ref="rootRef" class="relative">
    <button
      type="button"
      class="app-input flex items-center justify-between gap-3 text-left"
      :class="[
        { 'border-brand-300 ring-3 ring-brand-500/10 dark:border-brand-800': open },
        disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
      ]"
      :disabled="disabled"
      @click="toggle"
      @keydown.down.prevent="openDropdown"
      @keydown.enter.prevent="toggle"
      @keydown.esc.prevent="close"
    >
      <span class="min-w-0 truncate" :class="selectedLabel ? 'text-gray-800 dark:text-white/90' : 'text-gray-400 dark:text-white/30'">
        {{ selectedLabel || placeholder }}
      </span>
      <ChevronDownIcon class="h-5 w-5 shrink-0 text-gray-400 transition-transform" :class="{ 'rotate-180': open }" />
    </button>

    <div
      v-if="open"
      class="absolute left-0 right-0 top-full z-999 mt-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-900"
    >
      <div v-if="searchable" class="border-b border-gray-100 p-2 dark:border-gray-800">
        <input
          ref="searchRef"
          v-model.trim="keyword"
          class="h-10 w-full rounded-lg border border-gray-200 bg-gray-50 px-3 text-sm text-gray-800 outline-none transition placeholder:text-gray-400 focus:border-brand-300 focus:bg-white focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 dark:focus:bg-gray-900"
          :placeholder="searchPlaceholder"
          @keydown.down.prevent="focusNext"
          @keydown.up.prevent="focusPrevious"
          @keydown.enter.prevent="selectFocused"
          @keydown.esc.prevent="close"
        />
      </div>
      <div class="custom-scrollbar max-h-72 overflow-y-auto p-1">
        <button
          v-for="(item, index) in filteredOptions"
          :key="item.value || `empty-${index}`"
          type="button"
          class="flex min-h-10 w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors"
          :class="[
            item.value === modelValue
              ? 'bg-brand-50 text-brand-600 dark:bg-brand-500/15 dark:text-brand-300'
              : 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-white/[0.05]',
            index === focusedIndex ? 'bg-gray-50 dark:bg-white/[0.05]' : '',
          ]"
          @mouseenter="focusedIndex = index"
          @click="selectOption(item)"
        >
          <span class="min-w-0 truncate">{{ item.label }}</span>
          <CheckIcon v-if="item.value === modelValue" class="h-3.5 w-3.5 shrink-0 rounded-full bg-brand-500 p-0.5 text-white" />
        </button>
        <div v-if="!filteredOptions.length" class="px-3 py-6 text-center text-sm text-gray-500 dark:text-gray-400">
          暂无匹配选项
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { CheckIcon, ChevronDownIcon } from '@/icons'

type SelectOption = {
  label: string
  value: string
}

const props = withDefaults(
  defineProps<{
    modelValue: string
    options: SelectOption[]
    placeholder?: string
    searchPlaceholder?: string
    searchable?: boolean
    disabled?: boolean
  }>(),
  {
    placeholder: '请选择',
    searchPlaceholder: '搜索选项',
    searchable: true,
    disabled: false,
  },
)

const emit = defineEmits<{
  'update:modelValue': [value: string]
  change: []
}>()

const open = ref(false)
const keyword = ref('')
const focusedIndex = ref(0)
const rootRef = ref<HTMLElement | null>(null)
const searchRef = ref<HTMLInputElement | null>(null)

const selectedLabel = computed(() => props.options.find((item) => item.value === props.modelValue)?.label || '')
const filteredOptions = computed(() => {
  const value = keyword.value.trim().toLowerCase()
  if (!value) return props.options
  return props.options.filter((item) => item.label.toLowerCase().includes(value) || item.value.toLowerCase().includes(value))
})

watch(open, async (value) => {
  if (!value) return
  keyword.value = ''
  focusedIndex.value = Math.max(
    props.options.findIndex((item) => item.value === props.modelValue),
    0,
  )
  await nextTick()
  if (props.searchable) searchRef.value?.focus()
})

watch(filteredOptions, () => {
  if (focusedIndex.value >= filteredOptions.value.length) {
    focusedIndex.value = Math.max(filteredOptions.value.length - 1, 0)
  }
})

function toggle() {
  if (props.disabled) return
  open.value ? close() : openDropdown()
}

function openDropdown() {
  if (props.disabled) return
  open.value = true
}

function close() {
  open.value = false
}

function focusNext() {
  if (!filteredOptions.value.length) return
  focusedIndex.value = (focusedIndex.value + 1) % filteredOptions.value.length
}

function focusPrevious() {
  if (!filteredOptions.value.length) return
  focusedIndex.value = (focusedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length
}

function selectFocused() {
  const item = filteredOptions.value[focusedIndex.value]
  if (item) selectOption(item)
}

function selectOption(item: SelectOption) {
  emit('update:modelValue', item.value)
  emit('change')
  close()
}

function handleDocumentMouseDown(event: MouseEvent) {
  if (!rootRef.value?.contains(event.target as Node)) close()
}

document.addEventListener('mousedown', handleDocumentMouseDown)
onBeforeUnmount(() => {
  document.removeEventListener('mousedown', handleDocumentMouseDown)
})
</script>
