<template>
  <div class="ecp-tree-select" ref="rootRef">
    <el-input
      :value="displayText"
      :placeholder="placeholder"
      :disabled="disabled"
      :clearable="clearable"
      readonly
      suffix-icon="el-icon-arrow-down"
      class="ecp-tree-select__input"
      @focus="openDropdown"
      @clear="clearValue"
    />
    <transition name="el-zoom-in-top">
      <div v-show="dropdownVisible" class="ecp-tree-select__dropdown">
        <div v-if="filterable" class="ecp-tree-select__filter-inner">
          <el-input
            v-model="filterText"
            size="small"
            placeholder="搜索节点"
            prefix-icon="el-icon-search"
            clearable
            @click.native.stop
          />
        </div>
        <el-tree
          v-show="!loading"
          ref="treeRef"
          :data="treeData"
          :props="treeProps"
          :node-key="valueField"
          :filter-node-method="filterable ? filterNodeMethod : undefined"
          :highlight-current="true"
          default-expand-all
          @node-click="onNodeClick"
        />
        <div v-if="loading" class="ecp-tree-select__loading">
          <i class="el-icon-loading" /> 加载中...
        </div>
      </div>
    </transition>
  </div>
</template>

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

interface TreeNode {
  label?: string
  value?: unknown
  children?: TreeNode[]
  [key: string]: unknown
}

const props = withDefaults(
  defineProps<{
    value?: unknown
    /** 树数据，直接传入时优先使用，不请求 api */
    treeData?: unknown[]
    api?: (params?: Record<string, unknown>) => Promise<unknown>
    params?: Record<string, unknown>
    lazy?: boolean
    labelField?: string
    valueField?: string
    childrenField?: string
    filterable?: boolean
    placeholder?: string
    disabled?: boolean
    clearable?: boolean
  }>(),
  {
    labelField: 'label',
    valueField: 'value',
    childrenField: 'children',
    lazy: false,
  }
)

const emit = defineEmits<{ (e: 'input', value: unknown): void }>()

const rootRef = ref<HTMLElement>()
const treeRef = ref()
const dropdownVisible = ref(false)
const loading = ref(false)
const filterText = ref('')
const treeData = ref<TreeNode[]>([])
const flatLabelMap = ref<Record<string, string>>({})

const treeProps = computed(() => ({
  label: props.labelField,
  children: props.childrenField,
}))

function normalizeNode(node: Record<string, unknown>): TreeNode {
  const labelKey = props.labelField ?? 'label'
  const valueKey = props.valueField ?? 'value'
  const childrenKey = props.childrenField ?? 'children'
  const children = node[childrenKey] as unknown[]
  const out: TreeNode = {}
  out[props.labelField ?? 'label'] = node[labelKey] ?? node.label
  out[props.valueField ?? 'value'] = node[valueKey] ?? node.value
  if (Array.isArray(children) && children.length) {
    out[props.childrenField ?? 'children'] = children.map((c) => normalizeNode(c as Record<string, unknown>))
  }
  return out
}

function buildFlatLabelMap(nodes: TreeNode[], prefix = ''): Record<string, string> {
  const map: Record<string, string> = {}
  const labelKey = props.labelField ?? 'label'
  const valueKey = props.valueField ?? 'value'
  const childrenKey = props.childrenField ?? 'children'
  for (const node of nodes) {
    const label = String(node[labelKey] ?? node.label ?? '')
    const value = node[valueKey] ?? node.value
    if (value !== undefined && value !== null) {
      map[String(value)] = prefix ? prefix + ' / ' + label : label
    }
    const children = node[childrenKey] ?? node.children
    if (Array.isArray(children) && children.length) {
      Object.assign(map, buildFlatLabelMap(children as TreeNode[], label))
    }
  }
  return map
}

function hasTreeDataProp() {
  const td = props.treeData
  return Array.isArray(td) && td.length > 0
}

function applyTreeData(nodes: TreeNode[]) {
  treeData.value = nodes
  flatLabelMap.value = buildFlatLabelMap(nodes)
}

function syncFromTreeDataProp() {
  const td = props.treeData
  if (!Array.isArray(td) || td.length === 0) return
  const normalized = td.map((item) => normalizeNode(item as Record<string, unknown>))
  applyTreeData(normalized)
}

const displayText = computed(() => {
  if (props.value == null || props.value === '') return ''
  return flatLabelMap.value[String(props.value)] ?? String(props.value)
})

const filterNodeMethod = (value: string, data: TreeNode) => {
  if (!value) return true
  const labelKey = props.labelField ?? 'label'
  const label = String(data[labelKey] ?? data.label ?? '')
  return label.toLowerCase().includes(value.toLowerCase())
}

watch(filterText, (val) => {
  treeRef.value?.filter(val)
})

let clickOutsideHandler: ((e: MouseEvent) => void) | null = null

function openDropdown() {
  if (props.disabled) return
  dropdownVisible.value = true
  if (props.lazy && !hasTreeDataProp()) fetchData()
  nextTick(() => {
    clickOutsideHandler = (e: MouseEvent) => {
      if (rootRef.value && !rootRef.value.contains(e.target as Node)) {
        closeDropdown()
      }
    }
    document.addEventListener('click', clickOutsideHandler)
  })
}

function closeDropdown() {
  dropdownVisible.value = false
  filterText.value = ''
  if (clickOutsideHandler) {
    document.removeEventListener('click', clickOutsideHandler)
    clickOutsideHandler = null
  }
}

function clearValue() {
  emit('input', undefined)
}

function onNodeClick(data: TreeNode) {
  const valueKey = props.valueField ?? 'value'
  const val = data[valueKey] ?? data.value
  emit('input', val)
  closeDropdown()
}

async function fetchData() {
  if (!props.api || hasTreeDataProp()) 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[]) ??
        []
    treeData.value = raw.map((item) => normalizeNode(item as Record<string, unknown>))
    flatLabelMap.value = buildFlatLabelMap(treeData.value)
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  if (hasTreeDataProp()) {
    syncFromTreeDataProp()
  } else if (!props.lazy) {
    fetchData()
  }
})

watch(() => props.treeData, () => {
  if (hasTreeDataProp()) {
    syncFromTreeDataProp()
  } else {
    treeData.value = []
    flatLabelMap.value = {}
    if (!props.lazy && props.api) fetchData()
  }
}, { deep: true })

watch(() => props.api, () => {
  if (hasTreeDataProp()) return
  if (!props.lazy) fetchData()
  else { treeData.value = []; flatLabelMap.value = {} }
}, { deep: true })

watch(() => props.params, () => {
  if (hasTreeDataProp()) return
  if (props.lazy) { treeData.value = []; flatLabelMap.value = {} }
  else fetchData()
}, { deep: true })
</script>

<style>
.ecp-tree-select {
  position: relative;
  width: 100%;
}
.ecp-tree-select__filter-inner {
  margin-bottom: 8px;
}
.ecp-tree-select__dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  max-height: 280px;
  overflow: auto;
  background: #fff;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  margin-top: 4px;
  z-index: 1000;
  padding: 8px;
}
.ecp-tree-select__loading {
  padding: 24px;
  text-align: center;
  color: #909399;
  font-size: 14px;
}
</style>
