<template>
  <Teleport to="body">
    <div
      v-if="tabs.length > 0 && !minimized"
      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">
        <v-toolbar
          density="compact"
          color="surface"
          class="flex-grow-0 file-editor-toolbar"
          :class="{ 'file-editor-toolbar--draggable': !maximized }"
          @mousedown="onToolbarMouseDown"
        >
          <v-icon v-if="!maximized" size="18" class="mr-1 text-medium-emphasis file-editor-drag-hint">
            mdi-drag
          </v-icon>
          <v-tabs
            v-model="activeTabId"
            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"
            >
              <span class="text-truncate" style="max-width: 140px;">{{ tab.name }}</span>
              <span v-if="tab.modified" class="text-error ml-1">●</span>
              <v-btn
                icon="mdi-close"
                size="x-small"
                variant="text"
                class="ml-1"
                @click.stop="requestCloseTab(tab.id)"
              />
            </v-tab>
          </v-tabs>
      <v-select
        v-model="activeLanguageChoice"
        :items="EDITOR_LANGUAGE_OPTIONS"
        item-title="title"
        item-value="value"
        density="compact"
        variant="outlined"
        hide-details
        class="file-editor-lang-select flex-grow-0"
        label="语法"
        @mousedown.stop
        @update:model-value="onLanguageChoiceChange"
      />
      <v-spacer />
      <v-btn
        size="small"
        variant="tonal"
        prepend-icon="mdi-content-save"
            :loading="saving"
            :disabled="!activeTab?.modified || !ready"
            @mousedown.stop
            @click="saveActive"
          >
            保存
          </v-btn>
          <v-btn
            size="small"
            variant="text"
            :icon="maximized ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
            :title="maximized ? '退出全屏' : '全屏'"
            @mousedown.stop
            @click="maximized = !maximized"
          />
          <v-btn
            size="small"
            variant="text"
            icon="mdi-window-minimize"
            title="最小化"
            @mousedown.stop
            @click="minimized = true"
          />
          <v-btn
            size="small"
            variant="text"
            icon="mdi-close"
            title="关闭编辑器"
            @mousedown.stop
            @click="requestClosePanel"
          />
        </v-toolbar>

        <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"
              :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>
            <div class="file-editor-status text-caption px-3 py-1 d-flex flex-wrap ga-3">
              <span>行 {{ cursorLine }}, 列 {{ cursorColumn }}</span>
              <span>{{ activeLanguage }}</span>
              <span class="text-truncate flex-grow-1">{{ activeTab?.path ?? '' }}</span>
            </div>
          </div>
        </div>

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

    <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-error 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>

  <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>
</template>

<script setup lang="ts">
import { ref, computed, watch, nextTick, onBeforeUnmount, defineAsyncComponent } from 'vue'
import { ApiError, http } from '@/api'
import { useSnackbar } from '@/composables/useSnackbar'
import { useFloatingPanel } from '@/composables/useFloatingPanel'
import { applyNexusEditorTheme, buildMonacoEditorOptions, initMonaco } from '@/monaco/initMonaco'
import {
  EDITOR_LANGUAGE_OPTIONS,
  detectEditorLanguage,
  resolveTabLanguage,
} from '@/utils/editorLanguage'
import { normalizeRemotePath, parentRemotePath } from '@/utils/remotePath'

const FileDirectoryTree = defineAsyncComponent(() => import('@/components/FileDirectoryTree.vue'))

import type * as Monaco from 'monaco-editor'

const monaco = initMonaco()

export interface EditorTabState {
  id: string
  path: string
  name: string
  content: string
  originalContent: string
  modified: boolean
  /** `auto` 或 null 表示按文件名检测 */
  languageId: string | 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,
})

const tabs = ref<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 || '/')

const editorHost = ref<HTMLElement>()
let editor: Monaco.editor.IStandaloneCodeEditor | null = null
let model: Monaco.editor.ITextModel | null = null
let layoutObserver: ResizeObserver | null = null

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: () => activeTab.value?.languageId ?? 'auto',
  set: (v: string | null) => {
    if (!activeTab.value) return
    activeTab.value.languageId = v === 'auto' ? null : v
  },
})

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}」已修改但未保存，确定要关闭吗？`
})

function normalizeEol(text: string): string {
  return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
}

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

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

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 persistEditorToActiveTab() {
  if (!editor || !activeTab.value) return
  const value = normalizeEol(editor.getValue())
  activeTab.value.content = value
  activeTab.value.modified = value !== activeTab.value.originalContent
}

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

function destroyEditor() {
  layoutObserver?.disconnect()
  layoutObserver = null
  editor?.dispose()
  editor = null
  model?.dispose()
  model = null
  ready.value = false
}

function applyTabToEditor(tab: EditorTabState) {
  if (!editor) return
  const lang = tabLanguageId(tab)
  const content = normalizeEol(tab.content)
  if (model) {
    model.setValue(content)
    monaco.editor.setModelLanguage(model, lang)
  } else {
    model = monaco.editor.createModel(content, lang, monaco.Uri.file(tab.path))
    editor.setModel(model)
  }
  syncTreePathFromFile(tab.path)
  updateCursorStatus()
}

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

  destroyEditor()
  ready.value = false

  const tab = activeTab.value
  const lang = tabLanguageId(tab)
  const content = normalizeEol(tab.content)

  model = monaco.editor.createModel(content, lang, monaco.Uri.file(tab.path))
  editor = monaco.editor.create(editorHost.value, {
    ...buildMonacoEditorOptions(),
    model,
  })

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

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

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

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

  layoutObserver = new ResizeObserver(() => {
    editor?.layout()
  })
  layoutObserver.observe(editorHost.value)

  syncTreePathFromFile(tab.path)
  applyNexusEditorTheme(monaco)
  ready.value = true
  updateCursorStatus()
  requestAnimationFrame(() => editor?.layout())
}

function onTabSelected(id: unknown) {
  if (typeof id !== 'string' || !id) return
  persistEditorToActiveTab()
  activeTabId.value = id
  const tab = tabs.value.find((t) => t.id === id)
  if (tab && editor) {
    applyTabToEditor(tab)
  } else if (tab) {
    nextTick(() => mountEditor())
  }
}

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

function openFile(payload: { path: string; name: string; content: string }) {
  const id = tabId(payload.path)
  const normalized = normalizeEol(payload.content)
  const existing = tabs.value.find((t) => t.id === id)
  if (existing) {
    activeTabId.value = id
    minimized.value = false
    nextTick(() => {
      if (!editor) mountEditor()
      else applyTabToEditor(existing)
    })
    return
  }

  tabs.value.push({
    id,
    path: payload.path,
    name: payload.name,
    content: normalized,
    originalContent: normalized,
    modified: false,
    languageId: null,
  })
  activeTabId.value = id
  minimized.value = false
  syncTreePathFromFile(payload.path)
  nextTick(() => mountEditor())
}

function removeTab(id: string) {
  const idx = tabs.value.findIndex((t) => t.id === id)
  if (idx < 0) return
  tabs.value.splice(idx, 1)
  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
    nextTick(() => applyTabToEditor(next))
  }
}

function requestCloseTab(id: string) {
  const tab = tabs.value.find((t) => t.id === id)
  if (!tab) return
  if (activeTabId.value === id) persistEditorToActiveTab()
  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) {
    nextTick(() => mountEditor())
  }
}

function requestClosePanel() {
  persistEditorToActiveTab()
  if (tabs.value.some((t) => t.modified)) {
    pendingCloseTabId.value = null
    pendingClosePanel.value = true
    showCloseConfirm.value = true
    return
  }
  tabs.value = []
  activeTabId.value = null
  destroyEditor()
  maximized.value = false
  minimized.value = false
  emit('closed')
}

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

function confirmDiscardClose() {
  showCloseConfirm.value = false
  if (pendingClosePanel.value) {
    tabs.value = []
    activeTabId.value = null
    destroyEditor()
    maximized.value = false
    minimized.value = false
    emit('closed')
  } else if (pendingCloseTabId.value) {
    const id = pendingCloseTabId.value
    destroyEditor()
    removeTab(id)
    nextTick(() => {
      if (activeTabId.value) mountEditor()
    })
  }
  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)) {
      tabs.value = []
      activeTabId.value = null
      destroyEditor()
      maximized.value = false
      minimized.value = false
      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) {
        nextTick(() => mountEditor())
      }
    }
  }
}

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

  saving.value = true
  try {
    await http.post('/sync/write-file', {
      server_id: props.serverId,
      path: tab.path,
      content: editor.getValue(),
    })
    const saved = normalizeEol(editor.getValue())
    tab.content = saved
    tab.originalContent = saved
    tab.modified = false
    snackbar('文件已保存')
    emit('saved', tab.path)
  } catch (e: unknown) {
    const msg =
      e instanceof ApiError ? e.message : e instanceof Error ? e.message : '保存失败'
    snackbar(msg, 'error')
  } finally {
    saving.value = false
  }
}

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

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

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

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

defineExpose({
  openFile,
  hasOpenTabs,
})

onBeforeUnmount(() => {
  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));
}
.file-editor-toolbar--draggable {
  cursor: grab;
  user-select: none;
}
.file-editor-toolbar--draggable:active {
  cursor: grabbing;
}
.file-editor-main {
  min-height: 0;
}
.file-editor-sidebar {
  width: 220px;
  border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.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;
}
.file-editor-host {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.file-editor-status {
  background: #252526;
  color: #cccccc;
  border-top: 1px solid #3c3c3c;
  flex-shrink: 0;
}
.file-editor-lang-select {
  max-width: 132px;
  min-width: 108px;
}
.file-editor-progress {
  z-index: 2;
}
.file-editor-tabs :deep(.v-tab) {
  min-width: 0;
  padding-inline: 8px;
}
.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%
  );
}
.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>
