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 { AllCheckState, RelationSelectInterface, SelectSlotParams } from './types'

import './styles.scss'

export interface MultipleRelationSelectInterface {
  isOpened: boolean
}

export function defineMultipleRelationSelect<T extends { id: string, name: string }>() {
  return defineComponent({
    props: {
      api: {
        type: Object as PropType<ApiInterface>,
        default: undefined,
      },
      /**
       * Пользовательская установка значения **подсветки** \
       */
      customHighlightSearch: {
        type: Function as PropType<(value: string) => string>,
        default: undefined,
      },
      dataProvider: {
        type: Object as PropType<DataProviderInterface>,
        default: undefined,
      },
      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>,
      },
      modelValue: {
        type: Object as PropType<T[]>,
        required: true,
      },
      onValueChange: {
        type: Function as PropType<(v: T[]) => void>,
        required: true,
      },
      paginationType: {
        type: String,
      },
      params: {
        type: Object as PropType<{ [code: string] : string|string[] }>,
        default: () => ({}),
      },
      perPage: {
        type: Number,
        default: 10,
      },
      placeholder: {
        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,
      },
      size: {
        type: String as PropType<'M'|'S'>,
      },
      title: {
        type: String,
      },
      paramsSerialization: {
        type: String as PropType<paramsSerializationOptions>,
        default: 'default',
      },
      additionalItems: {
        type: Array as PropType<T[]>,
      },
      showItemsCount: {
        type: Number,
        default: DEFAULT_SHOW_ITEMS_COUNT,
      },
      hideSearch: {
        type: Boolean,
        default: false,
      },
      root: {
        type: Object as PropType<Ref<HTMLElement | null>>,
        default: document,
      },
      showDescriptionId: {
        type: Boolean,
        default: false,
      },
      /**
       * Использовать для пажинации флаги наличия следующей и предыдущей страницы вместо флага count
       */
      usePrevNextFlags: {
        type: Boolean,
        default: false,
      },
      /**
       * Ключ флага наличия следующей страницы в ответе бекенда
       */
      nextPageFlagKey: {
        type: String,
      },
      /**
       * Ключ флага наличия предыдущей страницы в ответе бекенда
       */
      prevPageFlagKey: {
        type: String,
      },
      requestSize: {
        type: Number,
      },
      canCheckAll: {
        type: Boolean,
        default: false,
      },
    },

    setup(props, context) {
      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 topItems: Ref<T[]> = ref([])

      topItems.value = [...props.modelValue]

      const requestSize = computed(() => props.requestSize || Math.max(topItems.value.length, props.showItemsCount) + 10)

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

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

      const {
        items, loadNext, loadPrev, initialLoad, reload, offset, clear, hasNextPage, hasPrevPage,
      } = useEndlessScrollApi<T>({
        endpoint: props.endpoint,
        params: queryParams,
        itemConverter: props.itemConverter,
        requestSize,
        paginationType: props.paginationType,
        requestPageKey: props.requestPageKey,
        requestPerPageKey: props.requestPerPageKey,
        api: props.api,
        dataProvider: props.dataProvider,
        paramsSerialization: props.paramsSerialization,
        autoReload: false,
        usePrevNextFlags: props.usePrevNextFlags,
        prevPageFlagKey: props.prevPageFlagKey,
        nextPageFlagKey: props.nextPageFlagKey,
        debug: props.placeholder === 'Код (дубль названия)',
        sortFieldKey: props.sortFieldKey,
      })

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

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

      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)
          }
        },
        trigger: triggerDown,
        root,
      })

      useScrollPagination({
        callback: async () => {
          if (canBeLoaded.value && !isLoadingNext.value && !isLoadingPrev.value) {
            const firstChildIndex = 1 + topItems.value.length + (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)
          }
        },
        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
        }
      })

      useClickOutside(select, () => {
        isOpened.value = false
        topItems.value = [...props.modelValue]
      }, props.root)

      const setSearch = (s: string) => {
        search.value = s
        topItems.value = [...props.modelValue]
      }

      const isSelected = (item: T) => Boolean(props.modelValue.find((i) => i.id === item.id))

      const toggleItem = (item: T) => {
        if (isSelected(item)) {
          const i = props.modelValue.find((i) => i.id === item.id)
          if (!i) return
          const newValue = [...props.modelValue]
          newValue.splice(newValue.indexOf(i), 1)

          if (props.onValueChange) props.onValueChange(newValue)
        } else if (props.onValueChange) props.onValueChange([...props.modelValue, item])
      }

      const toggleOpened = () => {
        isOpened.value = !isOpened.value
        topItems.value = [...props.modelValue]
      }

      const allCheckState = computed<AllCheckState>(() => {
        if (!props.canCheckAll || items.value.length === 0 || hasNextPage.value || (!hasNextPage.value && hasPrevPage.value)) return undefined
        if (!props.modelValue.length) return false

        const idsToExclude = new Set([
          ...items.value.map((item) => item.id),
          ...(props.additionalItems || []).map((item) => item.id),
        ])
        const filteredTopItems = topItems.value.filter((item) => !idsToExclude.has(item.id))

        return props.modelValue.length === items.value.length + (props.additionalItems?.length || 0) + filteredTopItems.length ? true : 'multiple'
      })

      const toggleAll = () => {
        if (allCheckState.value === true) {
          props.onValueChange([])
        } else {
          const idsToExclude = new Set([
            ...items.value.map((item) => item.id),
            ...(props.additionalItems || []).map((item) => item.id),
          ])

          const filteredTopItems = topItems.value.filter((item) => !idsToExclude.has(item.id))

          props.onValueChange([...filteredTopItems, ...items.value, ...props.additionalItems || []])
        }
      }

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

      context.expose(exposeParms)

      const SelectTemplate = defineTemplate<T>()

      return (): JSX.Element => {
        const templateProps = {
          clear: unset,
          disabled: props.disabled,
          errors: props.errors,
          isLoadingNext: isLoadingNext.value,
          isLoadingPrev: isLoadingPrev.value,
          isOpened: isOpened.value,
          isSelected,
          items: items.value,
          itemSlot: props.itemSlot,
          modelValue: props.modelValue,
          placeholder: props.placeholder,
          resetable: props.resetable,
          root,
          search: search.value,
          setSearch,
          select,
          title: props.title,
          toggleItem,
          toggleOpened,
          topItems: topItems.value,
          triggerUp,
          triggerDown,
          isSingle: false,
          additionalItems: props.additionalItems,
          size: props.size,
          showItemsCount: props.showItemsCount,
          hideSearch: props.hideSearch,
          showDescriptionId: props.showDescriptionId,
          allCheckState: allCheckState.value,
          toggleAll,
          customHighlightSearch: props.customHighlightSearch?.(search.value),
        }

        return <SelectTemplate {...templateProps}/>
      }
    },
  })
}
export default defineMultipleRelationSelect<{ id: string, name: string }>()
