<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 />
      <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 { 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
  /** 嵌入浮动编辑器侧栏：紧凑样式、填满高度 */
  embedded?: boolean
}>()

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([DEFAULT_FILES_ROOT]))
const childrenByParent = ref<Map<string, DirTreeNode[]>>(new Map())

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(DEFAULT_FILES_ROOT, 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)
    childrenByParent.value.set(parentPath, children)
    const existing = childrenByParent.value
    children.forEach((c) => {
      if (!existing.has(c.path)) existing.set(c.path, [])
    })
  } finally {
    loadingPath.value = null
  }
}

async function reloadRoot() {
  if (!props.serverId) return
  rootLoading.value = true
  childrenByParent.value = new Map()
  expandedPaths.value = new Set([DEFAULT_FILES_ROOT])
  try {
    await loadChildren(DEFAULT_FILES_ROOT)
    childrenByParent.value.get(DEFAULT_FILES_ROOT)?.forEach((n) => {
      childrenByParent.value.set(n.path, [])
    })
  } finally {
    rootLoading.value = false
  }
}

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

function expandAncestors(path: string) {
  const norm = normalizeRemotePath(path)
  if (norm === DEFAULT_FILES_ROOT) return
  const parts = norm.split('/').filter(Boolean)
  let acc = ''
  for (const part of parts) {
    acc = acc ? `${acc}/${part}` : `/${part}`
    expandedPaths.value.add(acc)
  }
  expandedPaths.value.add(DEFAULT_FILES_ROOT)
}

watch(
  () => props.serverId,
  (id) => {
    if (id) 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)
  expandAncestors(norm)
  if (!childrenByParent.value.has(DEFAULT_FILES_ROOT)) {
    await loadChildren(DEFAULT_FILES_ROOT)
  }
  if (norm === DEFAULT_FILES_ROOT) return
  const prefix = DEFAULT_FILES_ROOT.endsWith('/') ? DEFAULT_FILES_ROOT : DEFAULT_FILES_ROOT
  const rel = norm.startsWith(prefix) ? norm.slice(prefix.length) : norm
  const parts = rel.split('/').filter(Boolean)
  let acc = DEFAULT_FILES_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)
  },
)
</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: 0;
}
.file-dir-tree-embedded-bar {
  flex-shrink: 0;
  border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
</style>
