<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import TreeNode from './TreeNode.vue'
import type { CategoryNode, WeightDimKey, WeightDimMeta } from '../types/category'
import {
  DEFAULT_DIMS,
  buildWeightScale,
  collectWeights,
  filterTreeByDim,
  maxTreeDepth,
} from '../types/category'
import type { DemandGradeItem, DemandsByCategory } from '../types/demand'
import { downloadCategoryTreeHtml } from '../utils/exportCategoryTreeHtml'

const FULL_TREE_KEY = 'full' as const
type TreeTabKey = typeof FULL_TREE_KEY | WeightDimKey

const props = defineProps<{
  nodes: CategoryNode[]
  demandsByCategory: DemandsByCategory
  dims?: WeightDimMeta[]
  bizDt?: string | null
}>()

const DEFAULT_DEPTH = 3
const COL_WIDTH = 180
const COL_GAP = 36

const dimTabs = computed(() => (props.dims?.length ? props.dims : DEFAULT_DIMS))
const treeTabs = computed(() => [
  { key: FULL_TREE_KEY as TreeTabKey, label: '完整全局树' },
  ...dimTabs.value.map((d) => ({ key: d.key as TreeTabKey, label: d.label })),
])
const activeTab = ref<TreeTabKey>(FULL_TREE_KEY)

watch(
  treeTabs,
  (tabs) => {
    if (!tabs.some((d) => d.key === activeTab.value) && tabs[0]) {
      activeTab.value = tabs[0].key
    }
  },
  { immediate: true },
)

const isFullTree = computed(() => activeTab.value === FULL_TREE_KEY)
const activeDim = computed<WeightDimKey | null>(() =>
  isFullTree.value ? null : (activeTab.value as WeightDimKey),
)

const displayNodes = computed(() => {
  if (!activeDim.value) return props.nodes
  return filterTreeByDim(props.nodes, activeDim.value)
})

const weightScale = computed(() => {
  if (!activeDim.value) return []
  return buildWeightScale(collectWeights(displayNodes.value, activeDim.value))
})

const expandDepth = ref(DEFAULT_DEPTH)
const expandedMap = ref<Record<number, true>>({})
const collapsedMap = ref<Record<number, true>>({})

const drawerOpen = ref(false)
const drawerCategoryId = ref<number | null>(null)
const drawerCategoryName = ref('')
const drawerItems = ref<DemandGradeItem[]>([])

const treePanelRef = ref<HTMLElement | null>(null)
const isPanning = ref(false)
let panStartX = 0
let panStartY = 0
let panScrollLeft = 0
let panScrollTop = 0

const treeMaxDepth = computed(() => maxTreeDepth(displayNodes.value))

const depthOptions = computed(() => {
  const max = Math.max(treeMaxDepth.value, DEFAULT_DEPTH)
  return Array.from({ length: max }, (_, i) => i + 1)
})

/** 顶部层级栏：跟当前展示深度对齐；手动再展开时扩到整棵树最大层。 */
const headerLevels = computed(() => {
  let count = Math.max(expandDepth.value, 1)
  if (Object.keys(expandedMap.value).length > 0) {
    count = Math.max(count, treeMaxDepth.value)
  }
  if (treeMaxDepth.value > 0) {
    count = Math.min(count, treeMaxDepth.value)
  }
  return Array.from({ length: count }, (_, i) => i + 1)
})

function resetExpandState() {
  expandedMap.value = {}
  collapsedMap.value = {}
}

watch(expandDepth, resetExpandState)

watch(activeTab, () => {
  expandDepth.value = DEFAULT_DEPTH
  resetExpandState()
})

function findDepth(nodes: CategoryNode[], id: number, depth: number): number | null {
  for (const node of nodes) {
    if (node.id === id) return depth
    const found = findDepth(node.children, id, depth + 1)
    if (found != null) return found
  }
  return null
}

function toggleNode(id: number) {
  const wasCollapsed = !!collapsedMap.value[id]
  const wasExpanded = !!expandedMap.value[id]

  const nextExpanded = { ...expandedMap.value }
  const nextCollapsed = { ...collapsedMap.value }
  delete nextExpanded[id]
  delete nextCollapsed[id]

  if (wasCollapsed) {
    nextExpanded[id] = true
  } else if (wasExpanded) {
    nextCollapsed[id] = true
  } else {
    const depth = findDepth(displayNodes.value, id, 1)
    const autoOpen = depth != null && depth < expandDepth.value
    if (autoOpen) {
      nextCollapsed[id] = true
    } else {
      nextExpanded[id] = true
    }
  }

  expandedMap.value = nextExpanded
  collapsedMap.value = nextCollapsed
}

function expandAll() {
  expandDepth.value = treeMaxDepth.value || 1
}

function onInspect(payload: {
  categoryId: number
  categoryName: string
  items: DemandGradeItem[]
}) {
  if (drawerOpen.value && drawerCategoryId.value === payload.categoryId) {
    closeDrawer()
    return
  }
  drawerCategoryId.value = payload.categoryId
  drawerCategoryName.value = payload.categoryName
  drawerItems.value = payload.items
  drawerOpen.value = true
}

function closeDrawer() {
  drawerOpen.value = false
  drawerCategoryId.value = null
}

function exportHtml() {
  downloadCategoryTreeHtml({
    nodes: props.nodes,
    dims: dimTabs.value,
    bizDt: props.bizDt ?? null,
    demandsByCategory: props.demandsByCategory,
  })
}

function onPanStart(e: MouseEvent) {
  const el = treePanelRef.value
  if (!el || e.button !== 0) return
  const target = e.target as HTMLElement | null
  if (target?.closest('button, select, a, input')) return

  isPanning.value = true
  panStartX = e.clientX
  panStartY = e.clientY
  panScrollLeft = el.scrollLeft
  panScrollTop = el.scrollTop
  el.classList.add('is-panning')
  window.addEventListener('mousemove', onPanMove)
  window.addEventListener('mouseup', onPanEnd)
}

function onPanMove(e: MouseEvent) {
  const el = treePanelRef.value
  if (!el || !isPanning.value) return
  el.scrollLeft = panScrollLeft - (e.clientX - panStartX)
  el.scrollTop = panScrollTop - (e.clientY - panStartY)
}

function onPanEnd() {
  isPanning.value = false
  treePanelRef.value?.classList.remove('is-panning')
  window.removeEventListener('mousemove', onPanMove)
  window.removeEventListener('mouseup', onPanEnd)
}
</script>

<template>
  <div
    class="category-tree"
    :style="{ '--col-width': `${COL_WIDTH}px`, '--col-gap': `${COL_GAP}px` }"
  >
    <header class="toolbar">
      <div class="title-row">
        <h1>全局分类树</h1>
        <span v-if="bizDt" class="biz-dt">biz_dt {{ bizDt }} · avg</span>
        <span v-else class="biz-dt warn">暂无权重数据</span>
      </div>
      <div class="controls">
        <label class="control">
          <span>展开到第</span>
          <select v-model.number="expandDepth">
            <option v-for="n in depthOptions" :key="n" :value="n">{{ n }}</option>
          </select>
          <span>层</span>
        </label>
        <button type="button" class="btn" @click="expandAll">全部展开</button>
        <button type="button" class="btn btn-secondary" @click="exportHtml">导出 HTML</button>
        <span class="hint">
          共 {{ treeMaxDepth }} 层 · 空白处按住拖动可平移 · Shift+滚轮左右滚
        </span>
      </div>
    </header>

    <div class="dim-bar">
      <div class="tabs" role="tablist" aria-label="热度维度">
        <button
          v-for="dim in treeTabs"
          :key="dim.key"
          type="button"
          role="tab"
          class="tab"
          :class="{ active: activeTab === dim.key }"
          :aria-selected="activeTab === dim.key"
          @click="activeTab = dim.key"
        >
          {{ dim.label }}
        </button>
      </div>
      <div v-if="!isFullTree" class="legend" aria-hidden="true">
        <span class="legend-label">冷</span>
        <div class="legend-bar" />
        <span class="legend-label">热</span>
      </div>
    </div>

    <div v-if="!displayNodes.length" class="empty">
      {{ isFullTree ? '暂无分类数据' : '该维度暂无有数据的节点' }}
    </div>
    <div
      v-else
      ref="treePanelRef"
      class="tree-panel"
      @mousedown="onPanStart"
    >
      <div class="tree-scroll-inner">
        <div class="level-headers">
          <div
            v-for="n in headerLevels"
            :key="n"
            class="level-header"
          >
            第 {{ n }} 层
          </div>
        </div>
        <div class="forest">
          <TreeNode
            v-for="node in displayNodes"
            :key="node.id"
            :node="node"
            :depth="1"
            :expand-depth="expandDepth"
            :expanded-map="expandedMap"
            :collapsed-map="collapsedMap"
            :demands-by-category="demandsByCategory"
            :active-dim="activeDim"
            :weight-scale="weightScale"
            :inspect-category-id="drawerCategoryId"
            :inspect-category-name="drawerCategoryName"
            :inspect-items="drawerItems"
            @toggle="toggleNode"
            @inspect="onInspect"
            @close-inspect="closeDrawer"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.category-tree {
  display: flex;
  flex-direction: column;
  gap: 12px;
  height: 100%;
  min-height: 0;
  max-width: 100%;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px 24px;
  padding-bottom: 12px;
  border-bottom: 1px solid #e2e8f0;
  flex-shrink: 0;
}

.title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px 14px;
}

.toolbar h1 {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  color: #0f172a;
  letter-spacing: -0.02em;
}

.biz-dt {
  font-size: 12px;
  color: #64748b;
  font-variant-numeric: tabular-nums;
}

.biz-dt.warn {
  color: #b45309;
}

.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
}

.control {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: #334155;
}

.control select {
  height: 32px;
  padding: 0 8px;
  border: 1px solid #cbd5e1;
  border-radius: 6px;
  background: #fff;
  font-size: 14px;
  color: #0f172a;
}

.btn {
  height: 32px;
  padding: 0 12px;
  border: 1px solid #334155;
  border-radius: 6px;
  background: #0f172a;
  color: #fff;
  font-size: 13px;
  cursor: pointer;
}

.btn:hover {
  background: #1e293b;
}

.btn-secondary {
  background: #fff;
  color: #0f172a;
  border-color: #cbd5e1;
}

.btn-secondary:hover {
  background: #f8fafc;
  border-color: #94a3b8;
}

.hint {
  font-size: 12px;
  color: #94a3b8;
}

.dim-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 16px;
  flex-shrink: 0;
}

.tabs {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px;
  border: 1px solid #e2e8f0;
  border-radius: 10px;
  background: #f8fafc;
}

.tab {
  height: 32px;
  padding: 0 12px;
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: #475569;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
}

.tab:hover {
  color: #0f172a;
  background: #fff;
}

.tab.active {
  background: #0f172a;
  border-color: #0f172a;
  color: #fff;
}

.legend {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.legend-label {
  font-size: 11px;
  color: #94a3b8;
}

.legend-bar {
  width: 120px;
  height: 10px;
  border-radius: 999px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  background: linear-gradient(
    90deg,
    rgb(239, 68, 68) 0%,
    rgb(255, 255, 255) 50%,
    rgb(34, 197, 94) 100%
  );
}

.tree-panel {
  flex: 1;
  min-height: 0;
  min-width: 0;
  width: 100%;
  overflow: scroll;
  overscroll-behavior: contain;
  border: 1px solid #e2e8f0;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.55);
  cursor: grab;
  scrollbar-gutter: stable;
}

.tree-panel.is-panning {
  cursor: grabbing;
  user-select: none;
}

.tree-panel::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

.tree-panel::-webkit-scrollbar-thumb {
  background: #94a3b8;
  border-radius: 8px;
  border: 2px solid transparent;
  background-clip: content-box;
}

.tree-panel::-webkit-scrollbar-track {
  background: #e2e8f0;
  border-radius: 8px;
}

.tree-scroll-inner {
  display: inline-block;
  min-width: 100%;
  padding: 8px 12px 24px;
  vertical-align: top;
}

.level-headers {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  position: sticky;
  top: 0;
  z-index: 2;
  background: rgba(248, 250, 252, 0.96);
  backdrop-filter: blur(6px);
  border-bottom: 1px solid #e2e8f0;
  margin-bottom: 12px;
  padding: 8px 0;
  width: max-content;
  min-width: 100%;
}

.level-header {
  width: var(--col-width, 180px);
  flex-shrink: 0;
  margin-right: var(--col-gap, 36px);
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: #475569;
  letter-spacing: 0.02em;
}

.level-header:last-child {
  margin-right: 0;
}

.forest {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  width: max-content;
  min-width: 100%;
}

.empty {
  padding: 48px;
  text-align: center;
  color: #94a3b8;
  font-size: 15px;
}
</style>
