<template>
  <div ref="rootRef" class="vm-tree-select">
    <button
      ref="triggerRef"
      type="button"
      class="vm-tree-select__trigger"
      :class="{ 'is-open': open, 'is-disabled': disabled }"
      :disabled="disabled"
      @click="toggle"
    >
      <span v-if="displayLabel" class="vm-tree-select__value">{{ displayLabel }}</span>
      <span v-else class="vm-tree-select__ph">{{ placeholder || crudLabel('pleaseSelect') }}</span>
      <i
        v-if="clearable && hasValue"
        class="ri-close-circle-fill vm-tree-select__clear"
        @click.stop="clear"
      />
      <i class="ri-arrow-down-s-line vm-tree-select__caret" />
    </button>

    <Teleport to="body">
      <div
        v-if="open"
        ref="panelRef"
        class="vm-tree-select__panel vm-dialog-float"
        :style="panelStyle"
      >
        <div
          v-for="row in visibleRows"
          :key="row.key"
          class="vm-tree-select__row"
          :class="{ 'is-active': row.key === inner }"
          :style="{ paddingLeft: `${10 + row.depth * 16}px` }"
        >
          <button
            v-if="row.hasKids"
            type="button"
            class="vm-tree-select__expand"
            :class="{ 'is-open': expanded.has(row.key) }"
            @click.stop="toggleExpand(row.key)"
          >
            <i class="ri-arrow-right-s-line" />
          </button>
          <span v-else class="vm-tree-select__expand is-spacer" />
          <button type="button" class="vm-tree-select__label" @click="pick(row.key)">
            {{ row.label }}
          </button>
        </div>
        <p v-if="!visibleRows.length" class="vm-tree-select__empty">{{ crudLabel('empty') }}</p>
      </div>
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { crudLabel } from '../style'
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { POPUP_TRIGGER_GAP } from '../../lib/dialog-float'
import type { TreeSelectOption } from '../../../../typings/admin/comm/crud'
type FlatRow = {
  key: string
  label: string
  depth: number
  hasKids: boolean
}

defineOptions({ name: 'vm-tree-select' })

const props = withDefaults(
  defineProps<{
    modelValue?: string | number | boolean | null
    options?: TreeSelectOption[]
    labelKey?: string
    valueKey?: string
    placeholder?: string
    disabled?: boolean
    clearable?: boolean
    /** 默认全部展开（为 true 时忽略 defaultExpandLevel） */
    defaultExpandAll?: boolean
    /** 默认展开到第几级（1=仅根节点，2=展开根的下级，以此类推） */
    defaultExpandLevel?: number
  }>(),
  {
    labelKey: 'label',
    valueKey: 'value',
    clearable: true,
    defaultExpandAll: false,
    /** 1=仅展示一级（根收起）；选中深层值时仍会展开祖先路径 */
    defaultExpandLevel: 1,
  },
)

const emit = defineEmits<{
  'update:modelValue': [v: string]
  change: [v: string]
}>()

const rootRef = ref<HTMLElement | null>(null)
const triggerRef = ref<HTMLElement | null>(null)
const panelRef = ref<HTMLElement | null>(null)
const open = ref(false)
const expanded = ref<Set<string>>(new Set())
const panelStyle = ref<Record<string, string>>({})

const tree = computed(() => props.options || [])

const inner = computed(() =>
  props.modelValue == null || props.modelValue === ''
    ? ''
    : String(props.modelValue),
)

const hasValue = computed(() => inner.value !== '')

function findLabel(nodes: TreeSelectOption[], value: string): string | undefined {
  for (const n of nodes) {
    if (String(n[props.valueKey]) === value) {
      return String(n[props.labelKey] ?? '')
    }
    const kids = n.children
    if (kids?.length) {
      const hit = findLabel(kids, value)
      if (hit != null) return hit
    }
  }
  return undefined
}

const displayLabel = computed(() => {
  if (!inner.value) return ''
  return findLabel(tree.value, inner.value) || inner.value
})

function collectExpandable(nodes: TreeSelectOption[], into: Set<string>) {
  for (const n of nodes) {
    const kids = n.children
    if (kids?.length) {
      into.add(String(n[props.valueKey]))
      collectExpandable(kids, into)
    }
  }
}

/** 展开到指定层级：level=2 时根节点展开、二级可见、三级收起 */
function collectExpandToLevel(
  nodes: TreeSelectOption[],
  depth: number,
  maxLevel: number,
  into: Set<string>,
) {
  if (depth >= maxLevel - 1) return
  for (const n of nodes) {
    const kids = n.children
    if (!kids?.length) continue
    into.add(String(n[props.valueKey]))
    collectExpandToLevel(kids, depth + 1, maxLevel, into)
  }
}

function findAncestorKeys(nodes: TreeSelectOption[], value: string, trail: string[] = []): string[] | null {
  for (const n of nodes) {
    const key = String(n[props.valueKey] ?? '')
    if (key === value) return trail
    const kids = n.children
    if (kids?.length) {
      const hit = findAncestorKeys(kids, value, [...trail, key])
      if (hit) return hit
    }
  }
  return null
}

function syncExpanded() {
  const next = new Set<string>()
  const opts = tree.value
  if (props.defaultExpandAll) {
    collectExpandable(opts, next)
  } else {
    const level = Math.max(1, props.defaultExpandLevel ?? 1)
    collectExpandToLevel(opts, 0, level, next)
  }
  if (inner.value) {
    for (const key of findAncestorKeys(opts, inner.value) || []) {
      next.add(key)
    }
  }
  expanded.value = next
}

function flatten(
  nodes: TreeSelectOption[],
  depth: number,
  out: FlatRow[],
) {
  for (const n of nodes) {
    const key = String(n[props.valueKey] ?? '')
    const kids = n.children || []
    const hasKids = kids.length > 0
    out.push({
      key,
      label: String(n[props.labelKey] ?? ''),
      depth,
      hasKids,
    })
    if (hasKids && expanded.value.has(key)) {
      flatten(kids, depth + 1, out)
    }
  }
}

const visibleRows = computed(() => {
  const out: FlatRow[] = []
  flatten(tree.value, 0, out)
  return out
})

watch(
  () => [props.options, props.defaultExpandAll, props.defaultExpandLevel, inner.value],
  () => syncExpanded(),
  { immediate: true, deep: true },
)

function syncPanelPos() {
  const el = triggerRef.value
  if (!el) return
  const r = el.getBoundingClientRect()
  const width = Math.max(r.width, 260)
  // 贴触发框左侧 + 底部
  panelStyle.value = {
    position: 'fixed',
    top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
    left: `${r.left}px`,
    width: `${width}px`,
    zIndex: '200',
  }
}

async function toggle() {
  if (props.disabled) return
  open.value = !open.value
  if (open.value) {
    syncExpanded()
    await nextTick()
    syncPanelPos()
  }
}

function toggleExpand(key: string) {
  const next = new Set(expanded.value)
  if (next.has(key)) next.delete(key)
  else next.add(key)
  expanded.value = next
}

function pick(value: string) {
  emit('update:modelValue', value)
  emit('change', value)
  open.value = false
}

function clear() {
  emit('update:modelValue', '')
  emit('change', '')
}

function isInside(el: Node | null) {
  if (!el) return false
  if (rootRef.value?.contains(el)) return true
  if (panelRef.value?.contains(el)) return true
  return el instanceof Element && Boolean(el.closest('.vm-tree-select__panel'))
}

function onDocPointer(e: Event) {
  const t = e.target as Node | null
  if (!isInside(t)) open.value = false
}

function onWinChange() {
  if (open.value) syncPanelPos()
}

onMounted(() => {
  document.addEventListener('pointerdown', onDocPointer, true)
  window.addEventListener('resize', onWinChange)
  window.addEventListener('scroll', onWinChange, true)
})
onUnmounted(() => {
  document.removeEventListener('pointerdown', onDocPointer, true)
  window.removeEventListener('resize', onWinChange)
  window.removeEventListener('scroll', onWinChange, true)
})
</script>

<style lang="scss">
.vm-tree-select {
  position: relative;
  width: 100%;
}

.vm-tree-select__trigger {
  display: flex;
  width: 100%;
  height: var(--vm-control-height);
  min-height: var(--vm-control-height);
  align-items: center;
  gap: 6px;
  padding: 0 var(--vm-control-padding-x) 0 12px;
  border: 1px solid var(--input);
  border-radius: var(--vm-control-radius);
  background: var(--background);
  color: var(--foreground);
  font-size: var(--vm-control-font-size);
  text-align: left;
  cursor: pointer;
  transition:
    border-color 0.15s ease,
    box-shadow 0.15s ease;

  outline: none;

  &:hover:not(.is-disabled) {
    border-color: color-mix(in srgb, var(--brand) 45%, var(--input));
  }

  /* 弹层触发：不展示打开/聚焦描边 */

  &.is-disabled {
    cursor: not-allowed;
    opacity: 0.55;
  }
}

.vm-tree-select__value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vm-tree-select__ph {
  flex: 1;
  color: var(--muted-foreground);
}

.vm-tree-select__clear {
  flex-shrink: 0;
  color: var(--muted-foreground);
  font-size: 14px;
  cursor: pointer;

  &:hover {
    color: var(--foreground);
  }
}

.vm-tree-select__caret {
  flex-shrink: 0;
  opacity: 0.45;
  font-size: 16px;
}

.vm-tree-select__label {
  flex: 1;
  min-width: 0;
  padding: 6px 8px 6px 2px;
  border: none;
  background: transparent;
  color: var(--foreground);
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Teleport 到 body，不可 scoped */
.vm-tree-select__panel {
  max-height: min(320px, 50vh);
  overflow: auto;
  overscroll-behavior: contain;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card);
  box-shadow: var(--shadow-soft);
}

.vm-tree-select__row {
  display: flex;
  min-height: 28px;
  align-items: center;
  gap: 2px;
  border-radius: 8px;

  &.is-active {
    background: var(--brand-soft);

    .vm-tree-select__label {
      color: var(--brand);
      font-weight: 650;
    }
  }

  &:hover:not(.is-active) {
    background: var(--muted);
  }
}

.vm-tree-select__expand {
  display: inline-flex;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;

  i {
    font-size: 16px;
    transition: transform 0.15s ease;
  }

  &.is-open i {
    transform: rotate(90deg);
  }

  &.is-spacer {
    cursor: default;
    visibility: hidden;
  }

  &:hover:not(.is-spacer) {
    background: color-mix(in srgb, var(--muted) 80%, var(--foreground) 8%);
  }
}

.vm-tree-select__empty {
  margin: 0;
  padding: 16px;
  color: var(--muted-foreground);
  font-size: 14px;
  text-align: center;
}
</style>
