<script lang="ts" setup>
import BmsEmptyScreen from '@/components/table/BmsEmptyScreen.vue';
import BmsPagination from '@/components/table/BmsPagination.vue';
import UiBmsTable from '@/components/table/UiBmsTable.vue';
import UiFilterButton from '@/components/table/UiFilterButton.vue';
import { usePagination } from '@/composables/pagination.composable';
import {
  reflectFiltersToPath,
  useSearch,
} from '@/composables/search.composable';
import { useSort } from '@/composables/sort.composable';
import {
  Filter,
  SavedFilter,
  SelectMode,
  ServerTableRequestParams,
  Sort,
  SortValue,
  TableHeader,
  TableMode,
} from '@/models';
import _debounce from 'lodash/debounce';
import { computed, type Ref, ref, type Slots, watch, watchEffect } from 'vue';
import BmsTableFilters from '@/components/table/BmsTableFilters.vue';
import BmsSearch from '../form/BmsSearch.vue';
import { BmsTag } from '@/index';
import { Trash } from 'lucide-vue-next';
import { CanceledError } from 'axios';

interface UiTableProps {
  disableSearch?: boolean;
  headers: TableHeader[];
  filters?: Filter[];
  url?: string;
  mode?: TableMode | string; // string for retro compatibility
  persistent?: boolean;
  pagination?: number[];
  size?: number;
  initialPage?: number;
  debounceTime?: number;
  request?: (
    params: ServerTableRequestParams,
    abortController: AbortController,
    url?: string,
  ) => Promise<{ data: unknown[]; total: number }>;
  defaultFiltersOpened?: boolean;
  savedFilters?: SavedFilter[];
  canSaveFilters?: boolean;
  defaultSort?: Sort;
  selectedItems?: unknown[];
  selectable?: boolean;
  selectableDisabled?: boolean;
  maxSelectedSize?: number;
  selectMode?: SelectMode;
}

const props = withDefaults(defineProps<UiTableProps>(), {
  mode: TableMode.NORMAL,
  persistent: true,
  pagination: () => [25, 50, 100],
  size: 25,
  initialPage: 0,
  filters: () => [],
  defaultFiltersOpened: false,
  debounceTime: 400,
  savedFilters: () => [],
  canSaveFilters: false,
  defaultSort: () => ({
    key: null,
    value: SortValue.default,
  }),
  selectedItems: () => [],
  selectable: false,
  selectableDisabled: false,
  maxSelectedSize: 10000,
  selectMode: SelectMode.DEFAULT,
  request: async (
    {
      size,
      page,
      search,
      sort,
    }: { size: number; page: number; search?: string; sort?: Sort },
    controller: AbortController,
    url?: string,
  ) => {
    if (!url) {
      throw 'URL must be defined';
    }

    const searchParams: {
      size: string;
      page: string;
      search?: string;
      sort?: string;
    } = {
      size: '' + size,
      page: '' + page,
    };

    if (search) {
      searchParams['search'] = search;
    }

    if (sort && sort.key && sort.value !== SortValue.default) {
      searchParams['sort'] = `${sort.key},${sort.value}`;
    }

    const response = await fetch(
      `${url}?${new URLSearchParams(searchParams)}`,
      {
        method: 'GET',
        signal: controller.signal,
      },
    );
    const totalItems = response.headers.get('X-Total');
    const data = await response.json();
    return { data, total: totalItems ? parseInt(totalItems) : Infinity };
  },
});

const {
  offset,
  items,
  size,
  pagination,
  currentPage,
  paginationsOptions,
  goToPage,
  isMounting: isPaginationMounting,
} = usePagination(
  props.persistent,
  props.size,
  props.pagination,
  props.initialPage,
);

const {
  changeSort,
  sort,
  isMounting: isSortMounting,
} = useSort(props.persistent, props.headers, props.defaultSort);

defineSlots();

const {
  search,
  reflect,
  filters,
  isFilterVisible,
  numberOfActiveFilters,
  isMounting: isSearchMounting,
  toggleFilters,
  isSavedFilterActive,
  resetFilters,
  selectSavedFilter,
  updateFiltersFromProps,
} = useSearch(props.persistent, props.filters, props.defaultFiltersOpened);

const emits = defineEmits<{
  deleteSavedFilter: [value: SavedFilter];
  saveFilter: [value: SavedFilter];
  'update:selectMode': [selectMode: SelectMode];
  filterInput: [{ filterKey: string; value: any; e: InputEvent }];
  filterChange: [{ filterKey: string; value: any }];
}>();

const loading = ref<boolean>(false);
const total = ref<number>(Infinity);
const totalPages = computed(() => Math.ceil(total.value / size.value));
const controller = ref<AbortController>();
const isMounting = ref(true);
const selectedItems: Ref<unknown[]> = defineModel('selectedItems', {
  default: [] as unknown[],
});
const localSelectMode = computed({
  get: () => props.selectMode,
  set: (selectMode: SelectMode) => {
    emits('update:selectMode', selectMode);
  },
});

watchEffect(
  async () => {
    isMounting.value =
      isPaginationMounting.value ||
      isSortMounting.value ||
      isSearchMounting.value;
  },
  { flush: 'post' },
);

watchEffect(() => {
  reflect.value =
    props.persistent && !(props.disableSearch && props.filters.length === 0);
  size.value = props.size;
  pagination.value = props.pagination;
});

watch([currentPage, isMounting], () => {
  if (!isMounting.value && !loading.value) {
    fetchData();
  }
});
watch(
  () => props.filters,
  () => {
    updateFiltersFromProps(props.filters);
    reflectFiltersToPath(filters.value);
  },
  { deep: true },
);

// Watch vuejs cannot call directly the debounce function
const debouncedHandler = _debounce((callBack) => {
  callBack();
}, props.debounceTime);

watch(
  [() => filters.value, () => sort.value, size, search],
  () => {
    if (!isMounting.value) {
      const callBack = () => {
        currentPage.value = props.initialPage;
        emits('update:selectMode', SelectMode.DEFAULT);
        fetchData();
      };
      debouncedHandler(callBack);
    }
  },
  { deep: true },
);

watch(
  [() => filters.value, search],
  () => {
    if (!isMounting.value) {
      selectedItems.value = [];
    }
  },
  { deep: true },
);

async function fetchData() {
  if (controller.value) {
    controller.value.abort();
  }
  controller.value = new AbortController();
  loading.value = true;
  try {
    const { data, total: totalItems } = await props.request(
      {
        size: size.value,
        offset: offset.value,
        page: currentPage.value,
        search: search.value,
        filters: filters.value,
        sort: sort.value,
      },
      controller.value,
      props.url,
    );
    items.value = data;
    total.value = totalItems;
  } catch (error) {
    if (
      !(error instanceof CanceledError) &&
      (error as any)?.name !== 'AbortError'
    ) {
      throw error;
    }
  } finally {
    loading.value = false;
  }
}

const onPrevClick = () => {
  goToPage(currentPage.value - 1, total.value);
};

const onNextClick = () => {
  goToPage(currentPage.value + 1, total.value);
};

const go = (index: number) => {
  goToPage(index + props.initialPage, total.value);
};

defineExpose({
  refresh: fetchData,
});

const onSavedFilterDismiss = (savedFilter: SavedFilter) => {
  emits('deleteSavedFilter', savedFilter);
};

const onSaveFilter = (savedFilter: SavedFilter) => {
  emits('saveFilter', savedFilter);
};

const onClickHeader = (header: TableHeader) => {
  changeSort(header);
};

const onClearSelection = () => {
  emits('update:selectMode', SelectMode.DEFAULT);
  selectedItems.value = [];
};

const onSelectAll = () => emits('update:selectMode', SelectMode.ALL);

const isTableSmall = computed(() => props.mode === TableMode.SMALL);
</script>

<template>
  <UiBmsTable
    v-model:selectedItems="selectedItems"
    v-model:selectMode="localSelectMode"
    :loading="loading"
    :items="items"
    :headers="headers"
    :mode="mode as TableMode"
    :hasFilters="filters.length > 0"
    :sort="sort"
    :selectable="selectable"
    :selectableDisabled="selectableDisabled"
    :totalSize="total"
    :maxSelectedSize="maxSelectedSize"
    @clickHeader="onClickHeader"
    @selectAll="onSelectAll"
    @clearSelection="onClearSelection"
  >
    <template #custom-actions>
      <slot name="custom-action"></slot>
    </template>

    <template v-for="(_, name) in $slots" v-slot:[name]="slotData">
      <slot :name="name" v-bind="slotData" />
    </template>

    <template #filters>
      <Transition name="table-fade">
        <div v-show="isFilterVisible" class="filters-container">
          <BmsTableFilters
            v-model="filters"
            :canSaveFilters="canSaveFilters"
            :small="mode == 'small'"
            @saveFilter="onSaveFilter"
            @resetFilters="resetFilters"
            @filterInput="(filterEvent) => emits('filterInput', filterEvent)"
            @filterChange="(filterEvent) => emits('filterChange', filterEvent)"
          />
        </div>
      </Transition>
    </template>

    <template #saved-filters>
      <BmsTag
        v-if="canSaveFilters"
        v-for="savedFilter in savedFilters"
        :key="savedFilter.id"
        :active="isSavedFilterActive(savedFilter)"
        @click="selectSavedFilter(savedFilter)"
        @dismiss="onSavedFilterDismiss(savedFilter)"
      >
        <template #default>
          {{ savedFilter.name }}
        </template>
        <template #closeIcon>
          <Trash />
        </template>
      </BmsTag>
    </template>

    <template #search>
      <UiFilterButton
        v-if="filters.length > 0"
        :small="isTableSmall"
        :activeFiltersNb="numberOfActiveFilters"
        :isFilterVisible="isFilterVisible"
        @toggleFilters="toggleFilters"
      />
      <template v-if="!disableSearch">
        <slot name="search">
          <BmsSearch
            :modelValue="search"
            class="table-search"
            :small="isTableSmall"
            @update:modelValue="search = $event"
          />
        </slot>
      </template>
    </template>

    <template #empty>
      <slot name="empty">
        <BmsEmptyScreen
          :title="search ? 'Aucun résultat' : 'Aucune donnée'"
          :description="
            search
              ? 'Votre recherche n\'a retourné aucun résultat.'
              : 'Il semblerait qu\'il n\'y ait rien ici.'
          "
        />
      </slot>
    </template>

    <template #pagination>
      <div v-if="props.size" class="bms-table__pagination">
        <BmsPagination
          v-model:currentSize="size"
          :total="total"
          :currentPage="props.initialPage === 1 ? currentPage - 1 : currentPage"
          :sizes="paginationsOptions"
          :pages="totalPages"
          :small="isTableSmall"
          @prev="onPrevClick"
          @next="onNextClick"
          @go="go($event)"
        />
      </div>
    </template>
  </UiBmsTable>
</template>
<style lang="scss" scoped>
.table-search {
  width: 260px;
}
</style>
