<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ChevronDown, Check } from 'lucide-vue-next'
import { computeDropdownPosition } from '../utils/dropdown'

interface Props {
  modelValue: any
  options: any[]
  labelKey?: string
  valueKey?: string
  placeholder?: string
  disabled?: boolean
  placeholderValue?: any
  searchable?: boolean
  ariaLabelledby?: string
}

const props = withDefaults(defineProps<Props>(), {
  labelKey: 'name',
  valueKey: 'id',
  placeholder: 'Select...',
  disabled: false,
  placeholderValue: undefined,
  searchable: false,
})

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

const listboxId = 'appselect-lb-' + Math.random().toString(36).slice(2, 9)
const isOpen = ref(false)
const highlightedIndex = ref(-1)
const triggerRef = ref<HTMLButtonElement | null>(null)
const panelRef = ref<HTMLDivElement | null>(null)
const panelStyle = ref<Record<string, string>>({})
const searchQuery = ref('')
const searchInputRef = ref<HTMLInputElement | null>(null)

// Build the full list: placeholder + real options
const allItems = computed(() => {
  const placeholderItem = {
    _isPlaceholder: true,
    [props.valueKey]: props.placeholderValue,
    [props.labelKey]: props.placeholder,
  }
  return [placeholderItem, ...props.options]
})

const filteredItems = computed(() => {
  if (!props.searchable || !searchQuery.value) return allItems.value
  const q = searchQuery.value.toLowerCase()
  return allItems.value.filter(item => {
    if (item._isPlaceholder) return true
    return getOptionLabel(item).toLowerCase().includes(q)
  })
})

const selectedLabel = computed(() => {
  const option = props.options.find(
    (o) => o[props.valueKey] === props.modelValue
  )
  return option ? option[props.labelKey] : null
})

const isPlaceholderSelected = computed(() => {
  return selectedLabel.value === null
})

function getOptionValue(item: any): any {
  return item._isPlaceholder ? props.placeholderValue : item[props.valueKey]
}

function getOptionLabel(item: any): string {
  return item[props.labelKey]
}

function isSelected(item: any): boolean {
  const val = getOptionValue(item)
  return val === props.modelValue
}

function updatePosition() {
  if (!triggerRef.value) return
  panelStyle.value = computeDropdownPosition(triggerRef.value, {
    estimatedHeight: 280,
    panelEl: panelRef.value,
  })
}

function toggle() {
  if (props.disabled) return
  if (isOpen.value) {
    close()
  } else {
    open()
  }
}

function open() {
  if (props.disabled) return
  isOpen.value = true
  updatePosition()

  // Set highlighted index to the currently selected item
  const selectedIdx = allItems.value.findIndex((item) => isSelected(item))
  highlightedIndex.value = selectedIdx >= 0 ? selectedIdx : 0

  if (props.searchable) {
    searchQuery.value = ''
    nextTick(() => searchInputRef.value?.focus())
  }

  nextTick(() => {
    scrollHighlightedIntoView()
    // Reposition with actual panel height (fixes above-flip offset)
    updatePosition()
  })

  document.addEventListener('click', onClickOutside, true)
  document.addEventListener('scroll', onScrollOrResize, true)
  window.addEventListener('resize', onScrollOrResize)
}

function close() {
  isOpen.value = false
  highlightedIndex.value = -1
  document.removeEventListener('click', onClickOutside, true)
  document.removeEventListener('scroll', onScrollOrResize, true)
  window.removeEventListener('resize', onScrollOrResize)
}

function select(item: any) {
  emit('update:modelValue', getOptionValue(item))
  close()
}

function onClickOutside(e: MouseEvent) {
  const target = e.target as Node
  if (
    triggerRef.value?.contains(target) ||
    panelRef.value?.contains(target)
  ) {
    return
  }
  close()
}

function onScrollOrResize() {
  if (isOpen.value) {
    updatePosition()
  }
}

function scrollHighlightedIntoView() {
  if (!panelRef.value) return
  const items = panelRef.value.querySelectorAll('[data-option]')
  const item = items[highlightedIndex.value] as HTMLElement | undefined
  if (item) {
    item.scrollIntoView({ block: 'nearest' })
  }
}

function onKeydown(e: KeyboardEvent) {
  if (!isOpen.value) {
    if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      open()
      return
    }
    return
  }

  switch (e.key) {
    case 'ArrowDown':
      e.preventDefault()
      highlightedIndex.value = Math.min(
        highlightedIndex.value + 1,
        filteredItems.value.length - 1
      )
      nextTick(() => scrollHighlightedIntoView())
      break
    case 'ArrowUp':
      e.preventDefault()
      highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0)
      nextTick(() => scrollHighlightedIntoView())
      break
    case 'Enter':
    case ' ':
      e.preventDefault()
      if (highlightedIndex.value >= 0) {
        select(filteredItems.value[highlightedIndex.value])
      }
      break
    case 'Escape':
      e.preventDefault()
      close()
      triggerRef.value?.focus()
      break
    case 'Tab':
      close()
      break
  }
}

function onSearchKeydown(e: KeyboardEvent) {
  if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key)) {
    onKeydown(e)
  }
}

onMounted(() => {
  // Nothing needed on mount since listeners are added when opened
})

onBeforeUnmount(() => {
  // Clean up all listeners in case component is destroyed while open
  document.removeEventListener('click', onClickOutside, true)
  document.removeEventListener('scroll', onScrollOrResize, true)
  window.removeEventListener('resize', onScrollOrResize)
})
</script>

<template>
  <div class="relative">
    <!-- Trigger button -->
    <button
      ref="triggerRef"
      type="button"
      role="combobox"
      :aria-expanded="isOpen"
      aria-haspopup="listbox"
      :aria-activedescendant="isOpen && highlightedIndex >= 0 ? 'appselect-option-' + highlightedIndex : undefined"
      :aria-labelledby="ariaLabelledby"
      :aria-controls="isOpen ? listboxId : undefined"
      :disabled="disabled"
      @click="toggle"
      @keydown="onKeydown"
      class="w-full flex items-center justify-between gap-2 px-3 py-2 bg-bg-inset border border-border-subtle rounded-xl text-[0.8125rem] text-left transition-colors"
      :class="{
        'opacity-40 cursor-not-allowed': disabled,
        'cursor-pointer': !disabled,
      }"
      :style="
        isOpen
          ? {
              borderColor: 'var(--color-accent)',
              boxShadow: '0 0 0 2px var(--color-accent-muted)',
              outline: 'none',
            }
          : {}
      "
    >
      <slot name="selected" :label="selectedLabel ?? placeholder" :is-placeholder="isPlaceholderSelected">
        <span
          :class="isPlaceholderSelected ? 'text-text-tertiary' : 'text-text-primary'"
          class="truncate"
        >
          {{ selectedLabel ?? placeholder }}
        </span>
      </slot>
      <ChevronDown
        aria-hidden="true"
        class="w-4 h-4 text-text-secondary shrink-0 transition-transform duration-200"
        :class="{ 'rotate-180': isOpen }"
        :stroke-width="2"
      />
    </button>

    <!-- Dropdown panel -->
    <Teleport to="#app">
      <Transition name="dropdown">
      <div
        v-if="isOpen"
        ref="panelRef"
        :style="panelStyle"
        class="bg-bg-surface border border-border-visible rounded-xl shadow-[0_4px_24px_rgba(0,0,0,0.4)] overflow-hidden"
      >
        <div v-if="searchable" class="px-2 pt-2 pb-1">
          <input
            ref="searchInputRef"
            v-model="searchQuery"
            type="text"
            aria-label="Search options"
            class="w-full px-2.5 py-1.5 bg-bg-inset border border-border-subtle rounded-lg text-[0.75rem] text-text-primary placeholder-text-tertiary focus:outline-none focus:border-border-visible"
            placeholder="Search..."
            @keydown="onSearchKeydown"
          />
        </div>
        <div class="max-h-[240px] overflow-y-auto py-1" role="listbox" :id="listboxId">
          <div
            v-for="(item, index) in filteredItems"
            :key="item._isPlaceholder ? '__placeholder__' : item[valueKey]"
            role="option"
            :id="'appselect-option-' + index"
            :aria-selected="isSelected(item)"
            data-option
            @click="select(item)"
            @mouseenter="highlightedIndex = index"
            class="flex items-center justify-between gap-2 px-3 py-2 text-[0.8125rem] cursor-pointer transition-colors"
            :class="{
              'bg-bg-elevated': highlightedIndex === index,
              'text-text-tertiary': item._isPlaceholder,
              'text-text-primary': !item._isPlaceholder,
            }"
          >
            <slot name="option" :item="item" :label="getOptionLabel(item)" :selected="isSelected(item)">
              <span class="truncate">{{ getOptionLabel(item) }}</span>
            </slot>
            <Check
              v-if="isSelected(item)"
              aria-hidden="true"
              class="w-4 h-4 text-accent shrink-0"
              :stroke-width="2"
            />
          </div>
        </div>
      </div>
      </Transition>
    </Teleport>
  </div>
</template>
