<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useBoldQuery } from '../../composables/useBoldQuery'
import {
  formatPidWithDash,
  normalizePidSearchValue,
  PID_MASK,
  PID_MASK_OPTIONS,
  useIsPid,
} from '../../composables/useIsPid'
import { debounce } from '../../helpers/debounce'
import FdsPagination from '../FdsPagination/FdsPagination.vue'
import FdsSpinner from '../FdsSpinner/FdsSpinner.vue'
import FdsInput from '../Form/FdsInput/FdsInput.vue'
import type { FdsSearchSelectProps } from './types'

const props = withDefaults(defineProps<FdsSearchSelectProps>(), {
  items: () => [],
  page: undefined,
  totalPages: undefined,
  totalCount: undefined,
  loading: false,
  searchFields: () => [],
  preserveOrder: false,
  initialValue: '',
  disabled: false,
  dropdownAbsolute: false,
  labelLeft: false,
  label: '',
  meta: undefined,
  singleItemLabel: '',
  searchContext: () => ({ context: '', linkWord: '' }),
  valid: undefined,
  invalidMessage: '',
  noResultPrompt: '',
  borderless: false,
  marginless: false,
  maxListHeight: undefined,
  clearTrigger: false,
  dataTestid: undefined,
})

const searchFields = computed(() => props.searchFields ?? [])

const emit = defineEmits<{
  (e: 'searchSelected', value: Record<string, unknown> | null): void
  (e: 'paginate', value: number): void
  (e: 'total', value: number): void
  (e: 'change', value: string): void
  (e: 'input', ev: Event): void
  (e: 'clearInput', ev: Event): void
}>()

const dropdownVisible = ref(false)
const singleItemName = ref('')
const searchTerm = ref('')
const matchingItems = ref<Array<Record<string, unknown>>>([])
const displayedItems = ref<Array<Record<string, unknown>>>([])
const selectedItem = ref<Record<string, unknown> | null>(null)
const valid = ref<boolean | null>(props.valid ?? null)

// Use prop.totalCount if provided, otherwise calculate from items.length
const totalCount = computed(() => {
  if (props.totalCount !== undefined) {
    return props.totalCount
  }
  return props.items?.length || 0
})

watch(
  () => props.valid,
  (newValue) => {
    valid.value = newValue ?? null
  },
)

const componentRef = ref<HTMLElement | null>(null)
const dropdownRef = ref<HTMLElement | null>(null)
const inputComponentRef = ref<InstanceType<typeof FdsInput> | null>(null)
const inputId = `searchSelectInput-${Math.random().toString(36).slice(2, 9)}`
const listboxId = `select-dropdown-${Math.random().toString(36).slice(2, 9)}`

// Helper function to get the actual input element
const getInputElement = (): HTMLInputElement | null => {
  // Try to get from component ref first
  if (inputComponentRef.value?.$el) {
    const input = inputComponentRef.value.$el.querySelector('input') as HTMLInputElement
    if (input) return input
  }
  // Fallback to getElementById
  return document.getElementById(inputId) as HTMLInputElement | null
}

// Helper functions
const focusInput = () => {
  nextTick(() => {
    const input = getInputElement()
    input?.focus()
  })
}

const handleBlurInput = () => {
  const input = getInputElement()
  input?.blur()
}

const handleBlurAndClear = () => {
  const input = getInputElement()
  input?.blur()
  searchTerm.value = ''
  selectedItem.value = null
  valid.value = null
}

const uncheckAllRadios = () => {
  if (!dropdownRef.value) return
  const checkedRadio = dropdownRef.value.querySelector(
    'input[name="search-select-radio-group"]:checked',
  ) as HTMLInputElement
  if (checkedRadio) {
    checkedRadio.checked = false
  }
}

const focusFirstRadio = () => {
  nextTick(() => {
    const firstRadioButton = document.getElementById(`search-select-radio-0`) as HTMLInputElement
    if (firstRadioButton) {
      firstRadioButton.focus()
    }
  })
}

const hasInputValue = computed(() => searchTerm.value.length > 0)

const listWrapperClasses = computed(() => [
  'rounded-md box-border overflow-hidden z-4 w-full bg-white mt-2',
  props.marginless ? 'mb-0' : 'mb-8',
  props.borderless ? 'shadow-lg border-none' : 'border border-gray-200',
  props.dropdownAbsolute ? 'absolute right-0 z-50' : 'relative',
  props.maxListHeight && 'overflow-y-scroll',
])

const listItemClasses = computed(() => [
  'block m-0',
  'outline-none! hover:bg-blue_t-100 active:bg-blue_t-200',
  'border-b border-blue_t-200',
])

const listItemWrapperClasses = computed(() => [
  'block m-0 hover:outline-none',
  'has-focus:outline-2 has-focus:outline-blue-500 has-focus:-outline-offset-2 has-focus:outline-dashed',
])

const labelClasses = computed(() => 'block p-4 text-gray-700 cursor-pointer no-underline')

const headerClasses = computed(() => [
  'block m-0 font-light border-b border-gray-200 rounded-t-md',
  searchTerm.value && searchTerm.value.length ? 'p-6' : 'p-4',
])

const listWrapperStyle = computed(() => {
  if (!props.maxListHeight) return null

  // Convert to number in case it's passed as a string
  const maxHeight = typeof props.maxListHeight === 'string' ? parseInt(props.maxListHeight, 10) : props.maxListHeight

  return { maxHeight: `${maxHeight}px` }
})

const sortResponse = (response: Array<Record<string, unknown>>) => {
  if (!response.length) return []
  const firstItem = response[0] ?? {}
  const allKeys = [...searchFields.value, ...Object.keys(firstItem).filter((k) => !searchFields.value.includes(k))]
  return response.map((item) => {
    const sorted: Record<string, unknown> = {}
    allKeys.forEach((key) => {
      sorted[key] = item[key] || `${key}: den här uppgiften saknas.`
    })
    return sorted
  })
}

const matchesSearchTerm = (item: Record<string, unknown>): boolean => {
  if (!searchTerm.value) return true

  // When mask is active, use unmasked value (digits only) for searching
  // This ensures we can search even when user has partially typed a PID
  const searchValue = isPid.value ? normalizePidSearchValue(searchTerm.value) : searchTerm.value

  const searchLower = searchValue.toLowerCase()
  const pidRegex = /^\d{8}[a-zA-Z0-9]{4}$/

  // If search term matches PID pattern (unmasked), include all items
  if (pidRegex.test(searchValue)) return true

  // Check if any searchFields field contains the search term
  // For PID fields, also check unmasked version
  return searchFields.value.some((key) => {
    const value = item[key]
    if (!value) return false

    const stringValue = String(value)
    const valueLower = stringValue.toLowerCase()
    const unmaskedValue = isPid.value ? normalizePidSearchValue(stringValue) : stringValue.replace(/\D/g, '')

    // Check both masked and unmasked versions
    return (
      valueLower.includes(searchLower) ||
      (unmaskedValue.length > 0 && unmaskedValue.toLowerCase().includes(searchLower))
    )
  })
}

const filterAndPaginate = () => {
  if (!props.items?.length) {
    matchingItems.value = []
    displayedItems.value = []
    emit('total', 0)
    return
  }

  // Filter items based on search term
  let sourceData = props.items ?? []
  if (props.preserveOrder && sourceData.length) {
    sourceData = sortResponse(sourceData)
  }

  const matchedArray = sourceData.filter((item) => matchesSearchTerm(item))
  matchingItems.value = matchedArray

  // Never paginate internally - just show all filtered items
  // Parent component is responsible for pagination if needed
  displayedItems.value = matchedArray

  emit('total', matchingItems.value.length)
}

const handleClear = () => {
  searchTerm.value = ''
  selectedItem.value = null
  valid.value = null
  emit('searchSelected', null)
  emit('clearInput', new Event('clearInput'))
  filterAndPaginate()
  focusInput()
}

watch(
  () => props.clearTrigger,
  (value) => value && handleClear(),
)

watch(
  () => [props.items, props.page],
  () => {
    filterAndPaginate()
  },
  { immediate: true },
)

const debouncedEmitChange = debounce((value: string) => {
  const formattedValue = formatPidWithDash(value)
  emit('change', formattedValue)
}, 500)

const debouncedEmitInput = debounce((ev: Event) => {
  emit('input', ev)
}, 500)

watch(
  () => searchTerm.value,
  (newValue) => {
    filterAndPaginate()
    if (selectedItem.value === null) {
      debouncedEmitChange(newValue)
    }
  },
)

const onClickOutside = (e: PointerEvent) => {
  const target = e.target as Element
  if (componentRef.value?.contains(target)) return

  dropdownVisible.value = false
  if (props.initialValue === searchTerm.value && props.initialValue.length) {
    valid.value = true
  } else if (searchTerm.value.length > 0 && selectedItem.value === null) {
    valid.value = false
  }
}

const totalPages = computed(() => {
  // Only return totalPages if both page and totalPages props are provided
  if (props.page !== undefined && props.totalPages !== undefined) {
    return props.totalPages
  }
  return null
})

const handleInput = (e: Event) => {
  const target = e.target as HTMLInputElement
  const { value } = target

  searchTerm.value = value
  dropdownVisible.value = true
  selectedItem.value = null
  valid.value = null
  filterAndPaginate()
  debouncedEmitInput(e)
  uncheckAllRadios()
}

// Personnummer detection using composable
const { isPid } = useIsPid(searchTerm)

// Bold query matches using composable
const { boldQuery } = useBoldQuery(searchTerm)

// Mask configuration for personnummer (yyyymmdd-nnnn)
const pidMask = computed(() => (isPid.value ? PID_MASK : undefined))

const pidMaskOptions = computed(() => (isPid.value ? PID_MASK_OPTIONS : { lazy: true }))

const handleMatchingString = (item: Record<string, unknown>): string => {
  const values = searchFields.value.map((key) => String(item[key] || ''))

  let result = ''
  if (values.length === 1) {
    result = values[0] || ''
  } else {
    // Format PID if second value is 12 digits
    if (values[1] && /^\d{8}[a-zA-Z0-9]{4}$/.test(normalizePidSearchValue(values[1]))) {
      values[1] = formatPidWithDash(values[1])
    }

    if (props.preserveOrder) {
      // Combine first two values, then join remaining with <br>
      const combined = `${values[0]} (${values[1]})`
      const rest = values.slice(2).join('<br>')
      result = rest ? `${combined}<br>${rest}` : combined
    } else {
      result = values[0] || ''
      for (let i = 1; i < values.length; i++) {
        result += ` (${values[i]})`
      }
    }
  }

  if (searchTerm.value) {
    const formattedTerm = formatPidWithDash(searchTerm.value)
    const escaped = formattedTerm.replace(/[()]/g, '\\$&')
    if (new RegExp(escaped, 'i').test(result)) {
      return boldQuery(result)
    }
  }

  return result
}

const handlePagination = (_payload: { target: { id: string }; detail: number }) => {
  emit('paginate', _payload.detail)
}

const selectItem = (item: Record<string, unknown>) => {
  selectedItem.value = item
  valid.value = true
  emit('searchSelected', item)
}

const handleClick = (e: MouseEvent | KeyboardEvent, item: Record<string, unknown>) => {
  const keyEvent = e as KeyboardEvent
  if (keyEvent.key === 'Enter' || keyEvent.code === 'Space' || e.type === 'mouseup') {
    // Prevent event from bubbling and triggering other handlers
    e.preventDefault()
    e.stopPropagation()

    const firstField = searchFields.value[0]
    searchTerm.value = (firstField ? (item[firstField] as string) : '') || ''
    selectItem(item)
    dropdownVisible.value = false

    // Focus input after a short delay to prevent dropdown from reopening
    setTimeout(() => {
      focusInput()
    }, 100)
  }
}

const handleInputFocus = () => {
  // Only open dropdown if an item hasn't been selected and there are items to show
  if (!selectedItem.value && displayedItems.value.length > 0) {
    dropdownVisible.value = true
  }
}

const handleListKeyDown = (e: KeyboardEvent) => {
  // Handle Escape key to close dropdown
  if (e.key === 'Escape') {
    e.preventDefault()
    dropdownVisible.value = false
    uncheckAllRadios()
    focusInput()
  }
  // Arrow keys are handled natively by radio buttons
  // Enter/Space are handled by handleClick on the radio button change event
}

const handleInputKeyDown = (e: KeyboardEvent) => {
  // Handle ArrowDown to open dropdown and focus first radio button
  if (e.key === 'ArrowDown' && displayedItems.value.length > 0) {
    e.preventDefault()
    if (!dropdownVisible.value) {
      dropdownVisible.value = true
    }
    focusFirstRadio()
    return
  }

  // Handle Enter to open dropdown if closed
  if (e.key === 'Enter' && !dropdownVisible.value && displayedItems.value.length > 0) {
    e.preventDefault()
    dropdownVisible.value = true
    focusFirstRadio()
    return
  }

  // Handle Escape to close dropdown
  if (e.key === 'Escape' && dropdownVisible.value) {
    e.preventDefault()
    dropdownVisible.value = false
    uncheckAllRadios()
    focusInput()
  }
}

onMounted(() => {
  if (props.initialValue) {
    searchTerm.value = props.initialValue
    valid.value = true
  }

  window.addEventListener('mouseup', onClickOutside as EventListener)

  nextTick(() => {
    const input = getInputElement()
    if (input) {
      input.addEventListener('keydown', handleInputKeyDown)
      input.addEventListener('focus', handleInputFocus)
    }
  })

  filterAndPaginate()
})

onBeforeUnmount(() => {
  window.removeEventListener('mouseup', onClickOutside as EventListener)
  const input = getInputElement()
  if (input) {
    input.removeEventListener('keydown', handleInputKeyDown)
    input.removeEventListener('focus', handleInputFocus)
  }
})

const rootAttrs = computed(() => {
  const attrs: Record<string, string> = {}
  if (props.dataTestid) {
    attrs['data-testid'] = props.dataTestid
  }
  return attrs
})

// Computed property to find the currently checked radio button ID for aria-activedescendant
const activeDescendantId = computed(() => {
  if (!dropdownVisible.value || !dropdownRef.value) return null
  const checkedRadio = dropdownRef.value.querySelector(
    'input[name="search-select-radio-group"]:checked',
  ) as HTMLInputElement | null
  return checkedRadio?.id || null
})

// ARIA attributes for input (combobox pattern)
const inputAriaAttrs = computed(() => {
  const attrs: Record<string, string | boolean> = {
    role: 'combobox',
    'aria-expanded': dropdownVisible.value,
    'aria-autocomplete': 'list',
    'aria-haspopup': 'listbox',
  }

  if (dropdownVisible.value && listboxId) {
    attrs['aria-controls'] = listboxId
  }

  if (activeDescendantId.value) {
    attrs['aria-activedescendant'] = activeDescendantId.value
  }

  return attrs
})

defineExpose<{
  blur: () => void
  clear: () => void
  blurAndClear: () => void
  focus: () => void
}>({
  blur: handleBlurInput,
  clear: handleClear,
  blurAndClear: handleBlurAndClear,
  focus: focusInput,
})
</script>

<template>
  <div ref="componentRef" class="fds-search-select block mb-6" v-bind="rootAttrs">
    <div class="relative block">
      <div v-if="!singleItemName.length" class="relative">
        <div class="relative">
          <FdsInput
            ref="inputComponentRef"
            :label="label"
            :meta="meta ?? undefined"
            :labelLeft="labelLeft"
            class="relative mb-0! w-full!"
            :valid="valid"
            :invalidMessage="invalidMessage"
            :disabled="disabled"
            type="search"
            :value="searchTerm"
            :id="inputId"
            :clearButton="!!searchTerm"
            :mask="pidMask"
            :maskOptions="pidMaskOptions"
            v-bind="inputAriaAttrs"
            :searchIcon="!disabled && !hasInputValue"
            @input="handleInput"
            @clearInput="handleClear"
          />
        </div>
      </div>
      <div v-else>
        <div class="font-bold mb-2 tracking-wide">{{ singleItemLabel }}</div>
        <div>{{ singleItemName }}</div>
      </div>

      <!-- Dropdown List -->
      <div
        ref="dropdownRef"
        v-if="dropdownVisible && !singleItemName.length && !disabled"
        :class="listWrapperClasses"
        :style="listWrapperStyle"
      >
        <!-- Loading -->
        <div v-if="loading" class="flex justify-center p-4">
          <FdsSpinner color="blue" size="48px" />
        </div>

        <!-- Results -->
        <template v-else-if="displayedItems && displayedItems.length">
          <!-- Header -->
          <div v-if="searchTerm && searchTerm.length" :class="headerClasses">
            {{ displayedItems.length }} {{ searchContext.linkWord }} {{ totalCount }}
            {{ searchContext.context }}
          </div>
          <div v-else-if="!searchTerm.length" :class="headerClasses">{{ totalCount }} {{ searchContext.context }}</div>

          <!-- List -->
          <ul class="block m-0 list-none p-0" role="listbox" :id="listboxId" @keydown="handleListKeyDown">
            <li
              v-for="(item, index) in displayedItems"
              :key="index"
              :id="`search-select-item-${index}`"
              :class="listItemWrapperClasses"
              role="option"
              @mouseup="(e) => handleClick(e, item)"
            >
              <input
                type="radio"
                :id="`search-select-radio-${index}`"
                name="search-select-radio-group"
                class="absolute left-0 opacity-0"
                @keydown.enter="(e) => handleClick(e, item)"
                @keydown.space.prevent="(e) => handleClick(e, item)"
              />
              <label
                :for="`search-select-radio-${index}`"
                :class="[labelClasses, listItemClasses]"
                v-html="handleMatchingString(item)"
              ></label>
            </li>
          </ul>

          <!-- Pagination -->
          <FdsPagination
            v-if="page !== undefined && totalPages !== null && totalPages > 1"
            :current="page"
            :max="totalPages"
            @paginate="handlePagination"
            class="my-4! px-2"
          />
        </template>

        <!-- No Results -->
        <ul v-else-if="!loading" class="block m-0 list-none p-0">
          <li class="p-4">
            {{ noResultPrompt }}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>
