<template>
  <Teleport to="body">
    <div
      v-if="tabs.length > 0 && !minimized && activeTab"
      class="file-editor-float"
      :class="{ 'file-editor-float--maximized': maximized }"
      :style="floatContainerStyle"
    >
      <v-card border rounded="lg" class="file-editor-panel d-flex flex-column h-100">
        <!-- ── Toolbar ── -->
        <div
          class="file-editor-toolbar d-flex align-center flex-shrink-0"
          :class="{ 'file-editor-toolbar--draggable': !maximized }"
          @mousedown="onToolbarMouseDown"
        >
          <!-- Drag handle -->
          <v-icon v-if="!maximized" size="16" class="ml-2 mr-1 text-disabled file-editor-drag-hint" @mousedown.stop>
            mdi-drag
          </v-icon>

          <!-- File tabs -->
          <v-tabs
            :model-value="activeTabId ?? undefined"
            density="compact"
            show-arrows
            class="file-editor-tabs flex-grow-1 min-w-0"
            @update:model-value="onTabSelected"
            @mousedown.stop
          >
            <v-tab
              v-for="tab in tabs"
              :key="tab.id"
              :value="tab.id"
              class="text-none px-2"
            >
              <span class="text-truncate" style="max-width: 130px;">{{ tab.name }}</span>
              <span v-if="tab.modified" class="text-warning ml-1" style="font-size:10px;">●</span>
              <v-btn
                icon="mdi-close"
                size="x-small"
                variant="text"
                class="ml-1 opacity-60"
                density="compact"
                @click.stop="requestCloseTab(tab.id)"
              />
            </v-tab>
          </v-tabs>

          <!-- Separator -->
          <v-divider vertical class="mx-1" style="height:20px;align-self:center;" @mousedown.stop />

          <!-- Language selector -->
          <v-select
            v-model="activeLanguageChoice"
            :items="EDITOR_LANGUAGE_OPTIONS"
            item-title="title"
            item-value="value"
            density="compact"
            variant="plain"
            hide-details
            class="file-editor-lang-select flex-shrink-0"
            @mousedown.stop
            @update:model-value="onLanguageChoiceChange"
          />

          <!-- Word wrap toggle -->
          <v-btn
            size="small"
            variant="text"
            :color="wordWrap ? 'primary' : undefined"
            :title="wordWrap ? '关闭自动换行 (Alt+Z)' : '开启自动换行 (Alt+Z)'"
            density="compact"
            class="flex-shrink-0 text-none px-2"
            @mousedown.stop
            @click="toggleWordWrap"
          >
            自动换行
          </v-btn>

          <!-- Separator -->
          <v-divider vertical class="mx-1" style="height:20px;align-self:center;" @mousedown.stop />

          <!-- Save -->
          <v-btn
            size="small"
            variant="tonal"
            prepend-icon="mdi-content-save"
            :loading="saving"
            :disabled="!activeTab?.modified || !ready"
            class="flex-shrink-0 mr-1"
            @mousedown.stop
            @click="saveActive"
          >
            保存
          </v-btn>

          <!-- Window controls -->
          <v-btn
            size="small"
            variant="text"
            :icon="maximized ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
            :title="maximized ? '退出全屏 (Esc)' : '全屏'"
            density="compact"
            @mousedown.stop
            @click="maximized = !maximized"
          />
          <v-btn
            size="small"
            variant="text"
            icon="mdi-window-minimize"
            title="最小化"
            density="compact"
            @mousedown.stop
            @click="minimized = true"
          />
          <v-btn
            size="small"
            variant="text"
            icon="mdi-close"
            title="关闭编辑器"
            density="compact"
            class="mr-1"
            @mousedown.stop
            @click="requestClosePanel"
          />
        </div>

        <!-- ── Main area ── -->
        <div class="file-editor-main d-flex flex-grow-1 min-h-0">
          <aside class="file-editor-sidebar flex-shrink-0" @mousedown.stop>
            <FileDirectoryTree
              embedded
              :server-id="serverId || null"
              :root-path="editorTreeRoot"
              :current-path="treeBrowsePath"
              @navigate="onTreeNavigate"
              @paste-target="(p) => emit('paste-target', p)"
            />
          </aside>
          <div class="file-editor-content d-flex flex-column flex-grow-1 min-w-0 min-h-0">
            <div class="file-editor-body flex-grow-1 position-relative min-h-0">
              <v-progress-linear v-if="!ready" indeterminate absolute class="file-editor-progress" />
              <div ref="editorHost" class="file-editor-host" />
            </div>
            <!-- Status bar -->
            <div class="file-editor-status text-caption px-3 py-1 d-flex flex-wrap align-center ga-3">
              <span class="text-mono">{{ cursorLine }}:{{ cursorColumn }}</span>
              <v-divider vertical style="height:12px;" />
              <span>{{ activeLanguage }}</span>
              <v-divider vertical style="height:12px;" />
              <span v-if="wordWrap" class="text-primary" style="font-size:11px;">换行</span>
              <v-divider v-if="wordWrap" vertical style="height:12px;" />
              <span class="text-truncate flex-grow-1 text-medium-emphasis">{{ activeTab?.path ?? '' }}</span>
              <span v-if="activeTab?.modified" class="text-warning">未保存</span>
            </div>
          </div>
        </div>

        <div
          v-if="!maximized"
          class="file-editor-resize"
          title="拖动调整大小"
          @mousedown.stop="startResize"
        />
      </v-card>
    </div>

    <!-- Minimized dock -->
    <v-card
      v-if="tabs.length > 0 && minimized"
      border
      rounded="lg"
      class="file-editor-dock"
    >
      <span class="text-caption text-medium-emphasis mr-2">编辑器</span>
      <v-chip
        v-for="tab in tabs"
        :key="tab.id"
        size="small"
        :color="tab.id === activeTabId ? 'primary' : undefined"
        variant="tonal"
        class="cursor-pointer"
        @click="restoreTab(tab.id)"
      >
        {{ tab.name }}
        <span v-if="tab.modified" class="text-warning ml-1">●</span>
      </v-chip>
      <v-spacer />
      <v-btn size="x-small" variant="text" icon="mdi-dock-window" title="展开" @click="minimized = false" />
      <v-btn size="x-small" variant="text" icon="mdi-close" title="关闭全部" @click="requestClosePanel" />
    </v-card>
  </Teleport>

  <!-- Unsaved-changes dialog -->
  <v-dialog v-model="showCloseConfirm" max-width="400">
    <v-card border>
      <v-card-title>{{ closeConfirmTitle }}</v-card-title>
      <v-card-text>{{ closeConfirmText }}</v-card-text>
      <v-card-actions>
        <v-spacer />
        <v-btn variant="text" @click="cancelClose">取消</v-btn>
        <v-btn color="error" variant="flat" @click="confirmDiscardClose">不保存</v-btn>
        <v-btn color="primary" variant="flat" @click="confirmSaveClose">保存</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>

  <!-- Conflict dialog: file modified externally since last read -->
  <v-dialog v-model="showConflictDialog" max-width="440" persistent>
    <v-card border>
      <v-card-title class="text-warning">
        <v-icon start>mdi-alert-outline</v-icon>
        文件冲突
      </v-card-title>
      <v-card-text>
        「{{ conflictTabName }}」已被外部修改。<br>
        <strong>覆盖</strong>会以当前编辑内容覆盖服务器版本；<strong>放弃</strong>会丢弃本地修改。
      </v-card-text>
      <v-card-actions>
        <v-spacer />
        <v-btn variant="text" @click="cancelConflict">取消</v-btn>
        <v-btn color="error" variant="flat" @click="discardConflict">放弃本地修改</v-btn>
        <v-btn color="warning" variant="flat" @click="forceOverwrite">强制覆盖</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script setup lang="ts">
import { ref, shallowRef, computed, watch, nextTick, onBeforeUnmount, markRaw } from 'vue'
import { ApiError, http } from '@/api'
import FileDirectoryTree from '@/components/FileDirectoryTree.vue'
import { useSnackbar } from '@/composables/useSnackbar'
import { useFloatingPanel } from '@/composables/useFloatingPanel'
import { applyNexusEditorTheme, buildMonacoEditorOptions, initMonaco } from '@/monaco/initMonaco'
import {
  EDITOR_LANGUAGE_OPTIONS,
  resolveTabLanguage,
} from '@/utils/editorLanguage'
import { DEFAULT_FILES_ROOT, normalizeRemotePath, parentRemotePath } from '@/utils/remotePath'
import type { OpenFilePayload } from '@/composables/files/useFilesEditor'

import type * as Monaco from 'monaco-editor'

const monaco = initMonaco()

export interface EditorTabState {
  id: string
  path: string
  name: string
  /** LF-normalised content for comparison; Monaco model holds the canonical content */
  content: string
  originalContent: string
  modified: boolean
  /** `null` = auto-detect from filename */
  languageId: string | null
  /** Original EOL detected from server response; preserved on save */
  eol: 'LF' | 'CRLF'
  /** sha256hex from last read/write; used for conflict detection on save */
  etag: string | null
  /** Unix mtime from last read */
  mtime: number | null
  /** Each tab owns its Monaco model; disposed when tab is closed */
  model: Monaco.editor.ITextModel | null
  /** Saved editor view state (scroll, cursor, selections) for this tab */
  viewState: Monaco.editor.ICodeEditorViewState | null
}

const props = defineProps<{
  serverId: number
  currentPath?: string
}>()

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

const snackbar = useSnackbar()

const FLOAT_KEY = 'nexus_file_editor_float'
const defaultW = Math.min(1120, Math.round(window.innerWidth * 0.88))
const defaultH = Math.min(760, Math.round(window.innerHeight * 0.82))

const { panelStyle, startDrag, startResize, clampToViewport } = useFloatingPanel(FLOAT_KEY, {
  x: Math.max(24, (window.innerWidth - defaultW) / 2),
  y: Math.max(24, (window.innerHeight - defaultH) / 2),
  w: defaultW,
  h: defaultH,
})

/** shallowRef：避免 Vue 深度代理 tab 内大字符串与 Monaco model 元数据 */
const tabs = shallowRef<EditorTabState[]>([])
const activeTabId = ref<string | null>(null)
const minimized = ref(false)
const maximized = ref(false)
const saving = ref(false)
const ready = ref(false)
const showCloseConfirm = ref(false)
const cursorLine = ref(1)
const cursorColumn = ref(1)
const treeBrowsePath = ref(props.currentPath || DEFAULT_FILES_ROOT)

/** 目录树根：与文件管理页路径对齐（宝塔默认 /www/wwwroot） */
const editorTreeRoot = computed(() => {
  const p = normalizeRemotePath(props.currentPath || DEFAULT_FILES_ROOT)
  if (p === '/' || p.startsWith(DEFAULT_FILES_ROOT)) return DEFAULT_FILES_ROOT
  return p
})

const wordWrap = ref(true)

// Conflict dialog state
const showConflictDialog = ref(false)
const conflictTabId = ref<string | null>(null)

const editorHost = ref<HTMLElement>()
let editor: Monaco.editor.IStandaloneCodeEditor | null = null
let persistContentTimer: ReturnType<typeof setTimeout> | null = null
let mountGeneration = 0

function bumpTabs() {
  tabs.value = tabs.value.slice()
}

function syncTabModified(tab: EditorTabState, modified: boolean) {
  if (tab.modified === modified) return
  tab.modified = modified
  bumpTabs()
}

const pendingCloseTabId = ref<string | null>(null)
const pendingClosePanel = ref(false)

const activeTab = computed(() => tabs.value.find((t) => t.id === activeTabId.value) ?? null)
const activeLanguage = computed(() => {
  if (!activeTab.value) return 'plaintext'
  return resolveTabLanguage(activeTab.value.name, activeTab.value.path, activeTab.value.languageId)
})

const activeLanguageChoice = computed({
  get: () => {
    const id = activeTab.value?.languageId
    return id == null || id === '' ? 'auto' : String(id)
  },
  set: (v: string | null) => {
    if (!activeTab.value) return
    const s = v == null ? 'auto' : String(v)
    activeTab.value.languageId = s === 'auto' ? null : s
  },
})

const floatContainerStyle = computed(() => (maximized.value ? undefined : panelStyle.value))

const closeConfirmTitle = computed(() =>
  pendingClosePanel.value ? '关闭编辑器' : '关闭文件',
)
const closeConfirmText = computed(() => {
  const tab = pendingCloseTabId.value
    ? tabs.value.find((t) => t.id === pendingCloseTabId.value)
    : tabs.value.find((t) => t.modified)
  const name = tab?.name ?? '文件'
  return pendingClosePanel.value
    ? `「${name}」等未保存的更改将丢失，确定吗？`
    : `「${name}」已修改但未保存，确定要关闭吗？`
})

const conflictTabName = computed(() => {
  if (!conflictTabId.value) return ''
  return tabs.value.find((t) => t.id === conflictTabId.value)?.name ?? ''
})

// ─────────────────────── helpers ───────────────────────

/** Strip CR to get LF-only content for internal comparison */
function stripCr(text: string): string {
  return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
}

/** Restore original EOL sequence for saving */
function serializeContent(content: string, eol: 'LF' | 'CRLF'): string {
  const lf = stripCr(content)
  return eol === 'CRLF' ? lf.replace(/\n/g, '\r\n') : lf
}

function tabLanguageId(tab: EditorTabState): string {
  return resolveTabLanguage(tab.name, tab.path, tab.languageId)
}

function tabId(path: string): string {
  return `${props.serverId}:${normalizeRemotePath(path)}`
}

function syncTreePathFromFile(filePath: string) {
  const parent = parentRemotePath(normalizeRemotePath(filePath))
  if (parent) treeBrowsePath.value = parent
}

function onTreeNavigate(path: string) {
  treeBrowsePath.value = normalizeRemotePath(path)
  emit('path-change', treeBrowsePath.value)
}

function onToolbarMouseDown(e: MouseEvent) {
  if (maximized.value) return
  const target = e.target as HTMLElement
  if (target.closest('button, .v-tab, .v-btn, input, a')) return
  startDrag(e)
}

function updateCursorStatus() {
  if (!editor) return
  const pos = editor.getPosition()
  cursorLine.value = pos?.lineNumber ?? 1
  cursorColumn.value = pos?.column ?? 1
}

/**
 * Persist view state; content sync is debounced unless `immediate` (save / tab switch).
 */
function persistEditorToActiveTab(immediate = false) {
  if (!editor || !activeTab.value) return
  const tab = activeTab.value
  const vs = editor.saveViewState()
  tab.viewState = vs ? markRaw(vs) : null

  const syncContent = () => {
    if (!editor || activeTab.value?.id !== tab.id) return
    const raw = editor.getValue()
    tab.content = stripCr(raw)
    syncTabModified(tab, tab.content !== tab.originalContent)
  }

  if (immediate) {
    if (persistContentTimer) {
      clearTimeout(persistContentTimer)
      persistContentTimer = null
    }
    syncContent()
    return
  }
  if (persistContentTimer) clearTimeout(persistContentTimer)
  persistContentTimer = setTimeout(syncContent, 250)
}

/** Teleport + v-if 首帧后挂载 Monaco，并重复 layout 直至容器有尺寸 */
async function scheduleMountEditor() {
  const gen = ++mountGeneration
  await nextTick()
  await nextTick()
  if (gen !== mountGeneration || !activeTab.value) return
  mountEditor()
  const relayout = (attempt = 0) => {
    if (gen !== mountGeneration || !editor) return
    editor.layout()
    const host = editorHost.value
    if (host && (host.clientWidth < 2 || host.clientHeight < 2) && attempt < 12) {
      setTimeout(() => relayout(attempt + 1), 50)
    }
  }
  requestAnimationFrame(() => relayout())
}

/**
 * Destroy only the editor instance (not the per-tab models).
 * Models are owned by tabs and disposed in removeTab / closeAllTabs.
 */
function destroyEditor() {
  mountGeneration++
  if (persistContentTimer) {
    clearTimeout(persistContentTimer)
    persistContentTimer = null
  }
  editor?.dispose()
  editor = null
  ready.value = false
}

/** Dispose all tab models and reset state (called on panel close) */
function closeAllTabs() {
  for (const tab of tabs.value) {
    tab.model?.dispose()
  }
  tabs.value = []
  activeTabId.value = null
  destroyEditor()
  maximized.value = false
  minimized.value = false
}

function onLanguageChoiceChange(choice: string | null) {
  const tab = activeTab.value
  if (!tab || !tab.model) return
  tab.languageId = choice === 'auto' || !choice ? null : choice
  monaco.editor.setModelLanguage(tab.model, tabLanguageId(tab))
}

/**
 * Switch the editor to show the given tab's model, restoring its view state.
 * The editor instance must already exist.
 */
function switchToTab(tab: EditorTabState) {
  if (!editor) return
  if (!tab.model) {
    // Lazily create model if missing (shouldn't happen, but defensive)
    const lang = tabLanguageId(tab)
    const content = stripCr(tab.content)
    tab.model = markRaw(monaco.editor.createModel(
      content,
      lang,
      monaco.Uri.parse(`nexus://${props.serverId}${tab.path}`),
    ))
  }
  editor.setModel(tab.model)
  if (tab.viewState) {
    editor.restoreViewState(tab.viewState)
  }
  monaco.editor.setModelLanguage(tab.model, tabLanguageId(tab))
  syncTreePathFromFile(tab.path)
  updateCursorStatus()
}

function mountEditor() {
  if (!editorHost.value || !activeTab.value) return

  destroyEditor()
  ready.value = false

  const tab = activeTab.value

  // Ensure the tab has its own model
  if (!tab.model) {
    const lang = tabLanguageId(tab)
    const content = stripCr(tab.content)
    tab.model = markRaw(monaco.editor.createModel(
      content,
      lang,
      monaco.Uri.parse(`nexus://${props.serverId}${tab.path}`),
    ))
  }

  editor = monaco.editor.create(editorHost.value, {
    ...buildMonacoEditorOptions(),
    model: tab.model,
    wordWrap: wordWrap.value ? 'on' : 'off',
    automaticLayout: true,
  })

  if (tab.viewState) {
    editor.restoreViewState(tab.viewState)
  }

  editor.onDidChangeModelContent(() => {
    persistEditorToActiveTab()
    updateCursorStatus()
  })

  editor.onDidChangeCursorPosition(() => {
    updateCursorStatus()
  })

  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
    void saveActive()
  })

  editor.addCommand(monaco.KeyCode.Escape, () => {
    if (maximized.value) maximized.value = false
  })

  // Alt+Z: toggle word wrap (VSCode convention)
  editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.KeyZ, () => {
    toggleWordWrap()
  })

  syncTreePathFromFile(tab.path)
  applyNexusEditorTheme(monaco)
  ready.value = true
  updateCursorStatus()
}

function onTabSelected(id: unknown) {
  if (typeof id !== 'string' || !id) return
  persistEditorToActiveTab(true)
  activeTabId.value = id
  const tab = tabs.value.find((t) => t.id === id)
  if (!tab) return
  if (editor) {
    switchToTab(tab)
  } else {
    void scheduleMountEditor()
  }
}

function restoreTab(id: string) {
  activeTabId.value = id
  minimized.value = false
  void nextTick(() => {
    clampToViewport()
    const tab = tabs.value.find((t) => t.id === id)
    if (!tab) return
    if (!editor) void scheduleMountEditor()
    else switchToTab(tab)
  })
}

function openFile(payload: OpenFilePayload) {
  const id = tabId(payload.path)
  const existing = tabs.value.find((t) => t.id === id)
  if (existing) {
    activeTabId.value = id
    minimized.value = false
    void scheduleMountEditor().then(() => {
      if (editor) switchToTab(existing)
    })
    return
  }

  try {
    const normalizedContent = stripCr(payload.content)
    const lang = resolveTabLanguage(payload.name, payload.path, null)
    const uri = monaco.Uri.parse(`nexus://${props.serverId}${payload.path}`)
    const existingModel = monaco.editor.getModel(uri)
    if (existingModel) existingModel.dispose()
    const model = markRaw(monaco.editor.createModel(normalizedContent, lang, uri))

    tabs.value = [
      ...tabs.value,
      {
        id,
        path: payload.path,
        name: payload.name,
        content: normalizedContent,
        originalContent: normalizedContent,
        modified: false,
        languageId: null,
        eol: payload.eol,
        etag: payload.etag,
        mtime: payload.mtime,
        model,
        viewState: null,
      },
    ]
    activeTabId.value = id
    minimized.value = false
    syncTreePathFromFile(payload.path)
    void scheduleMountEditor()
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '无法打开编辑器'
    snackbar(msg, 'error')
    console.error('[FileEditorWorkbench] openFile failed', e)
  }
}

function removeTab(id: string) {
  const idx = tabs.value.findIndex((t) => t.id === id)
  if (idx < 0) return

  const tab = tabs.value[idx]
  tab.model?.dispose()
  tabs.value = tabs.value.filter((t) => t.id !== id)

  if (tabs.value.length === 0) {
    activeTabId.value = null
    destroyEditor()
    maximized.value = false
    minimized.value = false
    emit('closed')
    return
  }
  if (activeTabId.value === id) {
    const next = tabs.value[Math.min(idx, tabs.value.length - 1)]
    activeTabId.value = next.id
    void scheduleMountEditor().then(() => {
      const t = tabs.value.find((x) => x.id === next.id)
      if (t && editor) switchToTab(t)
    })
  }
}

function requestCloseTab(id: string) {
  const tab = tabs.value.find((t) => t.id === id)
  if (!tab) return
  if (activeTabId.value === id) persistEditorToActiveTab(true)
  if (tab.modified) {
    pendingCloseTabId.value = id
    pendingClosePanel.value = false
    showCloseConfirm.value = true
    return
  }
  const wasActive = activeTabId.value === id
  if (wasActive) destroyEditor()
  removeTab(id)
  if (wasActive && activeTabId.value) {
    void scheduleMountEditor()
  }
}

function requestClosePanel() {
  persistEditorToActiveTab(true)
  if (tabs.value.some((t) => t.modified)) {
    pendingCloseTabId.value = null
    pendingClosePanel.value = true
    showCloseConfirm.value = true
    return
  }
  closeAllTabs()
  emit('closed')
}

function cancelClose() {
  showCloseConfirm.value = false
  pendingCloseTabId.value = null
  pendingClosePanel.value = false
}

function confirmDiscardClose() {
  showCloseConfirm.value = false
  if (pendingClosePanel.value) {
    closeAllTabs()
    emit('closed')
  } else if (pendingCloseTabId.value) {
    const id = pendingCloseTabId.value
    const wasActive = activeTabId.value === id
    if (wasActive) destroyEditor()
    removeTab(id)
    if (wasActive && activeTabId.value) {
      void scheduleMountEditor()
    }
  }
  pendingCloseTabId.value = null
  pendingClosePanel.value = false
}

async function confirmSaveClose() {
  const closePanel = pendingClosePanel.value
  const closeTabId = pendingCloseTabId.value
  await saveActive()
  showCloseConfirm.value = false
  pendingClosePanel.value = false
  pendingCloseTabId.value = null

  if (closePanel) {
    if (!tabs.value.some((t) => t.modified)) {
      closeAllTabs()
      emit('closed')
    }
    return
  }
  if (closeTabId) {
    const tab = tabs.value.find((t) => t.id === closeTabId)
    if (tab && !tab.modified) {
      const wasActive = activeTabId.value === closeTabId
      if (wasActive) destroyEditor()
      removeTab(closeTabId)
      if (wasActive && activeTabId.value) {
        void scheduleMountEditor()
      }
    }
  }
}

function toggleWordWrap() {
  wordWrap.value = !wordWrap.value
  editor?.updateOptions({ wordWrap: wordWrap.value ? 'on' : 'off' })
}

async function saveActive(forceEtag = false) {
  if (!props.serverId) {
    snackbar('请先选择服务器', 'error')
    return
  }
  persistEditorToActiveTab(true)
  const tab = activeTab.value
  if (!tab || !tab.modified) return

  const content = serializeContent(editor?.getValue() ?? tab.content, tab.eol)

  saving.value = true
  try {
    const res = await http.post<{ success: boolean; etag?: string | null }>('/sync/write-file', {
      server_id: props.serverId,
      path: tab.path,
      content,
      etag: forceEtag ? null : tab.etag,
    })
    const saved = stripCr(content)
    tab.content = saved
    tab.originalContent = saved
    syncTabModified(tab, false)
    tab.etag = res?.etag ?? null
    bumpTabs()
    snackbar('文件已保存')
    emit('saved', tab.path)
  } catch (e: unknown) {
    if (e instanceof ApiError && e.status === 409) {
      // Concurrent modification detected
      conflictTabId.value = tab.id
      showConflictDialog.value = true
      return
    }
    const msg =
      e instanceof ApiError ? e.message : e instanceof Error ? e.message : '保存失败'
    snackbar(msg, 'error')
  } finally {
    saving.value = false
  }
}

function cancelConflict() {
  showConflictDialog.value = false
  conflictTabId.value = null
}

async function forceOverwrite() {
  showConflictDialog.value = false
  const id = conflictTabId.value
  conflictTabId.value = null
  if (!id || activeTabId.value !== id) return
  await saveActive(true)
}

function discardConflict() {
  showConflictDialog.value = false
  const id = conflictTabId.value
  conflictTabId.value = null
  if (!id) return
  const tab = tabs.value.find((t) => t.id === id)
  if (!tab) return
  // Discard local changes: reset content to originalContent
  tab.content = tab.originalContent
  syncTabModified(tab, false)
  if (tab.model) {
    const eolContent = serializeContent(tab.originalContent, tab.eol)
    tab.model.setValue(stripCr(eolContent))
  }
  bumpTabs()
  snackbar('已放弃本地修改', 'info')
}

watch(
  () => props.currentPath,
  (path) => {
    if (path) treeBrowsePath.value = normalizeRemotePath(path)
  },
)

watch(maximized, () => {
  void nextTick(() => editor?.layout())
})

watch(minimized, (v) => {
  if (!v) void nextTick(() => editor?.layout())
})

function hasOpenTabs(): boolean {
  return tabs.value.length > 0
}

defineExpose({
  openFile,
  hasOpenTabs,
})

onBeforeUnmount(() => {
  // Dispose all models before the component is torn down
  for (const tab of tabs.value) {
    tab.model?.dispose()
  }
  destroyEditor()
})
</script>

<style scoped>
.file-editor-float {
  position: fixed;
  z-index: 2400;
  display: flex;
  flex-direction: column;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}
.file-editor-float--maximized {
  inset: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  left: 0 !important;
  top: 0 !important;
  border-radius: 0;
}
.file-editor-panel {
  overflow: hidden;
  position: relative;
  height: 100%;
  background: rgb(var(--v-theme-surface));
}
/* ── Toolbar ── */
.file-editor-toolbar {
  height: 40px;
  min-height: 40px;
  background: rgb(var(--v-theme-surface));
  border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
  flex-shrink: 0;
}
.file-editor-toolbar--draggable {
  cursor: grab;
  user-select: none;
}
.file-editor-toolbar--draggable:active {
  cursor: grabbing;
}
.file-editor-drag-hint {
  cursor: grab;
  flex-shrink: 0;
}
.file-editor-tabs {
  min-width: 0;
  height: 40px;
}
.file-editor-tabs :deep(.v-tab) {
  min-width: 0;
  min-height: 40px;
  padding-inline: 6px;
  font-size: 12px;
}
.file-editor-tabs :deep(.v-tab--selected) {
  background: rgba(var(--v-theme-primary), 0.08);
}
.file-editor-lang-select {
  width: 110px;
  min-width: 90px;
  font-size: 12px;
}
.file-editor-lang-select :deep(.v-field__input) {
  font-size: 12px;
  padding-top: 0;
  padding-bottom: 0;
  min-height: 28px;
}
/* ── Main area ── */
.file-editor-main {
  min-height: 0;
}
.file-editor-sidebar {
  width: 210px;
  border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
  overflow: hidden;
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 2;
  flex-shrink: 0;
}
.file-editor-content {
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  position: relative;
  z-index: 0;
}
.file-editor-sidebar :deep(.file-dir-tree) {
  min-height: 0;
  max-height: none;
  height: 100%;
  border: none;
  border-radius: 0;
  box-shadow: none;
}
.file-editor-body {
  min-height: 120px;
  background: #1e1e1e;
  overflow: hidden;
}
.file-editor-host {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
/* ── Status bar ── */
.file-editor-status {
  background: rgb(var(--v-theme-primary));
  color: rgba(255, 255, 255, 0.9);
  border-top: none;
  flex-shrink: 0;
  height: 24px;
  font-size: 11px;
  line-height: 24px;
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}
.file-editor-status .text-primary {
  color: rgba(255, 255, 255, 1) !important;
  font-weight: 600;
}
.file-editor-status .text-warning {
  color: #FFD740 !important;
}
.file-editor-status .text-medium-emphasis {
  opacity: 0.75;
}
.text-mono {
  font-family: ui-monospace, monospace;
}
/* ── Resize handle ── */
.file-editor-progress {
  z-index: 2;
}
.file-editor-resize {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 18px;
  height: 18px;
  cursor: nwse-resize;
  z-index: 3;
  background: linear-gradient(
    135deg,
    transparent 50%,
    rgba(var(--v-theme-on-surface), 0.25) 50%
  );
}
/* ── Minimized dock ── */
.file-editor-dock {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 2390;
  max-width: 960px;
  margin: 0 auto;
  padding: 8px 12px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
}
</style>
