<template>
  <!--
    文件内容预览。
    - 文本/代码: highlight.js 语法高亮(按文件后缀自动选语言),git 变更可切 diff 视图
    - 图片: <img> 展示(走 convertFileSrc)
    - 二进制: 提示"二进制文件不支持预览"
    - 顶部: 文件路径 + 大小 + Git 状态 + Diff 切换按钮
  -->
  <div class="file-preview">
    <!-- 顶部元信息 -->
    <div class="preview-header">
      <div class="preview-meta-left">
        <span class="preview-path" :title="filePath ?? ''">{{ filePath || '—' }}</span>
        <span v-if="gitStatus" class="git-badge" :class="gitStatusClass(gitStatus)">
          {{ gitStatusLabel(gitStatus) }}
        </span>
      </div>
      <div class="preview-meta-right">
        <span v-if="fileSize !== null" class="preview-size">{{ formatSize(fileSize) }}</span>
        <span v-if="truncated" class="preview-truncated">⚠ {{ $t('fileExplorer.truncated') }}</span>
        <!-- Diff 切换按钮(仅 git 有变更时显示) -->
        <button
          v-if="gitStatus"
          class="diff-toggle-btn"
          :class="{ active: showDiff }"
          @click="toggleDiff"
          :title="showDiff ? $t('fileExplorer.showContent') : $t('fileExplorer.showDiff')"
        >
          {{ showDiff ? '📄' : '📝' }}
        </button>
      </div>
    </div>

    <!-- 内容区 -->
    <div class="preview-body">
      <!-- 未选文件占位 -->
      <div v-if="!filePath" class="preview-placeholder">
        <div class="preview-placeholder-icon">📄</div>
        <div class="preview-placeholder-text">{{ $t('fileExplorer.selectFileHint') }}</div>
      </div>

      <!-- 加载中 -->
      <div v-else-if="loading" class="preview-loading">
        <span class="spinner"></span>
        {{ $t('fileExplorer.loadingFile') }}
      </div>

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

      <!-- 二进制 -->
      <div v-else-if="isBinary" class="preview-binary">
        <span class="binary-icon">📄</span>
        <p>{{ $t('fileExplorer.binaryNotSupported') }}</p>
      </div>

      <!-- 图片 -->
      <div v-else-if="isImage" class="preview-image">
        <img :src="imageUrl ?? ''" :alt="filePath ?? ''" />
      </div>

      <!-- Diff 视图(切到 diff 模式时显示;置于 Markdown 之前,使 .md 文件也支持 Diff) -->
      <div v-else-if="showDiff && diffContent" class="preview-diff">
        <div v-for="(ln, idx) in diffLines" :key="idx" class="diff-line" :class="'diff-' + ln.type">
          <span class="diff-line-num">{{ ln.oldNum || '' }}</span>
          <span class="diff-line-num">{{ ln.newNum || '' }}</span>
          <span class="diff-line-prefix">{{ ln.prefix }}</span>
          <span class="diff-line-text">{{ ln.text }}</span>
        </div>
        <div v-if="diffLoading" class="diff-loading">
          <span class="spinner"></span>
        </div>
      </div>

      <!-- Markdown 渲染(marked + DOMPurify + 代码高亮) -->
      <div v-else-if="isMarkdown" class="preview-md ai-md" v-html="renderedMd"></div>

      <!-- 文本/代码(highlight.js 语法高亮 + 行号;非 diff 模式时显示) -->
      <div v-else class="preview-code-scroll">
        <div class="preview-line-numbers" aria-hidden="true">
          <div v-for="n in lineCount" :key="n" class="preview-line-num">{{ n }}</div>
        </div>
        <pre class="preview-code"><code :class="hljsClass" v-html="htmlContent"></code></pre>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, computed, onUnmounted } from 'vue'
import { moduleApi } from '@/api/module'
import hljs from 'highlight.js/lib/common'
import { useMarkdown, useRendered } from '@/composables/useMarkdown'
import { escapeHtml } from '@/utils/html'

const props = defineProps<{
  moduleId: string
  filePath: string | null
  moduleRootPath: string
  gitStatus?: string
}>()

const { } = useMarkdown()
const content = ref('')
const htmlContent = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const isBinary = ref(false)
const fileSize = ref<number | null>(null)
const truncated = ref(false)
const imageUrl = ref<string | null>(null)

/** 行号显示 — 按源文本真实行数计算(非 highlight.js 渲染 HTML 行数)。
 * 高亮 HTML 可能因多行 token / 转义与源码行数不一致,直接切分 htmlContent 易错位。
 * 末尾无换行时 split 会多出空串,需按实际换行符计数对齐渲染。 */
const lineCount = computed(() => {
  if (!content.value) return 0
  const text = content.value
  // 末尾换行不计为新一行的可视行号(渲染时 <pre> 也不会显示空行)。
  const norm = text.endsWith('\n') ? text.slice(0, -1) : text
  if (norm === '') return 1
  return norm.split('\n').length
})

/** Diff 显示控制 */
const showDiff = ref(false)
const diffContent = ref('')
const diffLoading = ref(false)

interface DiffLine {
  type: 'add' | 'del' | 'ctx' | 'hdr'
  prefix: string
  text: string
  oldNum: string
  newNum: string
}

/** 解析 @@ -a,b +c,d @@ 头,返回旧/新行起始号 */
function parseHunkHeader(line: string): { oldStart: number; newStart: number } {
  // 格式: @@ -10,7 +10,8 @@
  const m = line.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/)
  if (!m) return { oldStart: 1, newStart: 1 }
  return { oldStart: parseInt(m[1], 10), newStart: parseInt(m[2], 10) }
}

const diffLines = computed<DiffLine[]>(() => {
  if (!diffContent.value) return []
  const lines: DiffLine[] = []
  let oldNum = 0
  let newNum = 0
  for (const raw of diffContent.value.split('\n')) {
    if (raw.startsWith('@@')) {
      const h = parseHunkHeader(raw)
      oldNum = h.oldStart
      newNum = h.newStart
      lines.push({ type: 'hdr', prefix: '', text: raw, oldNum: '', newNum: '' })
    } else if (raw.startsWith('+')) {
      lines.push({ type: 'add', prefix: '+', text: raw.slice(1), oldNum: '', newNum: String(newNum++) })
    } else if (raw.startsWith('-')) {
      lines.push({ type: 'del', prefix: '-', text: raw.slice(1), oldNum: String(oldNum++), newNum: '' })
    } else if (raw.startsWith('\\')) {
      // No newline at end of file 等元信息
      lines.push({ type: 'ctx', prefix: '', text: raw, oldNum: '', newNum: '' })
    } else {
      // 空串(diff 头前的空行)或 context 行(以空格开头)
      const text = raw.startsWith(' ') ? raw.slice(1) : raw
      lines.push({ type: 'ctx', prefix: ' ', text, oldNum: String(oldNum++), newNum: String(newNum++) })
    }
  }
  return lines
})

function toggleDiff() {
  showDiff.value = !showDiff.value
  if (showDiff.value && !diffContent.value) {
    loadDiff()
  }
}

async function loadDiff() {
  if (!props.moduleId || !props.filePath) return
  diffLoading.value = true
  try {
    const res = await moduleApi.getModuleFileDiff(props.moduleId, props.filePath)
    diffContent.value = res.diff || ''
  } catch {
    diffContent.value = ''
  } finally {
    diffLoading.value = false
  }
}

/** 外部文件变更 — 通过 df-data-changed 事件接收 AI 工具写入通知,被动触发重载。
 * 无轮询,零性能开销。外部编辑器修改需用户点击刷新按钮。 */

/** 文件后缀 → highlight.js 语言名映射。 */
const EXT_LANG: Record<string, string> = {
  rs: 'rust', ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
  vue: 'xml', html: 'xml', xml: 'xml', css: 'css', scss: 'scss',
  json: 'json', toml: 'ini', yaml: 'yaml', yml: 'yaml',
  md: 'markdown', py: 'python', go: 'go', java: 'java',
  sh: 'bash', sql: 'sql',
}

/** 从文件路径推断 highlight.js 语言 class。 */
const hljsClass = computed(() => {
  if (!props.filePath) return 'language-plaintext'
  const ext = props.filePath.split('.').pop()?.toLowerCase() ?? ''
  const lang = EXT_LANG[ext] || 'plaintext'
  return `language-${lang}`
})

/** 图片后缀白名单。 */
const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico']

/** Markdown 后缀判定。 */
const isMarkdown = computed(() => {
  if (!props.filePath) return false
  return props.filePath.toLowerCase().endsWith('.md')
})

/** Markdown 渲染(复用 useRendered,含 marked + DOMPurify + 代码高亮)。 */
const { rendered: renderedMd, ensureLoaded: ensureMdLoaded } = useRendered(() => content.value)

const isImage = computed(() => {
  if (!props.filePath) return false
  const lower = props.filePath.toLowerCase()
  return IMAGE_EXT.some((ext) => lower.endsWith(ext))
})

/** 拉文件内容 + 高亮渲染。 */
async function loadFile() {
  if (!props.filePath) {
    content.value = ''
    htmlContent.value = ''
    imageUrl.value = null
    return
  }
  loading.value = true
  error.value = null
  // Markdown 文件触发预热(确保 marked 就绪,useRendered computed 据此渲染)
  if (props.filePath.toLowerCase().endsWith('.md')) {
    ensureMdLoaded()
  }
  if (imageUrl.value) {
    URL.revokeObjectURL(imageUrl.value)
    imageUrl.value = null
  }
  try {
    const res = await moduleApi.readModuleFile(props.moduleId, props.filePath)
    fileSize.value = res.size
    truncated.value = res.truncated
    isBinary.value = res.is_binary
    if (res.is_binary) {
      content.value = ''
      htmlContent.value = ''
      return
    }
    if (isImage.value) {
      content.value = ''
      htmlContent.value = ''
      try {
        const { convertFileSrc } = await import('@tauri-apps/api/core')
        const abs = joinPath(props.moduleRootPath, props.filePath)
        imageUrl.value = convertFileSrc(abs)
      } catch {
        imageUrl.value = null
      }
    } else {
      content.value = res.content
      // highlight.js 语法高亮:已知语言精确高亮,未知语言自动检测
      const ext = props.filePath.split('.').pop()?.toLowerCase() ?? ''
      const lang = EXT_LANG[ext]
      try {
        if (lang && hljs.getLanguage(lang)) {
          htmlContent.value = hljs.highlight(res.content, { language: lang }).value
        } else {
          htmlContent.value = hljs.highlightAuto(res.content).value
        }
      } catch {
        // 高亮失败 → 转义纯文本(防 XSS)
        htmlContent.value = escapeHtml(res.content)
      }
    }
  } catch (e) {
    error.value = e instanceof Error ? e.message : String(e)
  } finally {
    loading.value = false
  }
}

function joinPath(root: string, rel: string): string {
  const sep = root.includes('\\') ? '\\' : '/'
  const normRel = rel.split('/').join(sep)
  if (root.endsWith(sep)) return root + normRel
  return root + sep + normRel
}

function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}

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'
  return 'git-other'
}

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'
  return x.charAt(0) || '?'
}

watch(() => [props.moduleId, props.filePath], () => {
  showDiff.value = false
  diffContent.value = ''
  loadFile()
}, { immediate: true })

onUnmounted(() => {
  if (imageUrl.value) URL.revokeObjectURL(imageUrl.value)
})
</script>

<style scoped>
.file-preview {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--df-bg-card);
  border-left: 0.5px solid var(--df-border);
}

.preview-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  border-bottom: 0.5px solid var(--df-border);
  font-size: 12px;
  flex-shrink: 0;
}

.preview-meta-left,
.preview-meta-right {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.preview-path {
  font-family: var(--df-font-mono, 'Cascadia Code', Consolas, monospace);
  color: var(--df-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 460px;
}

.preview-size { color: var(--df-text-dim); flex-shrink: 0; }
.preview-truncated { color: #f0a020; flex-shrink: 0; }

.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; }

.preview-body {
  flex: 1;
  overflow: hidden;
  padding: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.preview-placeholder,
.preview-loading,
.preview-error,
.preview-binary {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  height: 100%;
  color: var(--df-text-dim);
  font-size: 13px;
  min-height: 200px;
}

.preview-placeholder-icon {
  font-size: 48px;
  opacity: 0.4;
}

.preview-placeholder-text {
  font-size: 13px;
}

.preview-error { color: #e05050; }
.binary-icon { font-size: 32px; opacity: 0.5; }

.preview-image {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 16px;
  height: 100%;
  overflow: auto;
}

.preview-image img {
  max-width: 100%;
  height: auto;
  border-radius: var(--df-radius-sm, 4px);
}

.preview-code {
  margin: 0;
  padding: 14px 16px;
  font-family: var(--df-font-mono, 'Cascadia Code', Consolas, 'Courier New', monospace);
  font-size: 12.5px;
  line-height: 1.55;
  white-space: pre;
  overflow: visible;
  tab-size: 2;
  min-width: 0;
}

.preview-code code { font-family: inherit; }

/* 行号 + 代码共享同一个滚动容器 */
.preview-code-scroll {
  display: flex;
  flex: 1;
  min-height: 0;
  overflow: auto;
}

/* 行号列 — sticky 固定在左侧,跟随滚动容器一起滚 */
.preview-line-numbers {
  flex-shrink: 0;
  padding: 14px 0;
  min-width: 44px;
  text-align: right;
  background: rgba(255, 255, 255, 0.03);
  border-right: 0.5px solid var(--df-border);
  user-select: none;
  position: sticky;
  left: 0;
  z-index: 1;
  align-self: flex-start;
}

.preview-line-num {
  padding: 0 10px 0 8px;
  font-family: var(--df-font-mono, 'Cascadia Code', Consolas, monospace);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--df-text-dim);
  opacity: 0.5;
}

.preview-line-num.is-active {
  color: var(--df-accent);
  opacity: 1;
  background: rgba(255, 255, 255, 0.05);
}

/* Diff 视图 — 在自身区域滚动,不溢出到父级 */
.preview-diff {
  font-family: var(--df-font-mono, 'Cascadia Code', Consolas, monospace);
  font-size: 12px;
  line-height: 1.55;
  overflow: auto;
  min-height: 0;
  flex: 1;
}

.spinner {
  width: 16px;
  height: 16px;
  border: 2px 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;
}

/* Diff 切换条 */
.diff-toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  font-size: 11px;
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm, 4px);
  background: transparent;
  color: var(--df-text-secondary);
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
}

.diff-toggle-btn:hover {
  background: var(--df-bg);
  color: var(--df-text);
}

.diff-toggle-btn.active {
  background: var(--df-accent);
  color: #fff;
  border-color: var(--df-accent);
}

/* Diff 视图 */
.preview-diff {
  font-family: var(--df-font-mono, 'Cascadia Code', Consolas, monospace);
  font-size: 12px;
  line-height: 1.55;
  overflow: auto;
  height: 100%;
}

.diff-line {
  display: flex;
  padding: 0 8px;
}

.diff-line-num {
  width: 36px;
  flex-shrink: 0;
  text-align: right;
  padding-right: 8px;
  color: var(--df-text-dim);
  opacity: 0.4;
  user-select: none;
}

.diff-line-prefix {
  width: 16px;
  flex-shrink: 0;
  text-align: center;
  user-select: none;
}

.diff-line-text {
  flex: 1;
  white-space: pre;
  overflow: hidden;
}

.diff-add {
  background: rgba(40, 160, 70, 0.12);
}

.diff-add .diff-line-prefix { color: #4caf50; }

.diff-del {
  background: rgba(220, 60, 60, 0.12);
}

.diff-del .diff-line-prefix { color: #e05050; }

.diff-hdr {
  background: rgba(60, 140, 220, 0.08);
  color: var(--df-text-dim);
  font-weight: 500;
}

.diff-ctx {
  color: var(--df-text);
}

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

<!-- highlight.js github-dark 主题(全局,非 scoped) -->
<style src="highlight.js/styles/github-dark.css"></style>
