<template>
  <div class="vm-check-tree">
    <Input
      v-if="filterable"
      v-model="keyword"
      class="vm-check-tree__filter"
      :placeholder="crudLabel('filterKeyword')"
    />
    <div class="vm-check-tree__box vm-scroll" :style="boxStyle">
      <div
        v-for="row in visibleRows"
        :key="String(row.id)"
        class="vm-check-tree__row"
        :class="{ 'is-hidden': !row.visible }"
      >
        <div
          class="vm-check-tree__main"
          :style="{ paddingLeft: `${8 + row.depth * 16}px` }"
        >
          <button
            v-if="row.hasChildren"
            type="button"
            class="vm-check-tree__expand"
            :class="{ 'is-open': expanded.has(row.id) }"
            @click="toggleExpand(row.id)"
          >
            <i class="ri-arrow-right-s-line" />
          </button>
          <span v-else class="vm-check-tree__expand is-spacer" />
          <Checkbox
            :model-value="checked.has(row.id)"
            :disabled="disabled"
            @update:model-value="(v) => onToggle(row.id, Boolean(v))"
          />
          <span class="vm-check-tree__label">{{ row.label }}</span>
        </div>
        <span v-if="row.meta" class="vm-check-tree__meta">{{ row.meta }}</span>
      </div>
      <p v-if="!visibleRows.length" class="vm-check-tree__empty">{{ crudLabel('empty') }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { crudLabel } from '../crud/style'
import { ref, computed, watch } from 'vue'
import type { CheckTreeNode } from '../../../typings/admin/comm/check-tree'

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

type FlatRow = CheckTreeNode & {
  depth: number
  hasChildren: boolean
  visible: boolean
}

const props = withDefaults(
  defineProps<{
    nodes?: CheckTreeNode[]
    modelValue?: Array<number | string>
    filterable?: boolean
    /** 父子联动（关联上下级） */
    cascade?: boolean
    disabled?: boolean
    /** 树区域最大高度 */
    maxHeight?: number | string
    /** 树区域固定高度（与 maxHeight 同时生效时优先固定高度） */
    height?: number | string
    /** 默认全部展开（为 true 时忽略 defaultExpandLevel） */
    defaultExpandAll?: boolean
    /** 默认展开到第几级（1=仅根节点，2=展开根的下级） */
    defaultExpandLevel?: number
  }>(),
  {
    nodes: () => [],
    modelValue: () => [],
    filterable: true,
    cascade: true,
    disabled: false,
    maxHeight: 320,
    defaultExpandAll: false,
    defaultExpandLevel: 2,
  },
)

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

const keyword = ref('')
const expanded = ref(new Set<string | number>())

const boxStyle = computed(() => {
  const style: Record<string, string> = {}
  if (props.height != null) {
    const h = typeof props.height === 'number' ? `${props.height}px` : props.height
    style.height = h
    style.maxHeight = h
  } else {
    const h = props.maxHeight
    style.maxHeight = typeof h === 'number' ? `${h}px` : h
  }
  return style
})

const checked = computed(() => new Set(props.modelValue))

const descendantMap = computed(() => {
  const map = new Map<string | number, Array<number | string>>()
  const walk = (node: CheckTreeNode): Array<number | string> => {
    const kids = node.children || []
    let ids: Array<number | string> = []
    for (const c of kids) {
      ids.push(c.id)
      ids = ids.concat(walk(c))
    }
    map.set(node.id, ids)
    return ids
  }
  for (const n of props.nodes) walk(n)
  return map
})

function collectExpandable(nodes: CheckTreeNode[], into: Set<string | number>) {
  for (const n of nodes) {
    if (n.children?.length) {
      into.add(n.id)
      collectExpandable(n.children, into)
    }
  }
}

/** 展开到指定层级：level=1 仅根可见；level=2 根展开、二级可见 */
function collectExpandToLevel(
  nodes: CheckTreeNode[],
  depth: number,
  maxLevel: number,
  into: Set<string | number>,
) {
  if (depth >= maxLevel - 1) return
  for (const n of nodes) {
    if (!n.children?.length) continue
    into.add(n.id)
    collectExpandToLevel(n.children, depth + 1, maxLevel, into)
  }
}

function syncExpanded() {
  const next = new Set<string | number>()
  const nodes = props.nodes || []
  if (props.defaultExpandAll) {
    collectExpandable(nodes, next)
  } else {
    const level = Math.max(1, props.defaultExpandLevel ?? 2)
    collectExpandToLevel(nodes, 0, level, next)
  }
  expanded.value = next
}

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

function flatten(
  nodes: CheckTreeNode[],
  depth = 0,
  out: FlatRow[] = [],
): FlatRow[] {
  for (const n of nodes) {
    const kids = n.children || []
    const hasChildren = kids.length > 0
    out.push({
      ...n,
      depth,
      hasChildren,
      visible: true,
    })
    if (hasChildren && expanded.value.has(n.id)) {
      flatten(kids, depth + 1, out)
    }
  }
  return out
}

function matchFilter(node: CheckTreeNode, q: string): boolean {
  if (!q) return true
  const hay = `${node.label} ${node.meta || ''}`.toLowerCase()
  if (hay.includes(q)) return true
  return (node.children || []).some((c) => matchFilter(c, q))
}

const visibleRows = computed(() => {
  const q = keyword.value.trim().toLowerCase()
  const rows = flatten(props.nodes || [])
  if (!q) return rows
  return rows.map((r) => ({
    ...r,
    visible: matchFilter(r, q),
  }))
})

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

function emitChecked(next: Set<string | number>) {
  emit('update:modelValue', [...next])
}

function onToggle(id: string | number, on: boolean) {
  if (props.disabled) return
  const next = new Set(checked.value)
  if (on) next.add(id)
  else next.delete(id)

  if (props.cascade) {
    const desc = descendantMap.value.get(id) || []
    if (on) {
      for (const d of desc) next.add(d)
    } else {
      for (const d of desc) next.delete(d)
    }
  }

  emitChecked(next)
}
</script>

<style lang="scss" scoped>
.vm-check-tree {
  display: grid;
  gap: 8px;
  width: 100%;
  min-width: 0;
  min-height: 0;
}

.vm-check-tree__filter {
  width: 100%;
  height: var(--vm-control-height);
  border-radius: var(--vm-control-radius);
  background: var(--muted);
  border-color: transparent;
}

.vm-check-tree__box {
  min-height: 0;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: var(--vm-control-radius);
  background: var(--card);
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.vm-check-tree__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 28px;
  border-radius: 8px;

  &.is-hidden {
    display: none;
  }

  &:hover {
    background: var(--muted);
  }
}

.vm-check-tree__main {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 6px;
}

.vm-check-tree__expand {
  display: inline-flex;
  width: 20px;
  height: 20px;
  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: 14px;
    transition: transform 0.15s ease;
  }

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

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

.vm-check-tree__label {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--foreground);
}

.vm-check-tree__meta {
  justify-self: start;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  text-align: left;
  color: var(--muted-foreground);
}

.vm-check-tree__empty {
  margin: 12px 0;
  text-align: center;
  font-size: 12px;
  color: var(--muted-foreground);
}
</style>
