<template>
  <!--
    文件树(Batch 10)— CSS 缩进树形展示(不引组件库,轻量)。
    懒加载:点击文件夹展开时 emit('load-children', path) 通知父组件拉子目录。
    Git 状态标记:仅文件显示(文件夹无标记),M 橙/A 绿/?? 灰。
  -->
  <div class="file-tree">
    <!-- 加载中 -->
    <div v-if="loading && entries.length === 0" class="tree-loading">
      <span class="spinner"></span>{{ $t('fileExplorer.loading') }}
    </div>

    <!-- 错误 -->
    <div v-else-if="error" class="tree-error">⚠ {{ error }}</div>

    <!-- 空目录(展开后子条目为空) -->
    <div v-else-if="entries.length === 0" class="tree-empty">
      <span class="tree-empty-icon">📭</span>
      <span>{{ $t('fileExplorer.emptyDir') }}</span>
    </div>

    <!-- 条目列表 -->
    <ul v-else class="tree-list">
      <li
        v-for="entry in entries"
        :key="entry.path"
        class="tree-item"
        :class="{
          'is-dir': entry.is_dir,
          'is-file': !entry.is_dir,
          'is-selected': !entry.is_dir && entry.path === selectedPath,
        }"
        :style="{ paddingLeft: indent + 'px' }"
      >
        <!-- 文件夹:点击切换展开 -->
        <div
          v-if="entry.is_dir"
          class="tree-row tree-row-dir"
          :title="entry.path"
          @click="toggleDir(entry)"
        >
          <span class="expand-icon">{{ expandedPaths.has(entry.path) ? '▾' : '▸' }}</span>
          <span class="file-icon">📁</span>
          <span class="file-name">{{ entry.name }}</span>
          <!-- 空目录标:has_children 由后端列目录时即时统计,未展开即可见「空」,
               避免用户误以为目录没加载出来(展开后才显空态) -->
          <span
            v-if="entry.has_children === false"
            class="tree-empty-badge"
            :title="$t('fileExplorer.emptyDir')"
          >{{ $t('fileExplorer.emptyDirMark') }}</span>
          <!-- B3[PD-P1-6]:展开失败 → 行内 ⚠(title 显原因),点击重试;不设整树 error,不炸其他已展开节点 -->
          <span
            v-if="toggleErrors.has(entry.path)"
            class="dir-toggle-error"
            :title="$t('fileExplorer.loadFailed') + ': ' + toggleErrors.get(entry.path)"
          >⚠</span>
        </div>

        <!-- 文件:点击通知父组件加载预览 -->
        <div
          v-else
          class="tree-row tree-row-file"
          :title="entry.path"
          @click="emit('file-select', entry.path)"
        >
          <span class="expand-icon placeholder"></span>
          <span class="file-icon">📄</span>
          <span class="file-icon file-icon-ext" v-html="fileIcon(entry.name)"></span>
          <span class="file-name">{{ entry.name }}</span>
          <!-- Git 状态标记(仅文件):M 橙 / A 绿 / ?? 灰 -->
          <span
            v-if="entry.git_status"
            class="git-badge"
            :class="gitStatusClass(entry.git_status)"
          >{{ gitStatusLabel(entry.git_status) }}</span>
        </div>

        <!-- 展开的子目录(递归子树;子节点缩进 +1 级) -->
        <FileTree
          v-if="entry.is_dir && expandedPaths.has(entry.path)"
          :module-id="moduleId"
          :sub-path="entry.path"
          :indent="indent + 16"
          :expanded-paths="expandedPaths"
          :loaded-children="loadedChildren"
          :selected-path="selectedPath"
          @toggle-dir="(p, e) => emit('toggle-dir', p, e)"
          @file-select="(p) => emit('file-select', p)"
          @load-children="(p) => emit('load-children', p)"
        />
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
/**
 * 递归文件树组件。
 *
 * 状态管理策略(避免子组件各自维护 loading/expanded 导致状态分散):
 * - expandedPaths / loadedChildren 由顶层 FileExplorer 持有,作为 props 透传;
 *   子组件只负责 emit('toggle-dir', path, entries) / emit('load-children', path),
 *   实际的网络请求与状态更新统一在 FileExplorer 中处理。
 * - 本组件自身的 loading/entries 仅用于"首次挂载时拉取自己 sub_path 的根条目";
 *   递归子树复用同一份 props/事件,且 loadEntries 会先查 loadedChildren 缓存命中即复用,
 *   避免父级 toggleDir 已拉取后子树 onMounted 再次重复请求同目录。
 *
 * 这样设计的好处:刷新(refresh)只需 FileExplorer 清空 loadedChildren 重拉根,
 * 所有展开的子树因 expandedPaths 被清而卸载,下次展开重新拉取,无脏数据。
 */
import { ref, reactive, watch, onMounted } from 'vue'
import { moduleApi, type FileTreeEntry } from '@/api/module'

const props = withDefaults(
  defineProps<{
    moduleId: string
    /** 相对工程根的子路径;根目录传空串。 */
    subPath?: string
    /** 当前缩进(px);根 = 12,每层 +16。 */
    indent?: number
    /** 展开的目录路径集合(顶层持有,跨子树共享)。 */
    expandedPaths: Set<string>
    /** 已加载子条目的目录路径映射(顶层持有,缓存避免重复请求)。 */
    loadedChildren: Map<string, FileTreeEntry[]>
    /** 当前选中文件路径(高亮)。 */
    selectedPath?: string | null
  }>(),
  {
    subPath: '',
    indent: 12,
    selectedPath: null,
  },
)

const emit = defineEmits<{
  (e: 'toggle-dir', path: string, entries: FileTreeEntry[]): void
  (e: 'file-select', path: string): void
  (e: 'load-children', path: string): void
}>()

const entries = ref<FileTreeEntry[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
// B3[PD-P1-6]:目录展开失败的行内错误态(路径 → 错误信息)。之前把 toggleDir 失败设到 error.value,
// 会让整个子树渲染分支(v-else-if="error")替换成错误文案,一次子目录失败就炸掉已展开的其他节点。
// 改为按 path 记录,条目行内显 ⚠ + title 原因,点击重试;成功加载即清除。
const toggleErrors = reactive(new Map<string, string>())

/** 拉取当前 subPath 的条目列表。 */
async function loadEntries() {
  // moduleId 为空时不发起请求(工程列表还在加载中)
  if (!props.moduleId) return
  // 去重:递归子树实例挂载时,父级 toggleDir 通常已为本目录拉取并缓存。
  // 命中缓存则直接复用,不再发同样的请求(避免 onMounted 与 toggleDir 双重拉取)。
  const cacheKey = props.subPath || ''
  const cached = props.loadedChildren.get(cacheKey)
  if (cached) {
    entries.value = cached
    return
  }
  loading.value = true
  error.value = null
  try {
    const res = await moduleApi.getModuleFileTree(props.moduleId, props.subPath || undefined)
    entries.value = res.entries
    // 缓存到顶层 loadedChildren(供 toggle 判断是否已有数据,避免重复请求)。
    props.loadedChildren.set(cacheKey, res.entries)
  } catch (e) {
    error.value = e instanceof Error ? e.message : String(e)
  } finally {
    loading.value = false
  }
}

/** 点击文件夹:切换展开态。 */
async function toggleDir(entry: FileTreeEntry) {
  if (!entry.is_dir) return
  // moduleId 为空时不发起请求(工程列表还在加载中)
  if (!props.moduleId) return
  if (props.expandedPaths.has(entry.path)) {
    // 收起:仅移除展开标记(loadedChildren 缓存保留,下次展开秒开)。
    emit('toggle-dir', entry.path, [])
  } else {
    // 展开:若未加载过则先拉取并缓存(去重——只在这里拉一次;
    // 递归子树实例 onMounted 时会复用此缓存,不重复请求同目录)。
    if (!props.loadedChildren.has(entry.path)) {
      emit('load-children', entry.path)
      try {
        loading.value = true
        const res = await moduleApi.getModuleFileTree(props.moduleId, entry.path)
        props.loadedChildren.set(entry.path, res.entries)
        toggleErrors.delete(entry.path)
      } catch (e) {
        // B3[PD-P1-6]:行内错误态(该目录 ⚠ + title 原因),不设整树 error;不展开,点击可重试
        toggleErrors.set(entry.path, e instanceof Error ? e.message : String(e))
        return
      } finally {
        loading.value = false
      }
    }
    emit('toggle-dir', entry.path, props.loadedChildren.get(entry.path) ?? [])
  }
}

/** Git 状态码 → CSS class(M* 橙 / A* 绿 / ?? 灰 / D 红 / 其他默认)。 */
function gitStatusClass(status: string): string {
  const x = status.trim()
  if (x === '??') return 'git-untracked'
  if (x.startsWith('A')) return 'git-added'
  if (x.startsWith('M')) return 'git-modified'
  if (x.startsWith('D')) return 'git-deleted'
  if (x.startsWith('R')) return 'git-modified'
  return 'git-other'
}

/** Git 状态码 → 简短标签字符(显示在文件名右侧)。 */
function gitStatusLabel(status: string): string {
  const x = status.trim()
  if (x === '??') return 'U'
  if (x.startsWith('A')) return 'A'
  if (x.startsWith('M')) return 'M'
  if (x.startsWith('D')) return 'D'
  if (x.startsWith('R')) return 'R'
  return x.charAt(0) || '?'
}

/** 文件后缀 → SVG 图标映射(纯文本,安全注入 v-html)。 */
const FILE_ICONS: Record<string, string> = {
  // 代码
  rs: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#DE5842"/><path d="M8 8h8M8 12h8M8 16h5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>',
  ts: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#3178C6"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">TS</text></svg>',
  tsx: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#3178C6"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">TSX</text></svg>',
  js: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#F7DF1E"/><text x="12" y="16" text-anchor="middle" fill="#000" font-size="11" font-weight="bold">JS</text></svg>',
  jsx: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#F7DF1E"/><text x="12" y="16" text-anchor="middle" fill="#000" font-size="10" font-weight="bold">JSX</text></svg>',
  vue: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#42B883"/><path d="M7 7l5 10 5-10h-3l-2 4-2-4H7z" fill="#fff"/></svg>',
  // 样式
  css: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#2965F1"/><path d="M7 7l1 10 4 1.5 4-1.5 1-10H7z" fill="#fff" opacity="0.9"/></svg>',
  scss: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#C6538C"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="9" font-weight="bold">SCSS</text></svg>',
  html: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#E34F26"/><path d="M8 7l-.5 5.5h8.5l-.5 4-3.5 1-3.5-1" stroke="#fff" stroke-width="1.2" fill="none"/><path d="M12 12h3l-.5-3H12" stroke="#fff" stroke-width="1.2" fill="none"/></svg>',
  // 配置
  json: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#5C5C5C"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="8" font-weight="bold">{ }</text></svg>',
  toml: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#8B5CF6"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="9" font-weight="bold">TOML</text></svg>',
  yaml: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#6BB5A0"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="9" font-weight="bold">YM</text></svg>',
  yml: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#6BB5A0"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="9" font-weight="bold">YM</text></svg>',
  // 文档
  md: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#083FA1"/><path d="M7 15V9l2.5 3L12 9v6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>',
  // Shell
  sh: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#4EAA25"/><path d="M8 9l3 3-3 3M13 15h3" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>',
  bash: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#4EAA25"/><path d="M8 9l3 3-3 3M13 15h3" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>',
  // 构建
  dockerfile: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#2496ED"/><path d="M7 12h1M9 12h1M11 12h1M13 12h1M15 12h1M17 10h-2v2h2v-2zM10 9H8v2h2V9z" stroke="#fff" stroke-width="1" fill="none"/></svg>',
  sql: '<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" fill="#E38C00"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="8" font-weight="bold">SQL</text></svg>',
}

/** 文件后缀 → 图标 HTML(v-html 安全注入)。 */
function fileIcon(name: string): string {
  const ext = name.split('.').pop()?.toLowerCase() ?? ''
  return FILE_ICONS[ext] || ''
}

onMounted(loadEntries)

// moduleId 变化(切换工程)时重拉。
watch(() => props.moduleId, loadEntries)

// 顶层强制刷新(loadedChildren 被清时,各子树自重拉):监听自身缓存失效。
watch(
  () => props.loadedChildren.has(props.subPath || ''),
  (has) => {
    if (!has && entries.value.length > 0) {
      // moduleId 为空时不发起请求
      if (!props.moduleId) return
      loadEntries()
    }
  },
)
</script>

<style scoped>
.file-tree {
  font-size: 13px;
  color: var(--df-text);
  user-select: none;
}

.tree-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.tree-item {
  display: block;
}

.tree-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  cursor: pointer;
  border-radius: var(--df-radius-sm, 4px);
  transition: background 0.12s;
}

.tree-row:hover {
  background: var(--df-bg-card, rgba(255, 255, 255, 0.04));
}

.tree-row-file.is-selected {
  background: var(--df-accent, #3a8);
  color: #fff;
}

.tree-row-file.is-selected .git-badge {
  background: rgba(255, 255, 255, 0.2);
  color: #fff;
}

.expand-icon {
  width: 12px;
  display: inline-block;
  text-align: center;
  color: var(--df-text-dim);
  font-size: 10px;
}

.expand-icon.placeholder {
  visibility: hidden;
}

.file-icon {
  font-size: 14px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

.file-icon-ext {
  margin-left: -18px;
}

.file-icon-ext svg {
  width: 18px;
  height: 18px;
  display: block;
}

.file-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* B3[PD-P1-6]:目录展开失败的行内 ⚠(flex-shrink 防挤压,危险色) */
.dir-toggle-error {
  flex-shrink: 0;
  font-size: 11px;
  color: #e05050;
  cursor: pointer;
}

/* 空目录小标(目录行右侧,未展开即可见;flex-shrink 防挤压) */
.tree-empty-badge {
  flex-shrink: 0;
  /* 文件名后稍靠右一点(不贴名、也不推到最右):固定小间隔即可 */
  margin-left: 8px;
  font-size: 10px;
  line-height: 1.4;
  padding: 1px 5px;
  border-radius: 8px;
  background: rgba(150, 150, 150, 0.14);
  color: var(--df-text-dim);
}

/* 展开后空态图标(📭,与文字同行) */
.tree-empty-icon {
  font-size: 14px;
  line-height: 1;
}

/* Git 状态徽章 */
.git-badge {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 8px;
  min-width: 14px;
  text-align: center;
}

.git-modified {
  background: rgba(255, 165, 0, 0.18);
  color: #f0a020;
}

.git-added {
  background: rgba(60, 180, 80, 0.18);
  color: #4caf50;
}

.git-untracked {
  background: rgba(150, 150, 150, 0.18);
  color: #999;
}

.git-deleted {
  background: rgba(220, 60, 60, 0.18);
  color: #e05050;
}

.git-other {
  background: rgba(100, 150, 220, 0.18);
  color: #6a9adc;
}

/* 加载/错误/空态 */
.tree-loading,
.tree-error,
.tree-empty {
  padding: 12px 8px;
  color: var(--df-text-dim);
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 6px;
}

.tree-error {
  color: #e05050;
}

.spinner {
  width: 12px;
  height: 12px;
  border: 1.5px solid var(--df-border, #444);
  border-top-color: var(--df-accent, #3a8);
  border-radius: 50%;
  animation: df-spin 0.8s linear infinite;
  display: inline-block;
}

@keyframes df-spin {
  to {
    transform: rotate(360deg);
  }
}
</style>
