<template>
  <v-card
    border
    rounded="lg"
    class="file-dir-tree d-flex flex-column"
    :class="{ 'file-dir-tree--embedded': embedded }"
  >
    <v-card-title v-if="!embedded" class="text-subtitle-2 py-2 px-3 d-flex align-center">
      <v-icon size="18" class="mr-1">mdi-file-tree</v-icon>
      目录
      <v-spacer />
      <v-btn icon="mdi-refresh" size="x-small" variant="text" :disabled="!serverId" @click="reloadRoot" />
    </v-card-title>
    <div v-else class="file-dir-tree-embedded-bar d-flex align-center px-2 py-1">
      <v-icon size="16" class="mr-1 text-medium-emphasis">mdi-file-tree</v-icon>
      <span class="text-caption text-medium-emphasis">目录</span>
      <v-spacer />
      <v-btn icon="mdi-refresh" size="x-small" variant="text" :disabled="!serverId" @click="reloadRoot" />
    </div>
    <v-divider />
    <v-card-text class="file-dir-tree-body pa-0 flex-grow-1">
      <div v-if="!serverId" class="text-caption text-medium-emphasis pa-3">请先选择服务器</div>
      <v-progress-linear v-else-if="rootLoading" indeterminate />
      <div
        v-else-if="visibleNodes.length === 0"
        class="text-caption text-medium-emphasis pa-3"
      >
        暂无子目录（可点刷新）
      </div>
      <v-list v-else density="compact" nav class="py-0">
        <div
          v-for="node in visibleNodes"
          :key="node.path"
          class="file-dir-tree-row d-flex align-center"
          :style="{ paddingLeft: `${8 + node.depth * 14}px` }"
        >
          <button
            type="button"
            class="file-dir-tree-toggle"
            @click.stop="toggleNode(node)"
          >
            <v-progress-circular v-if="loadingPath === node.path" indeterminate size="14" width="2" />
            <v-icon v-else size="16">
              {{ expandedPaths.has(node.path) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
            </v-icon>
          </button>
          <button
            type="button"
            class="file-dir-tree-label text-body-2 text-truncate"
            :class="{ 'file-dir-tree-label--active': normalizeRemotePath(currentPath) === node.path }"
            @click="emit('navigate', node.path)"
            @contextmenu.prevent="emit('paste-target', node.path)"
          >
            <v-icon size="14" class="mr-1">mdi-folder-outline</v-icon>
            {{ node.name }}
          </button>
        </div>
      </v-list>
    </v-card-text>
  </v-card>
</template>

<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { http } from '@/api'
import { useSnackbar } from '@/composables/useSnackbar'
import { parseBrowseResponse } from '@/utils/fileBrowse'
import { DEFAULT_FILES_ROOT, joinRemotePath, normalizeRemotePath } from '@/utils/remotePath'
import type { FileEntry } from '@/types/api'

export interface DirTreeNode {
  path: string
  name: string
  depth: number
  hasChildren: boolean
  loaded: boolean
}

const props = defineProps<{
  serverId: number | null
  currentPath: string
  /** 树展示的根目录（默认宝塔站点根） */
  rootPath?: string
  /** 嵌入浮动编辑器侧栏：紧凑样式、填满高度 */
  embedded?: boolean
}>()

const snackbar = useSnackbar()

const effectiveRoot = computed(() =>
  normalizeRemotePath(props.rootPath || DEFAULT_FILES_ROOT),
)

const emit = defineEmits<{
  navigate: [path: string]
  'paste-target': [path: string]
}>()

const rootLoading = ref(false)
const loadingPath = ref<string | null>(null)
const expandedPaths = ref<Set<string>>(new Set())
const childrenByParent = ref<Map<string, DirTreeNode[]>>(new Map())

function replaceExpanded(mutator: (next: Set<string>) => void) {
  const next = new Set(expandedPaths.value)
  mutator(next)
  expandedPaths.value = next
}

function replaceChildren(mutator: (next: Map<string, DirTreeNode[]>) => void) {
  const next = new Map(childrenByParent.value)
  mutator(next)
  childrenByParent.value = next
}

function resetExpandedToRoot() {
  expandedPaths.value = new Set([effectiveRoot.value])
}

const visibleNodes = computed(() => {
  const out: DirTreeNode[] = []
  const walk = (parentPath: string, depth: number) => {
    const kids = childrenByParent.value.get(parentPath) ?? []
    for (const node of kids) {
      out.push({ ...node, depth })
      if (expandedPaths.value.has(node.path)) {
        walk(node.path, depth + 1)
      }
    }
  }
  walk(effectiveRoot.value, 0)
  return out
})

async function fetchDirectories(path: string): Promise<DirTreeNode[]> {
  if (!props.serverId) return []
  const res = await http.post('/sync/browse', {
    server_id: props.serverId,
    path: normalizeRemotePath(path),
  })
  const { items } = parseBrowseResponse(res)
  return items
    .filter((e: FileEntry) => e.type === 'directory')
    .sort((a, b) => a.name.localeCompare(b.name))
    .map((e) => ({
      path: joinRemotePath(path, e.name),
      name: e.name,
      depth: 0,
      hasChildren: true,
      loaded: false,
    }))
}

async function loadChildren(parentPath: string) {
  if (!props.serverId) return
  loadingPath.value = parentPath
  try {
    const children = await fetchDirectories(parentPath)
    replaceChildren((map) => {
      map.set(parentPath, children)
      for (const c of children) {
        if (!map.has(c.path)) map.set(c.path, [])
      }
    })
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '加载目录失败'
    snackbar(msg, 'error')
  } finally {
    loadingPath.value = null
  }
}

async function reloadRoot() {
  if (!props.serverId) return
  rootLoading.value = true
  childrenByParent.value = new Map()
  resetExpandedToRoot()
  const root = effectiveRoot.value
  try {
    await loadChildren(root)
    replaceChildren((map) => {
      for (const n of map.get(root) ?? []) {
        if (!map.has(n.path)) map.set(n.path, [])
      }
    })
  } finally {
    rootLoading.value = false
  }
}

async function toggleNode(node: DirTreeNode) {
  if (expandedPaths.value.has(node.path)) {
    replaceExpanded((s) => {
      s.delete(node.path)
    })
    return
  }
  if (!childrenByParent.value.has(node.path)) {
    await loadChildren(node.path)
  }
  replaceExpanded((s) => {
    s.add(node.path)
  })
}

function expandAncestors(path: string) {
  const norm = normalizeRemotePath(path)
  const root = effectiveRoot.value
  if (norm === root) return
  replaceExpanded((s) => {
    s.add(root)
    if (!norm.startsWith(root)) return
    const rel = norm.slice(root.length).replace(/^\//, '')
    if (!rel) return
    let acc = root
    for (const part of rel.split('/').filter(Boolean)) {
      acc = joinRemotePath(acc, part)
      s.add(acc)
    }
  })
}

watch(
  () => [props.serverId, effectiveRoot.value] as const,
  ([id]) => {
    if (id) void reloadRoot()
    else {
      childrenByParent.value = new Map()
      expandedPaths.value = new Set()
    }
  },
  { immediate: true },
)

async function ensurePathVisible(targetPath: string) {
  if (!props.serverId) return
  const norm = normalizeRemotePath(targetPath)
  const root = effectiveRoot.value
  expandAncestors(norm)
  if (!childrenByParent.value.has(root)) {
    await loadChildren(root)
  }
  if (norm === root) return
  if (!norm.startsWith(root)) return
  const rel = norm.slice(root.length).replace(/^\//, '')
  const parts = rel.split('/').filter(Boolean)
  let acc = root
  for (const part of parts) {
    if (!childrenByParent.value.has(acc)) await loadChildren(acc)
    acc = joinRemotePath(acc, part)
  }
}

watch(
  () => props.currentPath,
  (path) => {
    if (path) void ensurePathVisible(path)
  },
  { immediate: true },
)
</script>

<style scoped>
.file-dir-tree {
  min-height: 320px;
  max-height: min(56vh, 560px);
}
.file-dir-tree-body {
  overflow: auto;
}
.file-dir-tree-toggle {
  width: 22px;
  height: 22px;
  border: none;
  background: transparent;
  cursor: pointer;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.file-dir-tree-label {
  flex: 1;
  min-width: 0;
  text-align: left;
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 4px;
}
.file-dir-tree-label:hover {
  background: rgba(var(--v-theme-on-surface), 0.06);
}
.file-dir-tree-label--active {
  color: rgb(var(--v-theme-primary));
  font-weight: 600;
  background: rgba(var(--v-theme-primary), 0.1);
}
.file-dir-tree--embedded {
  min-height: 0;
  max-height: none;
  height: 100%;
  border: none;
  border-radius: 0;
}
.file-dir-tree--embedded .file-dir-tree-body {
  flex: 1;
  min-height: 120px;
  overflow-y: auto;
}
.file-dir-tree-embedded-bar {
  flex-shrink: 0;
  border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
</style>
