<template>
  <div ref="rootRef" class="vm-group-cascader">
    <div
      ref="triggerRef"
      class="vm-group-cascader__trigger"
      :class="{ 'is-open': open, 'is-disabled': disabled }"
      @click="onTriggerClick"
    >
      <input
        v-if="editable"
        class="vm-group-cascader__input"
        :value="displayText"
        :placeholder="placeholder ?? crudLabel('pleaseSelect')"
        :disabled="disabled"
        @focus="openPanel"
        @input="onInput"
        @click.stop="openPanel"
      />
      <button
        v-else
        type="button"
        class="vm-group-cascader__btn"
        :disabled="disabled"
        @click.stop="toggle"
      >
        <span v-if="multiple && selectedList.length" class="vm-group-cascader__tags">
          <span class="vm-group-cascader__tag">{{ selectedList[0] }}</span>
          <span v-if="selectedList.length > 1" class="vm-group-cascader__more">
            + {{ selectedList.length - 1 }}
          </span>
        </span>
        <span v-else-if="displayText" class="vm-group-cascader__value">{{ displayText }}</span>
        <span v-else class="vm-group-cascader__ph">{{ placeholder ?? crudLabel('pleaseSelect') }}</span>
      </button>
      <i
        v-if="clearable && hasValue"
        class="ri-close-circle-fill vm-group-cascader__clear"
        @click.stop="clear"
      />
      <i class="ri-arrow-down-s-line vm-group-cascader__caret" @click.stop="toggle" />
    </div>

    <Teleport to="body">
      <div
        v-if="open"
        ref="panelRef"
        class="vm-group-cascader__panel vm-dialog-float"
        :style="panelStyle"
      >
        <div
          v-for="(col, colIdx) in columns"
          :key="colIdx"
          class="vm-group-cascader__col"
          :class="{ 'is-leaf': colIdx === columns.length - 1 && col.every((n) => n.isLeaf) }"
        >
          <button
            v-for="node in col"
            :key="node.value"
            type="button"
            class="vm-group-cascader__item"
            :class="{
              'is-active': path[colIdx] === node.value || isSelected(node),
              'is-leaf': node.isLeaf,
            }"
            @click="onPick(node, colIdx)"
          >
            <label
              v-if="multiple && node.isLeaf"
              class="vm-group-cascader__check"
              @click.stop
            >
              <input
                type="checkbox"
                :checked="isSelected(node)"
                @change="toggleLeaf(node)"
              />
            </label>
            <span>{{ node.label }}</span>
            <i v-if="!node.isLeaf && node.children?.length" class="ri-arrow-right-s-line" />
            <i
              v-else-if="!multiple && (node.isLeaf || selectAnyLevel) && isSelected(node)"
              class="ri-check-line vm-group-cascader__tick"
            />
          </button>
          <p v-if="!col.length" class="vm-group-cascader__empty">{{ crudLabel('empty') }}</p>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { crudLabel } from '../crud/style'
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { POPUP_TRIGGER_GAP } from '../lib/dialog-float'
import type { GroupCascaderNode } from '../../../typings/admin/comm/cascader'
defineOptions({ name: 'vm-group-cascader' })

const props = withDefaults(
  defineProps<{
    modelValue?: string | string[] | null
    tree?: GroupCascaderNode[]
    multiple?: boolean
    clearable?: boolean
    editable?: boolean
    disabled?: boolean
    placeholder?: string
    /** 单选时允许选中非叶子节点（如部门父级） */
    selectAnyLevel?: boolean
    /** 多选时分隔符 */
    separator?: string
  }>(),
  {
    tree: () => [],
    multiple: false,
    clearable: true,
    editable: true,
    placeholder: undefined,
    separator: ',',
  },
)

const emit = defineEmits<{
  'update:modelValue': [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 panelStyle = ref<Record<string, string>>({})
/** 当前展开路径上每一列选中的非叶子 value */
const path = ref<string[]>([])

const selectedSet = computed(() => {
  if (props.multiple) {
    const raw = Array.isArray(props.modelValue)
      ? props.modelValue
      : String(props.modelValue ?? '')
          .split(props.separator)
          .map((s) => s.trim())
          .filter(Boolean)
    return new Set(raw.map(String))
  }
  const v = props.modelValue
  if (v == null || v === '' || Array.isArray(v)) return new Set<string>()
  return new Set([String(v)])
})

const hasValue = computed(() => selectedSet.value.size > 0)

const selectedList = computed(() => [...selectedSet.value])

const displayText = computed(() => {
  if (props.multiple) {
    const arr = selectedList.value
    if (!arr.length) return ''
    return arr.length === 1 ? arr[0]! : `${arr[0]} +${arr.length - 1}`
  }
  return props.modelValue == null ? '' : String(props.modelValue)
})

/** 找叶子到根的祖先 value 链（不含叶子自身） */
function findAncestorPath(
  nodes: GroupCascaderNode[],
  target: string,
  trail: string[] = [],
): string[] | null {
  for (const n of nodes) {
    if (n.value === target) return trail
    if (n.children?.length) {
      const hit = findAncestorPath(n.children, target, [...trail, n.value])
      if (hit) return hit
    }
  }
  return null
}

const columns = computed(() => {
  const cols: GroupCascaderNode[][] = [props.tree || []]
  let level = props.tree || []
  for (const key of path.value) {
    const node = level.find((n) => n.value === key)
    if (!node?.children?.length) break
    cols.push(node.children)
    level = node.children
  }
  return cols
})

function isSelected(node: GroupCascaderNode) {
  return selectedSet.value.has(node.value)
}

function emitSingle(v: string) {
  emit('update:modelValue', v)
}

function emitMulti(next: Set<string>) {
  emit('update:modelValue', [...next].join(props.separator))
}

function onPick(node: GroupCascaderNode, colIdx: number) {
  if (!node.isLeaf && node.children?.length) {
    path.value = [...path.value.slice(0, colIdx), node.value]
    if (!props.selectAnyLevel || props.multiple) return
    emitSingle(node.value)
    open.value = false
    return
  }
  if (node.isLeaf) {
    if (props.multiple) {
      toggleLeaf(node)
    } else {
      emitSingle(node.value)
      open.value = false
    }
  }
}

function toggleLeaf(node: GroupCascaderNode) {
  const next = new Set(selectedSet.value)
  if (next.has(node.value)) next.delete(node.value)
  else next.add(node.value)
  emitMulti(next)
}

function onInput(e: Event) {
  const v = (e.target as HTMLInputElement).value
  emitSingle(v)
}

function syncPanelPos() {
  const el = triggerRef.value
  if (!el) return
  const r = el.getBoundingClientRect()
  const maxW = Math.min(820, window.innerWidth - 16)
  // 贴触发框左侧 + 底部
  panelStyle.value = {
    position: 'fixed',
    top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
    left: `${r.left}px`,
    zIndex: '200',
    maxWidth: `${maxW}px`,
  }
}

async function openPanel() {
  if (props.disabled) return
  open.value = true
  await nextTick()
  syncPanelPos()
}

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

/** 点触发框空白处也展开（editable 时 caret 旁） */
function onTriggerClick(e: MouseEvent) {
  if (props.disabled) return
  const t = e.target as HTMLElement | null
  if (t?.closest('.vm-group-cascader__clear')) return
  if (props.editable) openPanel()
}

function clear() {
  emit('update:modelValue', '')
  path.value = []
}

function syncPathFromValue() {
  if (props.multiple) return
  const v = props.modelValue
  if (v == null || v === '' || Array.isArray(v)) {
    path.value = []
    return
  }
  const target = String(v)
  const found = findAncestorPath(props.tree || [], target)
  if (found) {
    path.value = found
    return
  }
  const parts = target.replace(/^modules\//, '').split('/')
  const keys: string[] = []
  let acc = 'modules'
  let level = props.tree || []
  for (let i = 0; i < parts.length - 1; i++) {
    acc += `/${parts[i]}`
    const node = level.find((n) => n.value === acc || n.label === parts[i])
    if (!node || node.isLeaf) break
    keys.push(node.value)
    level = node.children || []
  }
  path.value = keys
}

watch(
  () => [props.modelValue, props.tree],
  () => syncPathFromValue(),
  { immediate: true, deep: true },
)

watch(open, (v) => {
  if (v) syncPathFromValue()
})

watch(columns, async () => {
  if (!open.value) return
  await nextTick()
  syncPanelPos()
})

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-group-cascader__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)
})

defineExpose({ open: openPanel, close: () => (open.value = false) })
</script>

<style lang="scss">
.vm-group-cascader {
  position: relative;
  width: 100%;
  height: auto !important;
  min-height: var(--vm-control-height);
}

.vm-group-cascader__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);
  transition: border-color 0.15s ease;
  cursor: pointer;
  outline: none;
  /* 弹层触发：不展示打开/聚焦描边 */

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

.vm-group-cascader__input {
  flex: 1;
  min-width: 0;
  height: 100%;
  border: none !important;
  background: transparent !important;
  box-shadow: none !important;
  outline: none;
  color: var(--foreground);
  font-size: 14px;
  font-weight: 500;
  cursor: text;

  &::placeholder {
    color: var(--muted-foreground);
  }
}

.vm-group-cascader__btn {
  display: flex;
  flex: 1;
  min-width: 0;
  height: 100%;
  align-items: center;
  border: none;
  background: transparent;
  color: var(--foreground);
  font-size: 14px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
}

.vm-group-cascader__value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vm-group-cascader__tags {
  display: flex;
  flex: 1;
  min-width: 0;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}

.vm-group-cascader__tag {
  max-width: 100%;
  overflow: hidden;
  padding: 2px 8px;
  border-radius: 8px;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 12px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vm-group-cascader__more {
  flex-shrink: 0;
  padding: 2px 8px;
  border-radius: 8px;
  background: var(--muted);
  color: var(--muted-foreground);
  font-size: 12px;
  font-weight: 600;
}

.vm-group-cascader__ph {
  color: var(--muted-foreground);
}

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

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

.vm-group-cascader__caret {
  flex-shrink: 0;
  opacity: 0.45;
  font-size: 16px;
  cursor: pointer;
}

.vm-group-cascader__panel {
  display: flex;
  height: 280px;
  overflow-x: auto;
  overflow-y: hidden;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--card);
  box-shadow: var(--shadow-soft);
  pointer-events: auto;
}

.vm-group-cascader__col {
  display: flex;
  width: 260px;
  height: 100%;
  flex-shrink: 0;
  flex-direction: column;
  overflow: auto;
  overscroll-behavior: contain;
  border-right: 1px solid var(--border);

  &.is-leaf,
  &:last-child {
    border-right: none;
  }
}

.vm-group-cascader__item {
  display: flex;
  width: 100%;
  flex-shrink: 0;
  min-height: 28px;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 5px 10px;
  border: none;
  background: transparent;
  color: var(--foreground);
  font-size: 14px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  box-sizing: border-box;

  > span {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  > i {
    flex-shrink: 0;
    opacity: 0.45;
  }

  &:hover,
  &.is-active {
    background: var(--brand-soft);
    color: var(--brand);
  }

  .vm-group-cascader__tick {
    color: var(--brand);
    opacity: 1;
  }
}

.vm-group-cascader__check {
  display: inline-flex;
  flex-shrink: 0;

  input {
    accent-color: var(--brand);
  }
}

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