<script lang="ts" setup>
import type {
  TableSelectorColumn,
  TableSelectorEmits,
  TableSelectorProps,
} from './types';

import { computed, ref, watch } from 'vue';

import { Table2 } from '@vben/icons';
import { $t } from '@vben/locales';

import {
  ElButton,
  ElCheckbox,
  ElEmpty,
  ElInput,
  ElOption,
  ElPagination,
  ElRadio,
  ElSelect,
  ElTable,
  ElTableColumn,
} from 'element-plus';

import { requestClient } from '#/api/request';
import { ZqDialog } from '#/components/zq-dialog';

defineOptions({
  name: 'TableSelector',
  inheritAttrs: false,
});

const selectorText = {
  clickToSelect: '点击选择',
  nodeLabel: '名称',
  nodeValue: '值',
  search: '搜索',
  selectData: '选择数据',
  selectedCount: (count: number) => `已选择 ${count} 项`,
};

const props = withDefaults(defineProps<TableSelectorProps>(), {
  multiple: false,
  placeholder: selectorText.clickToSelect,
  disabled: false,
  clearable: true,
  dialogTitle: selectorText.selectData,
  dialogWidth: '800px',
  columns: () => [],
  labelField: 'label',
  valueField: 'value',
  dataSourceType: 'static',
  apiMethod: 'GET',
  searchFields: () => [],
  collapseTags: false,
  options: () => [],
});

const emit = defineEmits<TableSelectorEmits>();

const dialogVisible = ref(false);
const loading = ref(false);
const tableData = ref<Array<Record<string, any>>>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const searchKeyword = ref('');
const selectedValues = ref<Set<string>>(new Set());
const selectedItemsMap = ref<Map<string, Record<string, any>>>(new Map());
const labelsLoading = ref(false);

function normalizeValues(value: undefined | string | string[]) {
  if (!value) return [];
  return Array.isArray(value) ? value.map(String) : [String(value)];
}

function pickList(response: any) {
  return Array.isArray(response)
    ? response
    : response?.items || response?.list || response?.data || [];
}

function pickTotal(response: any, fallback: number) {
  return Number(response?.total ?? response?.count ?? fallback);
}

function getItemLabel(item: undefined | Record<string, any>, value: string) {
  if (!item) return value;
  return (
    item[props.labelField] ??
    item.name ??
    item.title ??
    item.label ??
    item.text ??
    value
  );
}

async function loadInitialLabels(values: string[]) {
  if (values.length === 0) return;

  try {
    if (
      props.dataSourceType === 'static' ||
      (!props.dataSourceType && props.options.length > 0)
    ) {
      for (const item of props.options) {
        const value = String(item[props.valueField]);
        if (values.includes(value)) selectedItemsMap.value.set(value, item);
      }
      return;
    }

    if (props.dataSourceType === 'formData' && props.formCode) {
      const response = await requestClient.get(
        `/api/online_dev/form-data/${props.formCode}/list`,
        {
          params: {
            [`${props.valueField}__in`]: values.join(','),
            pageSize: values.length,
          },
        },
      );
      for (const item of pickList(response)) {
        selectedItemsMap.value.set(String(item[props.valueField]), item);
      }
      return;
    }

    if (props.dataSourceType === 'dict' && props.dictCode) {
      const response = await requestClient.get(
        `/api/core/dict_item/by/dict_code/${props.dictCode}`,
      );
      for (const item of pickList(response)) {
        const value = String(item[props.valueField]);
        if (values.includes(value)) selectedItemsMap.value.set(value, item);
      }
      return;
    }

    if (props.dataSourceType === 'dataSource' && props.dataSourceCode) {
      const response = await requestClient.get(
        `/api/core/data-source/execute/${props.dataSourceCode}`,
      );
      for (const item of pickList(response)) {
        const value = String(item[props.valueField]);
        if (values.includes(value)) selectedItemsMap.value.set(value, item);
      }
    }
  } catch (error) {
    console.error('load selector labels failed:', error);
  }
}

function emitSelectItem() {
  const selectedItems = [...selectedValues.value]
    .map((value) => selectedItemsMap.value.get(value))
    .filter(Boolean);
  emit('select-item', props.multiple ? selectedItems : selectedItems[0]);
}

watch(
  () => props.modelValue,
  async (value) => {
    const values = normalizeValues(value);
    selectedValues.value = new Set(values);
    const missingValues = values.filter((v) => !selectedItemsMap.value.has(v));
    if (missingValues.length > 0) {
      labelsLoading.value = true;
      await loadInitialLabels(missingValues);
      labelsLoading.value = false;
      emitSelectItem();
    }
  },
  { immediate: true },
);

const displayTags = computed(() =>
  [...selectedValues.value].map((value) => ({
    value,
    label: getItemLabel(selectedItemsMap.value.get(value), value),
  })),
);

const selectDisplayValue = computed(() => {
  const values = [...selectedValues.value];
  return props.multiple ? values : values[0];
});

const showPagination = computed(
  () =>
    props.dataSourceType === 'formData' ||
    props.dataSourceType === 'api' ||
    total.value > pageSize.value,
);

const displayColumns = computed<TableSelectorColumn[]>(() => {
  if (props.columns.length > 0) return props.columns;
  return [
    { field: props.labelField, label: selectorText.nodeLabel },
    { field: props.valueField, label: selectorText.nodeValue },
  ];
});

async function loadData() {
  loading.value = true;
  try {
    let data: Array<Record<string, any>> = [];
    let totalCount = 0;

    if (
      props.dataSourceType === 'static' ||
      (!props.dataSourceType && props.options.length > 0)
    ) {
      const keyword = searchKeyword.value.trim().toLowerCase();
      data = keyword
        ? props.options.filter((item) =>
            [props.labelField, props.valueField].some((field) =>
              String(item[field] ?? '')
                .toLowerCase()
                .includes(keyword),
            ),
          )
        : props.options;
      totalCount = data.length;
    } else if (props.dataSourceType === 'formData' && props.formCode) {
      const params: Record<string, any> = {
        page: currentPage.value,
        pageSize: pageSize.value,
      };
      if (searchKeyword.value) {
        const searchField = props.searchFields[0] || props.labelField;
        params[`${searchField}__like`] = searchKeyword.value;
      }
      const response = await requestClient.get(
        `/api/online_dev/form-data/${props.formCode}/list`,
        { params },
      );
      data = pickList(response);
      totalCount = pickTotal(response, data.length);
    } else if (props.dataSourceType === 'dict' && props.dictCode) {
      const response = await requestClient.get(
        `/api/core/dict_item/by/dict_code/${props.dictCode}`,
      );
      data = pickList(response);
      totalCount = data.length;
    } else if (props.dataSourceType === 'dataSource' && props.dataSourceCode) {
      const response = await requestClient.get(
        `/api/core/data-source/execute/${props.dataSourceCode}`,
      );
      data = pickList(response);
      totalCount = data.length;
    } else if (props.dataSourceType === 'api' && props.apiUrl) {
      const query = {
        page: currentPage.value,
        pageSize: pageSize.value,
        keyword: searchKeyword.value,
      };
      const response =
        props.apiMethod === 'POST'
          ? await requestClient.post(props.apiUrl, query)
          : await requestClient.get(props.apiUrl, { params: query });
      data = pickList(response);
      totalCount = pickTotal(response, data.length);
    }

    tableData.value = data;
    total.value = totalCount;
    for (const item of data) {
      const value = String(item[props.valueField]);
      if (selectedValues.value.has(value)) selectedItemsMap.value.set(value, item);
    }
  } catch (error) {
    console.error('load selector data failed:', error);
    tableData.value = [];
    total.value = 0;
  } finally {
    loading.value = false;
  }
}

function openDialog() {
  if (props.disabled) return;
  dialogVisible.value = true;
  loadData();
}

function handleSearch() {
  currentPage.value = 1;
  loadData();
}

function handlePageChange(page: number) {
  currentPage.value = page;
  loadData();
}

function handleSizeChange(size: number) {
  pageSize.value = size;
  currentPage.value = 1;
  loadData();
}

function isRowSelected(row: Record<string, any>) {
  return selectedValues.value.has(String(row[props.valueField]));
}

function getRowValue(row: Record<string, any>) {
  return String(row[props.valueField]);
}

function toggleRow(row: Record<string, any>, checked?: boolean | number | string) {
  const value = getRowValue(row);
  if (props.multiple) {
    const nextChecked = checked ?? !selectedValues.value.has(value);
    if (nextChecked) {
      selectedValues.value.add(value);
      selectedItemsMap.value.set(value, row);
    } else {
      selectedValues.value.delete(value);
      selectedItemsMap.value.delete(value);
    }
    selectedValues.value = new Set(selectedValues.value);
    return;
  }

  selectedValues.value = new Set([value]);
  selectedItemsMap.value.clear();
  selectedItemsMap.value.set(value, row);
}

const isAllSelected = computed(
  () =>
    tableData.value.length > 0 &&
    tableData.value.every((row) => isRowSelected(row)),
);

const isIndeterminate = computed(() => {
  const selectedCount = tableData.value.filter((row) => isRowSelected(row)).length;
  return selectedCount > 0 && selectedCount < tableData.value.length;
});

function handleSelectAll(checked: boolean | number | string) {
  for (const row of tableData.value) toggleRow(row, checked);
}

function handleConfirm() {
  const values = [...selectedValues.value];
  const result = props.multiple ? values : values[0];
  emit('update:modelValue', result);
  emit('change', result);
  emit('blur');
  emitSelectItem();
  dialogVisible.value = false;
}

function handleClear() {
  selectedValues.value = new Set();
  selectedItemsMap.value.clear();
  emit('update:modelValue', props.multiple ? [] : undefined);
  emit('change', props.multiple ? [] : undefined);
  emit('select-item', undefined);
}

function handleRemoveTag(value: string) {
  selectedValues.value.delete(value);
  selectedItemsMap.value.delete(value);
  selectedValues.value = new Set(selectedValues.value);
  const values = [...selectedValues.value];
  const result = props.multiple ? values : values[0];
  emit('update:modelValue', result);
  emit('change', result);
  emitSelectItem();
}
</script>

<template>
  <div class="table-selector">
    <ElSelect
      :model-value="labelsLoading ? undefined : selectDisplayValue"
      :placeholder="labelsLoading ? $t('common.loading') : placeholder"
      :disabled="disabled"
      :clearable="clearable"
      :multiple="multiple"
      :collapse-tags="multiple && collapseTags"
      :collapse-tags-tooltip="multiple && collapseTags"
      :loading="labelsLoading"
      :suffix-icon="Table2"
      class="w-full"
      popper-class="table-selector-popper-hidden"
      @click="openDialog"
      @clear="handleClear"
      @remove-tag="handleRemoveTag"
    >
      <ElOption
        v-for="tag in displayTags"
        :key="tag.value"
        :label="tag.label"
        :value="tag.value"
      />
    </ElSelect>

    <ZqDialog v-model="dialogVisible" :title="dialogTitle" :width="dialogWidth">
      <div class="table-selector-content flex h-[600px] flex-col">
        <div class="mb-4 flex shrink-0 items-center gap-2">
          <ElInput
            v-model="searchKeyword"
            :placeholder="selectorText.search"
            clearable
            class="w-64"
            @change="handleSearch"
            @keyup.enter="handleSearch"
          />
          <ElButton type="primary" @click="handleSearch">
            {{ $t('common.search') }}
          </ElButton>
        </div>

        <div class="min-h-0 flex-1">
          <ElTable
            v-loading="loading"
            :data="tableData"
            border
            stripe
            height="100%"
            :row-class-name="
              ({ row }: any) => (isRowSelected(row) ? 'selected-row' : '')
            "
            style="width: 100%"
            @row-click="toggleRow"
          >
            <ElTableColumn v-if="!multiple" width="55" align="center">
              <template #default="{ row }">
                <ElRadio
                  :model-value="isRowSelected(row) ? getRowValue(row) : ''"
                  :value="getRowValue(row)"
                  @click.stop
                  @change="toggleRow(row)"
                >
                  <span></span>
                </ElRadio>
              </template>
            </ElTableColumn>
            <ElTableColumn v-else width="55" align="center">
              <template #header>
                <ElCheckbox
                  :model-value="isAllSelected"
                  :indeterminate="isIndeterminate"
                  @change="handleSelectAll"
                />
              </template>
              <template #default="{ row }">
                <ElCheckbox
                  :model-value="isRowSelected(row)"
                  @click.stop
                  @change="toggleRow(row, $event)"
                />
              </template>
            </ElTableColumn>
            <ElTableColumn
              v-for="col in displayColumns"
              :key="col.field"
              :prop="col.field"
              :label="col.label"
              :width="col.width"
            />
            <template #empty>
              <ElEmpty :description="$t('common.noData')" />
            </template>
          </ElTable>
        </div>
      </div>

      <template #footer>
        <div class="flex w-full items-center">
          <div class="flex-shrink-0">
            <ElPagination
              v-if="showPagination"
              v-model:current-page="currentPage"
              v-model:page-size="pageSize"
              :total="total"
              :page-sizes="[10, 20, 50, 100]"
              layout="total, sizes, prev, pager, next"
              small
              @current-change="handlePageChange"
              @size-change="handleSizeChange"
            />
          </div>
          <div class="flex flex-1 justify-center">
            <span
              v-if="selectedValues.size > 0"
              class="text-sm text-[var(--el-text-color-secondary)]"
            >
              {{
                selectorText.selectedCount(selectedValues.size)
              }}
            </span>
          </div>
          <div class="flex flex-shrink-0 gap-2">
            <ElButton @click="dialogVisible = false">
              {{ $t('common.cancel') }}
            </ElButton>
            <ElButton type="primary" @click="handleConfirm">
              {{ $t('common.confirm') }}
            </ElButton>
          </div>
        </div>
      </template>
    </ZqDialog>
  </div>
</template>

<style scoped>
.table-selector {
  width: 100%;
}

.table-selector-content :deep(.selected-row) {
  background-color: var(--el-color-primary-light-9) !important;
}

.table-selector-content :deep(.el-table__row) {
  cursor: pointer;
}
</style>

<style>
.table-selector-popper-hidden {
  display: none !important;
}
</style>
