<template>
  <v-container fluid class="pa-6">
    <!-- Toolbar -->
    <v-card elevation="0" border rounded="lg" class="mb-4 pa-4">
      <v-row align="center" dense>
        <v-col cols="12" sm="4">
          <v-select
            v-model="selectedServer"
            :items="serverList"
            item-title="name"
            item-value="id"
            label="选择服务器"
            variant="outlined"
            density="compact"
            prepend-inner-icon="mdi-server"
            @update:model-value="onServerChange"
          />
        </v-col>
        <v-col cols="12" sm="4">
          <v-text-field
            v-model="currentPath"
            label="路径"
            variant="outlined"
            density="compact"
            prepend-inner-icon="mdi-folder"
            append-inner-icon="mdi-arrow-right"
            @click:append-inner="browseCurrent"
            @keydown.enter="browseCurrent"
          />
        </v-col>
        <v-col cols="12" sm="3">
          <v-text-field
            v-model="fileFilter"
            label="筛选文件名"
            variant="outlined"
            density="compact"
            prepend-inner-icon="mdi-magnify"
            clearable
            hide-details
          />
        </v-col>
        <v-col cols="12" sm="3">
          <v-select
            v-model="extFilter"
            :items="FILE_EXT_FILTERS"
            item-title="title"
            item-value="value"
            label="类型"
            variant="outlined"
            density="compact"
            hide-details
            clearable
          />
        </v-col>
        <v-col cols="12" sm="2">
          <v-select
            v-model="sortBy"
            :items="sortOptions"
            item-title="title"
            item-value="value"
            label="排序"
            variant="outlined"
            density="compact"
            hide-details
          />
        </v-col>
        <v-col cols="12" class="d-flex ga-2 flex-wrap justify-end">
          <v-btn size="small" variant="tonal" prepend-icon="mdi-upload" @click="showUpload = true">上传</v-btn>
          <v-btn size="small" variant="tonal" prepend-icon="mdi-file-plus" :disabled="!selectedServer" @click="showNewFile = true">新建文件</v-btn>
          <v-btn size="small" variant="tonal" prepend-icon="mdi-folder-plus" @click="showMkdir = true">新建目录</v-btn>
          <v-btn
            size="small"
            variant="tonal"
            prepend-icon="mdi-zip-box"
            :disabled="!selectedServer || !selectedFileCount"
            @click="openCompressDialog()"
          >
            压缩
          </v-btn>
          <v-btn
            size="small"
            variant="tonal"
            prepend-icon="mdi-content-paste"
            :disabled="!canPasteHere"
            @click="() => pasteClipboard()"
          >
            粘贴
          </v-btn>
          <v-btn
            size="small"
            variant="tonal"
            prepend-icon="mdi-arrow-up"
            :disabled="!canGoUp"
            @click="goUp"
          >
            上级
          </v-btn>
          <v-btn size="small" variant="tonal" prepend-icon="mdi-refresh" @click="browseCurrent">刷新</v-btn>
          <v-btn
            size="small"
            variant="tonal"
            prepend-icon="mdi-console"
            :disabled="!selectedServer"
            @click="openInTerminal"
          >
            终端
          </v-btn>
        </v-col>
      </v-row>
    </v-card>

    <v-alert
      v-if="filesAccessHint"
      :type="filesAccessHint.type"
      variant="tonal"
      closable
      prominent
      density="comfortable"
      class="mb-4"
      icon="mdi-shield-lock-outline"
      @click:close="dismissFilesAccessHint"
    >
      {{ filesAccessHint.text.split('目标机安装示例')[0].trim() }}
    </v-alert>

    <!-- Status -->
    <div class="text-caption text-medium-emphasis mb-2 d-flex flex-wrap align-center ga-2">
      <span>{{ statusSummary }}</span>
      <v-chip v-if="browseError" size="x-small" color="warning" variant="tonal">{{ browseError }}</v-chip>
      <v-chip
        v-if="hasClipboard"
        size="x-small"
        color="info"
        variant="tonal"
        class="cursor-pointer"
        @click="() => pasteClipboard()"
      >
        {{ clipboardSummary }} → {{ pasteDestLabel }} · Ctrl+V 粘贴
      </v-chip>
    </div>

    <!-- Breadcrumb -->
    <v-breadcrumbs :items="breadcrumbs" class="pa-0 mb-2">
      <template #item="{ item, index }">
        <v-breadcrumbs-item
          :disabled="item.disabled"
          :class="{ 'text-primary': !item.disabled, 'cursor-pointer': !item.disabled }"
          @click="onBreadcrumbClick(index)"
        >
          {{ item.title }}
        </v-breadcrumbs-item>
      </template>
      <template #divider>/</template>
    </v-breadcrumbs>

    <!-- Batch action bar -->
    <v-card v-if="selectedFileCount > 0" class="mb-4" color="primary" variant="tonal" rounded="lg">
      <v-card-text class="d-flex align-center py-2">
        <span class="text-body-2 mr-4">已选择 {{ selectedFileCount }} 个文件</span>
        <v-spacer />
        <v-btn size="small" variant="text" color="error" @click="batchDelete">批量删除</v-btn>
        <v-btn size="small" variant="text" @click="selectedFiles = []">取消选择</v-btn>
      </v-card-text>
    </v-card>

    <!-- File list (drag-drop upload) -->
    <v-card
      elevation="0"
      border
      rounded="lg"
      :class="{ 'files-drop-active': dropActive }"
      @dragenter.prevent="dropActive = true"
      @dragover.prevent="dropActive = true"
      @dragleave.prevent="dropActive = false"
      @drop.prevent="onDropFiles"
    >
      <v-data-table
        v-model="selectedFiles"
        :items="displayedFiles"
        :headers="fileHeaders"
        :loading="loading"
        show-select
        return-object
        hover
        density="comfortable"
        item-value="name"
        :row-props="fileRowProps"
      >
        <template #item.name="{ item }">
          <div class="d-flex align-center ga-2 min-w-0">
            <v-icon :color="entryIconColor(item)" size="20">
              {{ entryIcon(item) }}
            </v-icon>
            <v-icon
              v-if="isEntryLikelyUnreadable(item, sshUserForFiles)"
              size="18"
              color="warning"
              title="当前 SSH 用户可能无法读取"
            >
              mdi-shield-lock
            </v-icon>
            <div class="min-w-0">
              <span :class="{ 'font-weight-medium': item.type === 'directory' }">{{ item.name }}</span>
              <div v-if="item.type === 'symlink' && item.link_target" class="text-caption text-medium-emphasis text-truncate">
                → {{ item.link_target }}
              </div>
            </div>
          </div>
        </template>

        <template #item.owner="{ item }">
          <v-tooltip :text="permissionTooltip(item)" location="top">
            <template #activator="{ props: tipProps }">
              <v-chip
                v-bind="tipProps"
                size="small"
                variant="tonal"
                :color="ownerChipColor(item.owner || '')"
                class="text-none"
              >
                {{ formatOwnerChip(item) }}
              </v-chip>
            </template>
          </v-tooltip>
        </template>

        <template #item.size="{ item }">
          <span class="text-medium-emphasis">{{ item.type === 'directory' ? '—' : formatSize(item.size) }}</span>
        </template>

        <template #item.modified="{ item }">
          <span class="text-medium-emphasis">{{ item.modified || '—' }}</span>
        </template>

        <template #item.actions="{ item }">
          <div class="d-flex ga-1">
            <v-btn v-if="isRegularFile(item)" variant="text" size="x-small" density="compact" color="primary" @click.stop="editFile(item)">编辑</v-btn>
            <v-btn v-if="isRegularFile(item)" variant="text" size="x-small" density="compact" @click.stop="previewFile(item)">查看</v-btn>
            <v-btn v-if="isRegularFile(item)" variant="text" size="x-small" density="compact" @click.stop="downloadFile(item)">下载</v-btn>
            <v-btn variant="text" size="x-small" color="error" density="compact" @click.stop="confirmDeleteFile(item)">删除</v-btn>
          </div>
        </template>
        <template #no-data>
          <div class="text-center text-medium-emphasis py-8">
            <v-icon size="40" class="mb-2">mdi-folder-open-outline</v-icon>
            <div>{{ emptyHint }}</div>
            <div class="text-caption mt-2">
              单击进入目录 · 编辑文件将打开浮动编辑器（含目录树）· 复制/剪切后可在其他目录或选中文件夹粘贴
            </div>
          </div>
        </template>
      </v-data-table>
    </v-card>

    <!-- Upload dialog -->
    <v-dialog v-model="showUpload" max-width="500">
      <v-card border>
        <v-card-title>上传文件</v-card-title>
        <v-card-text>
          <v-file-input v-model="uploadFiles" label="选择文件" variant="outlined" multiple show-size />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showUpload = false">取消</v-btn>
          <v-btn color="primary" variant="flat" @click="doUpload" :loading="uploading">上传</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- New file dialog -->
    <v-dialog v-model="showNewFile" max-width="400">
      <v-card border>
        <v-card-title>新建文件</v-card-title>
        <v-card-text>
          <v-text-field
            v-model="newFileName"
            label="文件名"
            variant="outlined"
            density="compact"
            :rules="[validatePathSegment]"
            placeholder="example.conf"
            autofocus
            @keydown.enter="doNewFile"
          />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showNewFile = false">取消</v-btn>
          <v-btn color="primary" variant="flat" :loading="actionLoading" @click="doNewFile">创建</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Mkdir dialog -->
    <v-dialog v-model="showMkdir" max-width="400">
      <v-card border>
        <v-card-title>新建目录</v-card-title>
        <v-card-text>
          <v-text-field
            v-model="mkdirName"
            label="目录名"
            variant="outlined"
            density="compact"
            :rules="[validatePathSegment]"
            @keydown.enter="doMkdir"
          />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showMkdir = false">取消</v-btn>
          <v-btn color="primary" variant="flat" @click="doMkdir">创建</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Delete confirm -->
    <v-dialog v-model="showFileDelete" max-width="400">
      <v-card border>
        <v-card-title>确认删除</v-card-title>
        <v-card-text>确定要删除 <strong>{{ deletingFile?.name }}</strong> 吗？</v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showFileDelete = false">取消</v-btn>
          <v-btn color="error" variant="flat" @click="doDeleteFile">删除</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Rename dialog -->
    <v-dialog v-model="showRename" max-width="400">
      <v-card border>
        <v-card-title>重命名</v-card-title>
        <v-card-text>
          <v-text-field v-model="newName" label="新名称" variant="outlined" density="compact" :rules="[required('名称')]" autofocus @keydown.enter="doRename" />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showRename = false">取消</v-btn>
          <v-btn color="primary" variant="flat" @click="doRename">确定</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <FilePermissionDialog
      v-model="showChmod"
      :file="chmodFile"
      :server-id="selectedServer"
      :loading="chmodLoading"
      @apply="doChmod"
    />

    <!-- Compress dialog -->
    <v-dialog v-model="showCompress" max-width="440">
      <v-card border>
        <v-card-title>压缩</v-card-title>
        <v-card-text>
          <p class="text-body-2 mb-3">将压缩 {{ compressPaths.length }} 项到当前目录</p>
          <v-text-field v-model="compressDest" label="压缩包路径" variant="outlined" density="compact" />
          <v-select
            v-model="compressFormat"
            :items="[{ title: 'tar.gz', value: 'tar.gz' }, { title: 'zip', value: 'zip' }]"
            item-title="title"
            item-value="value"
            label="格式"
            variant="outlined"
            density="compact"
          />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showCompress = false">取消</v-btn>
          <v-btn color="primary" variant="flat" :loading="actionLoading" @click="doCompress">压缩</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Decompress dialog -->
    <v-dialog v-model="showDecompress" max-width="440">
      <v-card border>
        <v-card-title>解压</v-card-title>
        <v-card-text>
          <p class="text-body-2 mb-2 text-truncate">{{ decompressSource }}</p>
          <v-text-field v-model="decompressDest" label="解压到目录" variant="outlined" density="compact" />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showDecompress = false">取消</v-btn>
          <v-btn color="primary" variant="flat" :loading="actionLoading" @click="doDecompress">解压</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Context Menu -->
    <v-menu v-model="showContextMenu" :target="[contextX, contextY]" location="bottom start">
      <v-list density="compact" nav>
        <v-list-item
          v-if="contextFile && contextFile.type === 'directory'"
          prepend-icon="mdi-folder-open"
          @click="contextAction('open')"
        >
          <v-list-item-title>打开</v-list-item-title>
        </v-list-item>
        <v-list-item v-if="contextFile && isRegularFile(contextFile)" prepend-icon="mdi-pencil" @click="contextAction('edit')">
          <v-list-item-title>编辑</v-list-item-title>
        </v-list-item>
        <v-list-item v-if="contextFile && isRegularFile(contextFile)" prepend-icon="mdi-eye" @click="contextAction('preview')">
          <v-list-item-title>查看</v-list-item-title>
        </v-list-item>
        <v-list-item v-if="contextFile && isRegularFile(contextFile)" prepend-icon="mdi-download" @click="contextAction('download')">
          <v-list-item-title>下载</v-list-item-title>
        </v-list-item>
        <v-list-item
          v-if="contextFile"
          prepend-icon="mdi-content-copy"
          @click="contextAction('copy')"
        >
          <v-list-item-title>复制 (Ctrl+C)</v-list-item-title>
        </v-list-item>
        <v-list-item
          v-if="contextFile"
          prepend-icon="mdi-content-cut"
          @click="contextAction('cut')"
        >
          <v-list-item-title>剪切 (Ctrl+X)</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-content-paste" :disabled="!canPasteHere" @click="contextAction('paste')">
          <v-list-item-title>粘贴到当前目录 (Ctrl+V)</v-list-item-title>
        </v-list-item>
        <v-list-item
          v-if="contextFile?.type === 'directory' && canPasteHere"
          prepend-icon="mdi-folder-arrow-down"
          @click="contextAction('pasteInto')"
        >
          <v-list-item-title>粘贴到「{{ contextFile.name }}」</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-link-variant" @click="contextAction('copyPath')">
          <v-list-item-title>复制路径</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-console" @click="contextAction('terminal')">
          <v-list-item-title>终端</v-list-item-title>
        </v-list-item>
        <v-divider />
        <v-list-item
          v-if="contextFile && (isRegularFile(contextFile) || contextFile.type === 'directory')"
          prepend-icon="mdi-zip-box"
          @click="contextAction('compress')"
        >
          <v-list-item-title>压缩</v-list-item-title>
        </v-list-item>
        <v-list-item
          v-if="contextFile && isArchiveName(contextFile.name)"
          prepend-icon="mdi-package-up"
          @click="contextAction('decompress')"
        >
          <v-list-item-title>解压</v-list-item-title>
        </v-list-item>
        <v-divider />
        <v-list-item prepend-icon="mdi-file-rename-box" @click="contextAction('rename')">
          <v-list-item-title>重命名</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-lock" @click="contextAction('chmod')">
          <v-list-item-title>修改权限</v-list-item-title>
        </v-list-item>
        <v-divider />
        <v-list-item prepend-icon="mdi-delete" @click="contextAction('delete')">
          <v-list-item-title class="text-error">删除</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>

    <!-- Preview dialog -->
    <v-dialog v-model="showPreview" max-width="900" scrollable>
      <v-card border>
        <v-card-title class="d-flex align-center">
          <span class="text-truncate">{{ previewTitle }}</span>
          <v-spacer />
          <v-btn icon="mdi-close" variant="text" @click="showPreview = false" />
        </v-card-title>
        <v-divider />
        <v-card-text style="max-height: 70vh;">
          <v-progress-linear v-if="previewLoading" indeterminate class="mb-2" />
          <pre v-else class="files-preview-body text-body-2">{{ previewContent }}</pre>
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showPreview = false">关闭</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- 浮动 IDE 编辑器（目录树在编辑器内，可拖动/缩放） -->
    <FileEditorWorkbench
      ref="editorWorkbench"
      :server-id="selectedServer ?? 0"
      :current-path="currentPath"
      @saved="onEditorSaved($event)"
      @path-change="navigateToPath"
      @paste-target="pasteClipboard"
    />
  </v-container>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick, defineAsyncComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { http } from '@/api'
import { useServerList } from '@/composables/useServerList'
import { useSnackbar } from '@/composables/useSnackbar'
import { required } from '@/utils/validation'
const FileEditorWorkbench = defineAsyncComponent(() => import('@/components/FileEditorWorkbench.vue'))
import { useFilesHotkeys } from '@/composables/useFilesHotkeys'
import { useFilesClipboard } from '@/composables/useFilesClipboard'
import {
  FILE_EXT_FILTERS,
  isArchiveFile,
  matchesExtensionFilter,
  sortFileEntries,
  type FileSortKey,
} from '@/utils/fileSort'
import type { BrowseResponse, FileEntry, FilesCapabilityResult } from '@/types/api'
import { parseBrowseResponse } from '@/utils/fileBrowse'
import {
  buildFilesAccessHint,
  formatOwnerChip,
  isEntryLikelyUnreadable,
  isPermissionDeniedMessage,
  ownerChipColor,
  permissionTooltip,
} from '@/utils/fileMode'
import FilePermissionDialog from '@/components/FilePermissionDialog.vue'
import {
  buildRemoteBreadcrumbs,
  DEFAULT_FILES_ROOT,
  joinRemotePath,
  normalizeRemotePath,
  parentRemotePath,
  validatePathSegment,
  type RemoteBreadcrumbItem,
} from '@/utils/remotePath'
import { saveBlobAsFile } from '@/utils/fileDownload'
import {
  clearFilePreloadCache,
  exceedsEditorMaxSize,
  FILE_PRELOAD_MAX_BYTES,
  invalidateCachedFile,
  isPreloadEligible,
  preloadDirectoryFiles,
  pruneFilePreloadCache,
  readRemoteFileContent,
} from '@/utils/filePreload'

const snackbar = useSnackbar()
const { servers: serverList, loadServers } = useServerList()
const {
  hasClipboard,
  clipboardSummary,
  setFromSelection,
  canPasteOnServer,
  pasteToDirectory,
} = useFilesClipboard()

const canPasteHere = computed(() => canPasteOnServer(selectedServer.value))

/** 粘贴目标：单选文件夹 → 该文件夹；否则当前浏览目录 */
function resolvePasteDestination(explicitDest?: string): string {
  if (explicitDest) return normalizeRemotePath(explicitDest)
  const dirs = selectedFiles.value.filter((f) => f.type === 'directory')
  if (dirs.length === 1 && selectedFiles.value.length === 1) {
    return joinRemotePath(currentPath.value, dirs[0].name)
  }
  return normalizeRemotePath(currentPath.value)
}

const pasteDestLabel = computed(() => {
  const dest = resolvePasteDestination()
  const cur = normalizeRemotePath(currentPath.value)
  return dest === cur ? '当前目录' : dest
})

const route = useRoute()
const router = useRouter()

// ── State ──
const selectedServer = ref<number | null>(null)
const currentPath = ref(DEFAULT_FILES_ROOT)
const files = ref<FileEntry[]>([])
const loading = ref(false)
const browseError = ref('')
const fileFilter = ref('')
const extFilter = ref('')
const sortBy = ref<FileSortKey>('name')
const sortOptions = [
  { title: '名称', value: 'name' as const },
  { title: '大小', value: 'size' as const },
  { title: '修改时间', value: 'modified' as const },
]
const dropActive = ref(false)
const filesCapability = ref<FilesCapabilityResult | null>(null)
const lastReadDeniedPath = ref<string | null>(null)
const accessHintDismissed = ref(false)

function dismissFilesAccessHint() {
  accessHintDismissed.value = true
  sessionStorage.setItem(FILES_HINT_DISMISSED_KEY, '1')
}

const sshUserForFiles = computed(() => filesCapability.value?.ssh_user ?? null)

const unreadableFileCount = computed(() => {
  const user = sshUserForFiles.value
  if (!user) return 0
  return files.value.filter((f) => isEntryLikelyUnreadable(f, user)).length
})

const filesAccessHint = computed(() => {
  if (accessHintDismissed.value || !selectedServer.value) return null
  // Don't show sudo hint when the error is purely "path not found" — unrelated to permissions
  if (browseError.value && isNotFoundError(browseError.value)) return null
  const cap = filesCapability.value
  return buildFilesAccessHint({
    sshUser: cap?.ssh_user,
    canSudo: cap?.can_sudo_nopasswd ?? false,
    whitelistOk: cap?.whitelist_ok ?? false,
    isRootSsh: cap?.is_root ?? false,
    filesElevation: cap?.files_elevation,
    capabilityMessage: cap?.message,
    lastReadDeniedPath: lastReadDeniedPath.value,
    unreadableCount: unreadableFileCount.value,
  })
})

// Preview
const showPreview = ref(false)
const previewTitle = ref('')
const previewContent = ref('')
const previewLoading = ref(false)

// Dialogs
const showUpload = ref(false)
const showNewFile = ref(false)
const newFileName = ref('')
const showMkdir = ref(false)
const showCompress = ref(false)
const compressDest = ref('')
const compressFormat = ref<'tar.gz' | 'zip'>('tar.gz')
const compressPaths = ref<string[]>([])
const showDecompress = ref(false)
const decompressSource = ref('')
const decompressDest = ref('.')
const showFileDelete = ref(false)
const uploadFiles = ref<File[]>([])
const uploading = ref(false)
const mkdirName = ref('')
const actionLoading = ref(false)
const deletingFile = ref<FileEntry | null>(null)

// Rename
const showRename = ref(false)
const newName = ref('')
const renamingFile = ref<FileEntry | null>(null)

// Chmod
const showChmod = ref(false)
const chmodLoading = ref(false)
const chmodFile = ref<FileEntry | null>(null)

// Context menu
const showContextMenu = ref(false)
const contextX = ref(0)
const contextY = ref(0)
const contextFile = ref<FileEntry | null>(null)

// Batch select
const selectedFiles = ref<FileEntry[]>([])
const selectedFileCount = computed(() => selectedFiles.value.length)

// 浮动编辑器（Teleport，含侧栏目录树）
const editorWorkbench = ref<{
  openFile: (p: { path: string; name: string; content: string }) => void
} | null>(null)
const fileHeaders = [
  { title: '名称', key: 'name' },
  { title: '归属', key: 'owner', width: 110, sortable: false },
  { title: '大小', key: 'size', width: 120 },
  { title: '修改时间', key: 'modified', width: 180 },
  { title: '操作', key: 'actions', width: 200, align: 'end' as const },
]

// ── Breadcrumbs & path navigation ──
const breadcrumbs = computed((): RemoteBreadcrumbItem[] =>
  buildRemoteBreadcrumbs(currentPath.value),
)

const canGoUp = computed(() => parentRemotePath(currentPath.value) !== null)

const displayedFiles = computed(() => {
  let list = sortFileEntries(files.value, sortBy.value, 'asc')
  const ext = extFilter.value
  if (ext) list = list.filter((f) => matchesExtensionFilter(f.name, ext))
  const q = fileFilter.value.trim().toLowerCase()
  if (q) list = list.filter((f) => f.name.toLowerCase().includes(q))
  return list
})

function isArchiveName(name: string): boolean {
  return isArchiveFile(name)
}

const statusSummary = computed(() => {
  const dirs = files.value.filter((f) => f.type === 'directory').length
  const regular = files.value.filter((f) => f.type === 'file').length
  const links = files.value.filter((f) => f.type === 'symlink').length
  const preloadable = files.value.filter(isPreloadEligible).length
  const parts = [`${dirs} 个目录`, `${regular} 个文件`]
  if (links) parts.push(`${links} 个链接`)
  if (preloadable) parts.push(`${preloadable} 个 ≤300KB 可预加载`)
  return `${parts.join('，')} · ${currentPath.value}`
})

const emptyHint = computed(() => {
  if (!selectedServer.value) return '请先选择服务器'
  if (browseError.value) return browseError.value
  if (fileFilter.value.trim()) return '无匹配文件'
  const cap = filesCapability.value
  if (cap?.is_root) {
    return `目录为空（${currentPath.value}），可在上方修改路径或到「服务器」核对 target_path`
  }
  return `目录为空（${currentPath.value}）`
})

function entryIcon(item: FileEntry): string {
  if (item.type === 'directory') return 'mdi-folder'
  if (item.type === 'symlink') return 'mdi-link-variant'
  return 'mdi-file-outline'
}

function entryIconColor(item: FileEntry): string {
  if (item.type === 'directory') return 'amber'
  if (item.type === 'symlink') return 'teal'
  return 'blue'
}

function isRegularFile(item: FileEntry): boolean {
  return item.type === 'file'
}

function syncRouteQuery() {
  const query: Record<string, string | undefined> = { ...route.query as Record<string, string> }
  if (selectedServer.value) {
    query.server_id = String(selectedServer.value)
    query.path = currentPath.value === DEFAULT_FILES_ROOT ? undefined : currentPath.value
  } else {
    delete query.server_id
    delete query.path
  }
  router.replace({ query })
}

function openEntry(item: FileEntry) {
  if (item.type === 'directory') {
    navigateToPath(joinRemotePath(currentPath.value, item.name))
    return
  }
  if (item.type === 'symlink') {
    const target = item.link_target?.trim()
    if (target?.startsWith('/')) {
      navigateToPath(target)
    } else {
      snackbar(target ? `符号链接 → ${target}` : '相对符号链接（请在终端中打开）', 'info')
    }
  }
}

function navigateToPath(path: string) {
  if (!selectedServer.value) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  currentPath.value = normalizeRemotePath(path)
  selectedFiles.value = []
  browse()
}

function browseCurrent() {
  if (!selectedServer.value) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  currentPath.value = normalizeRemotePath(currentPath.value)
  browse()
}

function goUp() {
  const parent = parentRemotePath(currentPath.value)
  if (parent != null) navigateToPath(parent)
}

function onBreadcrumbClick(index: number) {
  const bc = breadcrumbs.value[index]
  if (bc && !bc.disabled) navigateToPath(bc.path)
}

// ── Data loading ──
function openInTerminal() {
  if (!selectedServer.value) return
  router.push({
    path: '/terminal',
    query: {
      server_id: String(selectedServer.value),
      path: currentPath.value || '/',
    },
  })
}

async function browse() {
  if (!selectedServer.value) return
  loading.value = true
  const path = normalizeRemotePath(currentPath.value)
  currentPath.value = path
  try {
    const res = await http.post<BrowseResponse | FileEntry[]>('/sync/browse', {
      server_id: selectedServer.value,
      path,
    })
    const { items, error } = parseBrowseResponse(res)

    // Auto-fallback: if path doesn't exist, try parent directories automatically
    if (error && isNotFoundError(error) && path !== '/') {
      browseError.value = ''
      loading.value = false
      const parent = parentRemotePath(path)
      if (parent) {
        snackbar(`路径 "${path}" 不存在，已自动跳转到上级目录`, 'info')
        currentPath.value = parent
        void browse()
        return
      }
    }

    browseError.value = error ? friendlyBrowseError(error) : ''
    if (error && !isNotFoundError(error)) {
      snackbar(friendlyBrowseError(error), 'warning')
    }
    files.value = items
    if (selectedServer.value && items.length) {
      pruneFilePreloadCache(selectedServer.value, path)
      preloadDirectoryFiles(selectedServer.value, path, items)
    }
  } catch {
    files.value = []
    snackbar('浏览目录失败', 'error')
  } finally {
    loading.value = false
    syncRouteQuery()
  }
}

/** 判断 ls 报错是否为「路径不存在」（与权限无关） */
function isNotFoundError(err: string): boolean {
  return err.includes('No such file or directory') || err.includes('not a directory')
}

/** 将原始 ls 错误转为用户友好文案 */
function friendlyBrowseError(err: string): string {
  if (isNotFoundError(err)) return `路径不存在：${currentPath.value}，请检查服务器「target_path」设置`
  if (err.includes('Permission denied') || err.includes('Operation not permitted'))
    return `无读取权限（${currentPath.value}）`
  return err
}

async function refreshFilesCapability() {
  if (!selectedServer.value) {
    filesCapability.value = null
    return
  }
  try {
    filesCapability.value = await http.get<FilesCapabilityResult>(
      `/servers/${selectedServer.value}/files-capability`,
    )
  } catch {
    filesCapability.value = null
  }
}

function defaultPathForServer(serverId: number | null): string {
  if (serverId == null) return DEFAULT_FILES_ROOT
  const srv = serverList.value.find((s) => s.id === serverId)
  const tp = srv?.target_path?.trim()
  return tp ? normalizeRemotePath(tp) : DEFAULT_FILES_ROOT
}

function onServerChange() {
  if (selectedServer.value) {
    sessionStorage.setItem(FILES_LAST_SERVER_KEY, String(selectedServer.value))
  }
  clearFilePreloadCache()
  currentPath.value = defaultPathForServer(selectedServer.value)
  fileFilter.value = ''
  selectedFiles.value = []
  lastReadDeniedPath.value = null
  accessHintDismissed.value = false
  sessionStorage.removeItem(FILES_HINT_DISMISSED_KEY)
  void refreshFilesCapability()
  browse()
}

function resolveInitialServerId(): number | null {
  const qServer = route.query.server_id
  if (qServer) {
    const id = Number(qServer)
    if (Number.isFinite(id) && serverList.value.some((s) => s.id === id)) return id
  }
  const stored = sessionStorage.getItem(FILES_LAST_SERVER_KEY)
  if (stored) {
    const id = Number(stored)
    if (Number.isFinite(id) && serverList.value.some((s) => s.id === id)) return id
  }
  return serverList.value[0]?.id ?? null
}

function onEditorSaved(path: string) {
  if (selectedServer.value && path) {
    invalidateCachedFile(selectedServer.value, path)
  }
  browse()
}

function rejectOversizedFile(item: FileEntry, action: '编辑' | '查看'): boolean {
  if (exceedsEditorMaxSize(item.size)) {
    snackbar(
      `文件超过 300KB（${formatSize(item.size)}），无法${action}，请下载或使用终端`,
      'warning',
    )
    return true
  }
  return false
}

const FILES_LAST_SERVER_KEY = 'nexus:files:last_server_id'
const FILES_HINT_DISMISSED_KEY = 'nexus:files-hint-dismissed'

function isRowActionTarget(target: EventTarget | null): boolean {
  const el = target instanceof Element ? target : null
  return Boolean(el?.closest('.v-selection-control, .v-btn, .v-checkbox, button, a'))
}

/** Vuetify 4 已移除 @click:row，通过 row-props 绑定行级事件 */
function fileRowProps(data: { item: FileEntry }) {
  const item = data.item
  return {
    class: item.type === 'directory' ? 'files-row--dir' : undefined,
    style: { cursor: item.type === 'directory' || item.type === 'symlink' ? 'pointer' : 'default' },
    onClick: (e: MouseEvent) => {
      if (isRowActionTarget(e.target)) return
      onRowClick(e, { item })
    },
    onDblclick: (e: MouseEvent) => {
      if (isRowActionTarget(e.target)) return
      onRowDblClick(e, { item })
    },
    onContextmenu: (e: MouseEvent) => {
      if (isRowActionTarget(e.target)) return
      onRowContext(e, { item })
    },
  }
}

function onRowClick(_event: Event, { item }: { item: FileEntry }) {
  if (item.type === 'directory' || item.type === 'symlink') {
    openEntry(item)
  }
}

function onRowDblClick(_event: Event, { item }: { item: FileEntry }) {
  if (isRegularFile(item)) {
    editFile(item)
  }
}

async function onDropFiles(e: DragEvent) {
  dropActive.value = false
  if (!selectedServer.value) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  const dropped = e.dataTransfer?.files
  if (!dropped?.length) return
  uploadFiles.value = Array.from(dropped)
  await doUpload()
}

function onRowContext(e: MouseEvent, { item }: { item: FileEntry }) {
  e.preventDefault()
  contextFile.value = item
  contextX.value = e.clientX
  contextY.value = e.clientY
  showContextMenu.value = true
}

function selectedAbsolutePaths(extra?: FileEntry[]): string[] {
  const items = extra?.length ? extra : selectedFiles.value
  return items.map((f) => joinRemotePath(currentPath.value, f.name))
}

function copyToClipboard(items?: FileEntry[]) {
  if (!selectedServer.value) return
  const paths = selectedAbsolutePaths(items)
  if (!paths.length) {
    snackbar('请先选择文件或目录', 'warning')
    return
  }
  if (setFromSelection('copy', selectedServer.value, paths)) {
    snackbar(`已复制 ${paths.length} 项`)
  }
}

function cutToClipboard(items?: FileEntry[]) {
  if (!selectedServer.value) return
  const paths = selectedAbsolutePaths(items)
  if (!paths.length) {
    snackbar('请先选择文件或目录', 'warning')
    return
  }
  if (setFromSelection('cut', selectedServer.value, paths)) {
    snackbar(`已剪切 ${paths.length} 项`)
  }
}

async function pasteClipboard(explicitDest?: string) {
  if (!selectedServer.value) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  if (!canPasteOnServer(selectedServer.value)) {
    snackbar('剪贴板为空或服务器不一致', 'warning')
    return
  }
  const targetDir = resolvePasteDestination(explicitDest)
  actionLoading.value = true
  try {
    await pasteToDirectory(targetDir)
    selectedFiles.value = []
    clearFilePreloadCache()
    await browse()
    snackbar(`已粘贴到 ${targetDir}`)
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '粘贴失败'
    snackbar(msg, 'error')
  } finally {
    actionLoading.value = false
  }
}

function contextAction(action: string) {
  showContextMenu.value = false
  if (!contextFile.value) return
  const item = contextFile.value
  switch (action) {
    case 'open': openEntry(item); break
    case 'copy': copyToClipboard([item]); break
    case 'cut': cutToClipboard([item]); break
    case 'paste': pasteClipboard(); break
    case 'pasteInto':
      if (item.type === 'directory') {
        pasteClipboard(joinRemotePath(currentPath.value, item.name))
      }
      break
    case 'edit': editFile(item); break
    case 'preview': previewFile(item); break
    case 'download': downloadFile(item); break
    case 'copyPath': copyRemotePath(item); break
    case 'terminal': openInTerminal(); break
    case 'compress': openCompressDialog([item]); break
    case 'decompress': openDecompressDialog(item); break
    case 'rename': startRename(item); break
    case 'chmod': startChmod(item); break
    case 'delete': confirmDeleteFile(item); break
  }
}

async function copyRemotePath(item: FileEntry) {
  const path = joinRemotePath(currentPath.value, item.name)
  try {
    await navigator.clipboard.writeText(path)
    snackbar('路径已复制')
  } catch {
    snackbar('复制失败', 'error')
  }
}

function openCompressDialog(items?: FileEntry[]) {
  if (!selectedServer.value) return
  const targets = items?.length
    ? items
    : selectedFiles.value.length
      ? selectedFiles.value
      : []
  if (!targets.length) {
    snackbar('请选择要压缩的文件或目录', 'warning')
    return
  }
  compressPaths.value = targets.map((f) => joinRemotePath(currentPath.value, f.name))
  const base = targets.length === 1 ? targets[0].name.replace(/\.[^.]+$/, '') : 'archive'
  compressDest.value = joinRemotePath(currentPath.value, `${base}.tar.gz`)
  compressFormat.value = 'tar.gz'
  showCompress.value = true
}

function openDecompressDialog(item: FileEntry) {
  if (!selectedServer.value) return
  if (!isArchiveFile(item.name)) {
    snackbar('仅支持 .tar.gz / .tgz / .zip', 'warning')
    return
  }
  decompressSource.value = joinRemotePath(currentPath.value, item.name)
  decompressDest.value = currentPath.value
  showDecompress.value = true
}

async function doCompress() {
  if (!selectedServer.value || !compressPaths.value.length || !compressDest.value) return
  actionLoading.value = true
  try {
    let dest = compressDest.value
    if (compressFormat.value === 'zip' && !dest.endsWith('.zip')) {
      dest = dest.replace(/\.tar\.gz$/i, '.zip')
      if (!dest.endsWith('.zip')) dest += '.zip'
    }
    await http.post('/sync/compress', {
      server_id: selectedServer.value,
      paths: compressPaths.value,
      dest,
      format: compressFormat.value,
    })
    showCompress.value = false
    selectedFiles.value = []
    browse()
    snackbar('压缩完成')
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '压缩失败'
    snackbar(msg, 'error')
  } finally {
    actionLoading.value = false
  }
}

async function doDecompress() {
  if (!selectedServer.value || !decompressSource.value) return
  actionLoading.value = true
  try {
    await http.post('/sync/decompress', {
      server_id: selectedServer.value,
      path: decompressSource.value,
      dest: decompressDest.value || '.',
    })
    showDecompress.value = false
    browse()
    snackbar('解压完成')
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '解压失败'
    snackbar(msg, 'error')
  } finally {
    actionLoading.value = false
  }
}

async function doNewFile() {
  if (!selectedServer.value || !newFileName.value) return
  const check = validatePathSegment(newFileName.value)
  if (check !== true) {
    snackbar(check, 'warning')
    return
  }
  const name = newFileName.value
  const path = joinRemotePath(currentPath.value, name)
  actionLoading.value = true
  try {
    await http.post('/sync/write-file', {
      server_id: selectedServer.value,
      path,
      content: '',
    })
    showNewFile.value = false
    newFileName.value = ''
    invalidateCachedFile(selectedServer.value, path)
    await browse()
    snackbar('文件已创建')
    await nextTick()
    editorWorkbench.value?.openFile({ path, name, content: '' })
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '创建失败'
    snackbar(msg, 'error')
  } finally {
    actionLoading.value = false
  }
}

function startRename(item: FileEntry) {
  renamingFile.value = item
  newName.value = item.name
  showRename.value = true
}

async function doRename() {
  if (!selectedServer.value || !renamingFile.value || !newName.value) return
  try {
    await http.post('/sync/file-ops', {
      server_id: selectedServer.value,
      operation: 'rename',
      path: joinRemotePath(currentPath.value, renamingFile.value.name),
      new_path: joinRemotePath(currentPath.value, newName.value),
    })
    showRename.value = false
    browse()
    snackbar('重命名成功')
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '重命名失败'
    snackbar(msg, 'error')
  }
}

function startChmod(item: FileEntry) {
  chmodFile.value = item
  showChmod.value = true
}

async function doChmod(payload: { mode: string; owner?: string; group?: string; recursive?: boolean }) {
  if (!selectedServer.value || !chmodFile.value) return
  chmodLoading.value = true
  try {
    await http.post('/sync/chmod', {
      server_id: selectedServer.value,
      path: joinRemotePath(currentPath.value, chmodFile.value.name),
      mode: payload.mode,
      owner: payload.owner,
      group: payload.group,
      recursive: payload.recursive ?? false,
    })
    showChmod.value = false
    browse()
    snackbar('权限已修改')
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '修改权限失败'
    snackbar(msg, 'error')
  } finally {
    chmodLoading.value = false
  }
}

async function batchDelete() {
  if (!selectedServer.value || !selectedFiles.value.length) return
  if (!confirm(`确定删除 ${selectedFiles.value.length} 个文件？`)) return
  let ok = 0
  let fail = 0
  for (const f of selectedFiles.value) {
    try {
      await http.post('/sync/file-ops', {
        server_id: selectedServer.value,
        operation: 'delete',
        path: joinRemotePath(currentPath.value, f.name),
      })
      ok++
    } catch {
      fail++
    }
  }
  selectedFiles.value = []
  browse()
  if (fail === 0) snackbar(`已删除 ${ok} 项`)
  else snackbar(`删除完成：成功 ${ok}，失败 ${fail}`, fail > 0 ? 'warning' : 'success')
}

async function previewFile(item: FileEntry) {
  if (!selectedServer.value) return
  if (rejectOversizedFile(item, '查看')) return
  const remotePath = joinRemotePath(currentPath.value, item.name)
  previewTitle.value = item.name
  previewContent.value = ''
  previewLoading.value = true
  showPreview.value = true
  try {
    const raw = await readRemoteFileContent(selectedServer.value, remotePath, FILE_PRELOAD_MAX_BYTES)
    previewContent.value = raw || '(空文件)'
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '读取失败'
    previewContent.value = msg
    if (isPermissionDeniedMessage(msg)) {
      lastReadDeniedPath.value = item.name
      accessHintDismissed.value = false
    }
    snackbar(msg, 'error')
  } finally {
    previewLoading.value = false
  }
}

async function editFile(item: FileEntry) {
  if (!selectedServer.value) return
  if (rejectOversizedFile(item, '编辑')) return
  actionLoading.value = true
  try {
    const path = joinRemotePath(currentPath.value, item.name)
    const content = await readRemoteFileContent(selectedServer.value, path, FILE_PRELOAD_MAX_BYTES)
    await nextTick()
    editorWorkbench.value?.openFile({ path, name: item.name, content })
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '读取失败'
    if (isPermissionDeniedMessage(msg)) {
      lastReadDeniedPath.value = item.name
      accessHintDismissed.value = false
    }
    snackbar(msg, 'error')
  } finally {
    actionLoading.value = false
  }
}

async function downloadFile(item: FileEntry) {
  if (!selectedServer.value) return
  try {
    const remotePath = joinRemotePath(currentPath.value, item.name)
    const { blob, filename } = await http.download('/sync/download', {
      server_id: selectedServer.value,
      path: remotePath,
    })
    saveBlobAsFile(blob, filename)
    snackbar(`已下载 ${filename}`)
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '下载失败'
    snackbar(msg, 'error')
  }
}

async function confirmDeleteFile(item: FileEntry) {
  deletingFile.value = item
  showFileDelete.value = true
}

async function doDeleteFile() {
  if (!selectedServer.value || !deletingFile.value) return
  actionLoading.value = true
  try {
    await http.post('/sync/file-ops', {
      server_id: selectedServer.value,
      operation: 'delete',
      path: joinRemotePath(currentPath.value, deletingFile.value.name),
    })
    showFileDelete.value = false
    browse()
    snackbar('已删除')
  } catch (e: any) {
    snackbar(e.message || '删除失败', 'error')
  } finally {
    actionLoading.value = false
  }
}

async function doMkdir() {
  if (!selectedServer.value || !mkdirName.value) return
  const check = validatePathSegment(mkdirName.value)
  if (check !== true) {
    snackbar(check, 'warning')
    return
  }
  actionLoading.value = true
  try {
    await http.post('/sync/file-ops', {
      server_id: selectedServer.value,
      operation: 'mkdir',
      path: joinRemotePath(currentPath.value, mkdirName.value),
    })
    showMkdir.value = false
    mkdirName.value = ''
    browse()
    snackbar('目录已创建')
  } catch (e: any) {
    snackbar(e.message || '创建失败', 'error')
  } finally {
    actionLoading.value = false
  }
}

async function doUpload() {
  if (!selectedServer.value || !uploadFiles.value.length) return
  uploading.value = true
  try {
    const form = new FormData()
    for (const f of uploadFiles.value) form.append('files', f)
    form.append('server_id', String(selectedServer.value))
    form.append('remote_path', currentPath.value)
    await http.upload('/sync/upload', form)
    showUpload.value = false
    uploadFiles.value = []
    browse()
    snackbar('上传成功')
  } catch (e: any) {
    snackbar(e.message || '上传失败', 'error')
  } finally {
    uploading.value = false
  }
}

function formatSize(bytes: number | undefined) {
  if (!bytes) return '—'
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / 1024 / 1024).toFixed(1) + ' MB'
}


// ── Init ──
onMounted(async () => {
  await loadServers()
  if (sessionStorage.getItem(FILES_HINT_DISMISSED_KEY)) {
    accessHintDismissed.value = true
  }
  if (!serverList.value.length) {
    snackbar('暂无可用服务器，请先在「服务器」页添加', 'warning')
    return
  }
  const initialId = resolveInitialServerId()
  if (initialId == null) return
  selectedServer.value = initialId
  const qPath = route.query.path
  currentPath.value =
    typeof qPath === 'string' && qPath
      ? normalizeRemotePath(qPath)
      : defaultPathForServer(initialId)
  await refreshFilesCapability()
  browse()
})

watch(
  () => route.query.server_id,
  (id) => {
    if (id && Number(id) !== selectedServer.value) {
      selectedServer.value = Number(id)
      const qPath = route.query.path
      currentPath.value =
        typeof qPath === 'string' && qPath
          ? normalizeRemotePath(qPath)
          : defaultPathForServer(Number(id))
      lastReadDeniedPath.value = null
      void refreshFilesCapability()
      browse()
    }
  },
)

useFilesHotkeys(
  {
    refresh: browseCurrent,
    goUp,
    renameSelection: () => {
      if (selectedFiles.value.length === 1) startRename(selectedFiles.value[0])
    },
    deleteSelection: () => {
      if (selectedFiles.value.length > 0) void batchDelete()
    },
    selectAll: () => {
      selectedFiles.value = [...displayedFiles.value]
    },
    copySelection: () => copyToClipboard(),
    cutSelection: () => cutToClipboard(),
    pasteClipboard: () => {
      void pasteClipboard()
    },
  },
  selectedFiles,
)

</script>

<style scoped>
.files-drop-active {
  outline: 2px dashed rgb(var(--v-theme-primary));
  outline-offset: -2px;
}
.files-preview-body {
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  margin: 0;
}
</style>
