<template>
  <el-select v-bind="{ ...$attrs, value: value, placeholder, disabled, loading, clearable, filterable, multiple }"
    @input="$emit('input', $event)" @change="$emit('change', $event)" @visible-change="onVisibleChange">
    <el-option v-for="opt in options" :key="String(opt.value)" :label="opt.label" :value="opt.value" />
  </el-select>
</template>

<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'

const props = withDefaults(
  defineProps<{
    value?: unknown
    /** 拉取选项的接口，返回 Promise<Array<{ label, value }>> 或 { list: [] }；可接收当前 params 作为参数 */
    api?: (params?: Record<string, unknown>) => Promise<unknown>
    /** 请求参数，变化时会重新拉取 options */
    params?: Record<string, unknown>
    /** 是否懒加载：为 true 时在展开下拉时再请求数据，不在挂载时请求 */
    lazy?: boolean
    /** 接口返回列表中「标签」字段名，默认 'label' */
    labelField?: string
    /** 接口返回列表中「值」字段名，默认 'value' */
    valueField?: string
    placeholder?: string
    disabled?: boolean
    clearable?: boolean
    filterable?: boolean
    multiple?: boolean
  }>(),
  { labelField: 'label', valueField: 'value', lazy: false }
)

defineEmits<{ (e: 'input', value: unknown): void; (e: 'change', value: unknown): void }>()

const loading = ref(false)
const options = ref<Array<{ label: string; value: unknown }>>([])
const rawOptions = ref<unknown[]>([])

const sortStringify = (obj: unknown): string => {
  if (obj === null || obj === undefined) return 'null'
  return JSON.stringify(sortKeys(obj))
}

const sortKeys = (obj: unknown): unknown => {
  if (Array.isArray(obj)) return obj.map(sortKeys)
  if (obj !== null && typeof obj === 'object') {
    return Object.keys(obj as Record<string, unknown>)
      .sort()
      .reduce<Record<string, unknown>>((acc, key) => {
        acc[key] = sortKeys((obj as Record<string, unknown>)[key])
        return acc
      }, {})
  }
  return obj
}

const lastFetchedParamsKey = ref<string>(sortStringify(props.params ?? null))

const onVisibleChange = (visible: boolean) => {
  if (props.lazy && visible) {
    const paramsKey = sortStringify(props.params ?? null)
    if (paramsKey !== lastFetchedParamsKey.value || options.value.length === 0) {
      fetchOptions()
    }
  }
}

const fetchOptions = async () => {
  if (!props.api) return
  loading.value = true
  try {
    const res = await props.api(props.params)
    const raw = Array.isArray(res)
      ? res
      : ((res as Record<string, unknown>)?.list as unknown[]) ??
      ((res as Record<string, unknown>)?.data as unknown[]) ??
      []
    rawOptions.value = raw
    const labelKey = props.labelField ?? 'label'
    const valueKey = props.valueField ?? 'value'
    options.value = raw.map((item: unknown) => {
      const o = item as Record<string, unknown>
      return {
        label: String(o[labelKey] ?? o.label ?? ''),
        value: o[valueKey] ?? o.value,
      }
    })
    lastFetchedParamsKey.value = sortStringify(props.params ?? null)
  } finally {
    loading.value = false
  }
}

defineExpose({
  options,
  rawOptions,
  loading,
  fetchOptions,
})

onMounted(() => {
  if (!props.lazy) fetchOptions()
})
watch(() => props.api, () => { 
  if (!props.lazy) {
    fetchOptions()
  } else {
    options.value = []
    rawOptions.value = []
  }
}, { deep: true }
)
watch(() => props.params, () => {
  const newKey = sortStringify(props.params ?? null)
  if (newKey === lastFetchedParamsKey.value) return
  if (props.lazy) {
    options.value = []
    rawOptions.value = []
  } else {
    fetchOptions()
  }
}, { deep: true })
</script>
