import { isEqual } from 'lodash'
import {
  computed,
  defineComponent,
  onMounted,
  ref,
  watch,
  type PropType,
  type Ref,
} from 'vue'
import useClickOutside from '../../composables/use-click-outside'
import useEndlessScrollApi, { type paramsSerializationOptions } from '../../composables/use-endless-scroll-api'
import useScrollPagination from '../../composables/use-scroll-pagination'
import { DebounceRejectError } from '../../helpers'
import type ApiInterface from '../../types/api-interface'
import type DataProviderInterface from '../../types/data-provider-interface'
import defineTemplate, { DEFAULT_SHOW_ITEMS_COUNT } from './template'
import type { RelationSelectInterface, SelectSlotParams } from './types'

import './styles.scss'

/**
 * Компонент представляет собой селект, получающий данные из API.
 * Обладает различными настраиваемыми свойствами: параметры запроса, сериализации, добавление пользовательских элементов,
  поиск элемента, настройка пагинации и пр.
 *
 * <p>
 *   <a href="https://www.figma.com/file/Q4Z4CnFx4i0qhD5fbgnPpf/%F0%9F%92%BB-%D0%94%D0%B8%D0%B7%D0%B0%D0%B9%D0%BD-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0-ARK-SPACE?node-id=13818%3A138256&mode=dev" target="_blank">Дизайн</a>
 * </p>
 */

export function defineRelationSelect<T extends { id: string, name: string }>() {
  return defineComponent({
    props: {
      /**
       * Опция копирования значения
       */
      isWithCopy: {
        type: Boolean,
        default: false,
      },
      /**
       * Интерфейс API
       */
      api: {
        type: Object as PropType<ApiInterface>,
        default: undefined,
      },

      /**
       * Пользовательская установка значения **подсветки** \
       */
      customHighlightSearch: {
        type: Function as PropType<(value: string) => string>,
        default: undefined,
      },

      /**
       * Интерфейс dataProvider.
       *
       * Обращение к бэку без использования axios
       */
      dataProvider: {
        type: Object as PropType<DataProviderInterface>,
        default: undefined,
      },
      /**
       * Вариант отображения селекта (filter, form)
       */
      appearance: {
        type: String as PropType<'filter'|'form'>,
        default: 'filter',
      },
      /**
       * Вкл/Выкл возможность очищать выбранное значение
       */
      resetable: {
        type: Boolean,
        default: false,
      },
      /**
       * Флаг, сброса значений при одинаковых параметрах
       */
      resetOnQueryParamsChange: {
        type: Boolean,
        default: false,
      },
      /**
       * Флаг, указывающий, отключен ли селект
       */
      disabled: {
        type: Boolean,
        default: false,
      },
      /**
       * Эндпоинд, на который будут отправляться запросы на получение массива элементов / поиска
       */
      endpoint: {
        type: String,
        required: true,
      },
      /**
       * Массив ошибок
       */
      errors: {
        type: Array as PropType<string[]>,
        default: () => [],
      },
      /**
       * Функция в которой можно привести массив элементов к нужному формату
       */
      itemConverter: {
        type: Function as PropType<(item: any) => T>,
      },
      /**
       * Функция со слотом кастомных элементов списка
       */
      itemSlot: {
        type: Function as PropType<(item: T, params: SelectSlotParams<T>) => JSX.Element>,
      },
      /**
       * Слот слева перед установленным значением
       */
      leftSlot: {
        type: Function as PropType<(item: T) => JSX.Element>,
        default: undefined,
      },
      /**
       * Значение селекта
       */
      modelValue: {
        type: Object as PropType<T|undefined>,
        default: undefined,
      },
      /**
       * Функция смены значения
       */
      onValueChange: {
        type: Function as PropType<(v: T|undefined) => void>,
        default: () => () => {
          // do nothing
        },
      },
      /**
       * Тип пагинации скроллом или постраничная
       */
      paginationType: {
        type: String,
      },
      /**
       * Объект с запрашиваемыми параметрами в URL'e
       */
      params: {
        type: Object as PropType<{ [code: string] : string|string[] }>,
        default: () => ({}),
      },
      /**
       * Количество элементов запрашиваемое в запросе к бэку на одной странице пагинации
       */
      perPage: {
        type: Number,
        default: 10,
      },
      /**
       * Текстовая подсказка в окне селекта при значении undefined в modelValue
       */
      placeholder: {
        type: String,
        default: '',
      },
      /**
       * Описание селекта
       */
      description: {
        type: String,
        default: '',
      },
      /**
       * Флаг, отвечающий за предвыбранный элемент из списка
       */
      preselected: {
        type: Boolean,
        default: false,
      },
      /**
       * Ключ пагинации текущей страницы
       */
      requestPageKey: {
        type: String,
      },
      /**
       * Ключ пагинации куда указывать количество элементов запрашиваемое в запросе к бэку на одной странице пагинации
       */
      requestPerPageKey: {
        type: String,
      },
      /**
       * Ключ массива из которого брать элементы списка
       */
      responseItemsKey: {
        type: String,
      },
      /**
       * Ключ количества всех элементов
       */
      responseTotalKey: {
        type: String,
      },
      /**
       * Ключ параметра отвечающий за поиск элемента на бэке
       */
      searchKey: {
        type: String,
        default: 'name',
      },
      /**
       * Название параметра сортировки
       */
      sortFieldKey: {
        type: String,
        default: undefined,
      },
      /**
       * Размер селекта: 'S' или 'M'.
       * Если не указать, то по умолчанию 'M'.
       */
      size: {
        type: String as PropType<'M'|'S'>,
      },
      /**
       * Название селекта
       */
      title: {
        type: String,
      },
      /**
       * Отметить как обязательный
       */
      isRequired: {
        type: Boolean,
        default: false,
      },
      /**
       * Тип сериализации квери параметров: 'default' | 'custom' | 'comma'
       */
      paramsSerialization: {
        type: String as PropType<paramsSerializationOptions>,
        default: 'default',
      },
      /**
       * Массив элементов добавляемый юзером
       */
      additionalItems: {
        type: Array as PropType<T[]>,
      },
      /**
       * Функция проверки наличия элементов в массиве
       */
      onInitialLoad: {
        type: Function as PropType<(isEmpty: boolean) => void>,
      },
      /**
       * Число отображаемых элементов в раскрытом списке
       */
      showItemsCount: {
        type: Number,
        default: DEFAULT_SHOW_ITEMS_COUNT,
      },
      /**
       * Флаг, в значении true отображает id каждого элемента
       */
      showDescriptionId: {
        type: Boolean,
        default: false,
      },
      /**
       * Использовать для пажинации флаги наличия следующей и предыдущей страницы вместо флага count
       */
      usePrevNextFlags: {
        type: Boolean,
        default: false,
      },
      /**
       * Ключ флага наличия следующей страницы в ответе бекенда
       */
      nextPageFlagKey: {
        type: String,
      },
      /**
       * Ключ флага наличия предыдущей страницы в ответе бекенда
       */
      prevPageFlagKey: {
        type: String,
      },
      hideSearch: {
        type: Boolean,
        default: false,
      },
    },

    setup(props, { expose }) {
      const isOpened = ref(false)
      const triggerDown = ref(null) as unknown as Ref<HTMLElement>
      const triggerUp = ref(null) as unknown as Ref<HTMLElement>
      const canBeLoaded = ref(false)
      const root = ref(null) as unknown as Ref<HTMLElement>
      const select = ref(null) as unknown as Ref<HTMLElement>
      const search = ref('')
      const isLoadingPrev = ref(false)
      const isLoadingNext = ref(false)

      const value = computed(() => props.modelValue as T)

      const unset = () => {
        if (props.onValueChange) props.onValueChange(undefined)
      }

      const queryParams = computed((): typeof props.params => {
        const params = { ...props.params } // Де-структуризация ломает реактивность
        if (search.value !== '') params[props.searchKey] = search.value
        return params
      })

      const {
        items, loadNext, loadPrev, initialLoad, reload, offset, clear, count,
      } = useEndlessScrollApi<T>({
        endpoint: props.endpoint,
        params: queryParams,
        itemConverter: props.itemConverter,
        paginationType: props.paginationType,
        requestPageKey: props.requestPageKey,
        requestPerPageKey: props.requestPerPageKey,
        api: props.api,
        dataProvider: props.dataProvider,
        paramsSerialization: props.paramsSerialization,
        autoReload: false,
        requestSize: ref(props.showItemsCount + 5),
        usePrevNextFlags: props.usePrevNextFlags,
        prevPageFlagKey: props.prevPageFlagKey,
        nextPageFlagKey: props.nextPageFlagKey,
        sortFieldKey: props.sortFieldKey,
      })

      if (props.preselected) {
        onMounted(async () => {
          await initialLoad()
          canBeLoaded.value = true

          if (props.onValueChange) props.onValueChange(items.value[0])

          if (props.onInitialLoad) {
            props.onInitialLoad(Boolean(items.value.length))
          }
        })
      }

      useScrollPagination({
        callback: async () => {
          if (canBeLoaded.value && !isLoadingNext.value && !isLoadingPrev.value) {
            const lastChild = root.value.children[root.value.children.length - 2] as HTMLElement
            isLoadingNext.value = true
            const flag = await loadNext()
            if (flag) {
              root.value.scrollTo({ top: lastChild.offsetTop + lastChild.clientHeight - root.value.clientHeight })
              setTimeout(() => { isLoadingNext.value = false }, 100)
            } else isLoadingNext.value = false
          }
        },
        trigger: triggerDown,
        root,
      })

      useScrollPagination({
        callback: async () => {
          if (canBeLoaded.value && !isLoadingNext.value && !isLoadingPrev.value) {
            const firstChildIndex = 1 + (props.additionalItems?.length || 0)
            const firstChild = root.value.children[firstChildIndex] as HTMLElement

            isLoadingPrev.value = true
            const flag = await loadPrev()
            if (flag) {
              root.value.scrollTo({ top: firstChild.offsetTop })
              setTimeout(() => { isLoadingPrev.value = false }, 100)
            } else isLoadingPrev.value = false
          }
        },
        trigger: triggerUp,
        root,
      })

      watch(() => props.params, (after, before) => {
        if (props.resetOnQueryParamsChange && !isEqual(after, before)) {
          unset()
        }
      })

      const deferredReload = async () => {
        // мгновенная перезагрузка происходит только в том случае, если селект раскрыт, иначе она будет выполена отложенно при раскрытии
        if (isOpened.value) {
          isLoadingNext.value = true
          isLoadingPrev.value = true

          try {
            await reload()
            isLoadingNext.value = false
            isLoadingPrev.value = false
          } catch (error) {
            if (error instanceof DebounceRejectError) return
            isLoadingNext.value = false
            isLoadingPrev.value = false
          }

          return
        }

        clear()
      }

      watch(() => props.endpoint, async () => {
        deferredReload()
      })

      watch(queryParams, async (after, before) => {
        if (isEqual(after, before)) return
        deferredReload()
      })

      watch(isOpened, async () => {
        if (isOpened.value && items.value.length === 0) {
          isLoadingNext.value = true
          await initialLoad()
          isLoadingNext.value = false
          canBeLoaded.value = true

          if (props.onInitialLoad) {
            props.onInitialLoad(Boolean(items.value.length))
          }
        }
      })

      useClickOutside(select, () => { isOpened.value = false })

      const toggleOpened = () => {
        isOpened.value = !isOpened.value
      }

      const toggleItem = (item: T) => {
        if (props.onValueChange) props.onValueChange(item)
        toggleOpened()
      }

      const isSelected = (item: T) => value.value?.id === item.id

      const exposeParms: RelationSelectInterface<T> = {
        items,
        isOpened,
        clear,
        unset,
      }

      expose(exposeParms)

      const SelectTemplate = defineTemplate<T>()

      return (): JSX.Element => {
        const templateProps = {
          isWithCopy: props.isWithCopy,
          clear: unset,
          disabled: props.disabled,
          errors: props.errors,
          isLoadingNext: isLoadingNext.value,
          isLoadingPrev: isLoadingPrev.value,
          isOpened: isOpened.value,
          isSelected,
          items: items.value,
          itemSlot: props.itemSlot,
          leftSlot: props.leftSlot ? ((value: T[]) => props.leftSlot!(value[0])) : undefined,
          modelValue: value.value ? [value.value] : [],
          placeholder: props.placeholder,
          description: props.description,
          isRequired: props.isRequired,
          resetable: props.resetable,
          root,
          search: search.value,
          setSearch: (s: string) => { search.value = s },
          select,
          title: props.title,
          toggleItem,
          toggleOpened,
          topItems: [],
          triggerUp,
          triggerDown,
          isSingle: true,
          appearance: props.appearance,
          additionalItems: props.additionalItems,
          size: props.size,
          showItemsCount: props.showItemsCount,
          hideSearch: props.hideSearch,
          showDescriptionId: props.showDescriptionId,
          customHighlightSearch: props.customHighlightSearch?.(search.value),
        }

        return <SelectTemplate {...templateProps}/>
      }
    },
  })
}

export default defineRelationSelect<{ id: string, name: string }>()
