<template>
  <ComboBox
    v-model="modelValue"
    v-bind="props"
    :choices="filteredChoices"
    :close-on-select="props.closeOnSelect ?? !Array.isArray(modelValue)"
    input-container-class="tw-flex tw-flex-wrap tw-p-2 tw-gap-2 tw-items-baseline tw-w-full form-control tw-h-auto tw-min-h-[46px]"
    input-class="tw-border-none tw-px-1 tw-w-[100px] focus:tw-shadow-none focus:tw-outline-none focus:tw-ring-0"
    @search="searchQuery = $event"
  >
    <template #default="{ choice, ...attrs }">
      <slot :choice="choice as T" v-bind="attrs">
        <li v-bind="attrs">
          {{ (choice as T).name }}
        </li>
      </slot>
    </template>

    <template #selected="{ value, deselect, isOpen }">
      <slot
        name="selected"
        :value="value as (T | T[] | null)"
        :deselect="deselect"
        :is-open="isOpen"
      >
        <template v-if="Array.isArray(value)">
          <Tag
            v-for="(item, index) in value"
            :key="index"
            :label="item.name"
            removable
            @remove="deselect(item)"
          />
        </template>
        <template v-else>
          <p v-if="value">{{ (value as T).name }}</p>
        </template>
      </slot>
    </template>
  </ComboBox>
</template>

<script lang="ts">
import type { ComboBoxProps } from '@/components/ComboBox.vue'

export type ComboBoxSimpleProps<T> = Omit<ComboBoxProps<T>, 'choices'> & {
  choices?: T[]
  searchProvider?: (query: string, signal: AbortSignal) => Promise<T[]>
}
</script>

<script setup lang="ts" generic="T extends { id: ID, name?: string }">
import { ID } from '@rokoli/bnb/drf'
import { computedAsync } from '@vueuse/core'
import { computed, ref, toValue } from 'vue'
import { computedDebounced, matchesSearch } from '@/util'
import Tag from './generic/Tag.vue'
import ComboBox from './ComboBox.vue'

defineOptions({
  compatConfig: { MODE: 3 },
})
defineSlots<{
  default(
    props: Record<string, unknown> & {
      id: string
      choice: T
      index: number
      activeIndex: number
    },
  ): unknown
  selected(props: {
    value: null | T | T[]
    deselect: (choice: null | T) => void
    isOpen: boolean
  }): unknown
}>()

const modelValue = defineModel<null | T | T[]>({ required: true })
const props = defineProps<ComboBoxSimpleProps<T>>()

const searchQuery = ref('')
const debouncedSearchQuery = computedDebounced(searchQuery, (t) => (t.trim() ? 0.3 : 0))
const selectedIds = computed(() => {
  if (Array.isArray(modelValue.value)) {
    return modelValue.value.map((item) => item.id)
  } else if (modelValue.value !== null) {
    return [modelValue.value.id]
  } else {
    return []
  }
})

let abortController: AbortController | null = null
const searchedChoices = computedAsync(
  async () => {
    if (props.choices) {
      const query = toValue(searchQuery)
      return props.choices.filter(({ name }) => matchesSearch(name ?? '', query))
    } else if (props.searchProvider) {
      const query = toValue(debouncedSearchQuery)
      if (abortController) abortController.abort()
      abortController = new AbortController()
      try {
        return await props.searchProvider(query, abortController.signal)
      } finally {
        abortController = null
      }
    } else {
      return []
    }
  },
  props.choices ?? [],
  { lazy: false },
)
const filteredChoices = computed(() =>
  searchedChoices.value.filter((choice) => !selectedIds.value.includes(choice.id)),
)
</script>