<template>
  <v-container
    fluid
    class="pa-0 d-flex flex-column terminal-root"
    :class="theme.global.current.value.dark ? 'terminal-root--dark' : 'terminal-root--light'"
  >
    <!-- ── 终端/命令栏右键菜单（中文、加大，避免浏览器英文菜单） ── -->
    <v-menu
      v-model="showTermMenu"
      :target="[menuX, menuY]"
      :location="termMenuLocation"
      content-class="terminal-ctx-menu"
    >
      <v-list density="comfortable" nav class="py-1">
        <v-list-item prepend-icon="mdi-content-copy" rounded="lg" @click="termCopy">
          <v-list-item-title>复制</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-content-paste" rounded="lg" @click="termPaste">
          <v-list-item-title>粘贴</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-select-all" rounded="lg" @click="termSelectAll">
          <v-list-item-title>全选</v-list-item-title>
        </v-list-item>
        <template v-if="termMenuContext === 'terminal'">
          <v-divider class="my-1" />
          <v-list-item prepend-icon="mdi-delete-sweep" rounded="lg" @click="termClear">
            <v-list-item-title>清屏</v-list-item-title>
          </v-list-item>
          <v-list-item
            prepend-icon="mdi-logout"
            rounded="lg"
            :disabled="!activeSession"
            @click="termDisconnect"
          >
            <v-list-item-title class="text-error">断开连接</v-list-item-title>
          </v-list-item>
        </template>
      </v-list>
    </v-menu>

    <!-- ── App Bar (replaces hardcoded height calc) ── -->
    <div class="d-flex flex-column flex-grow-1" style="min-height: 0">
      <!-- ── Tab Bar ── -->
      <div class="d-flex align-center px-2 py-1 shrink-0" style="background: rgb(var(--v-theme-surface)); border-bottom: 1px solid rgb(var(--v-theme-surface-variant))">
        <div class="d-flex align-center ga-1 overflow-x-auto flex-grow-1">
          <v-chip
            v-for="tab in sessions"
            :key="tab.id"
            :variant="tab.id === activeSessionId ? 'flat' : 'outlined'"
            :color="tab.id === activeSessionId ? 'primary' : 'default'"
            size="small"
            label
            class="cursor-pointer"
            @click="switchTab(tab.id)"
          >
            <template #prepend>
              <v-icon size="8" :color="tab.status === 'connected' ? 'success' : tab.status === 'connecting' ? 'warning' : 'grey'">mdi-circle</v-icon>
            </template>
            <span class="text-truncate" style="max-width: 100px">{{ tab.serverName }}</span>
            <v-icon end size="12" class="ml-1" @click.stop="closeTab(tab.id)">mdi-close</v-icon>
          </v-chip>
          <v-btn size="small" variant="tonal" prepend-icon="mdi-plus" @click="newSession()">
            新建会话
          </v-btn>
        </div>
      </div>

      <!-- ── Toolbar（有会话时才显示，避免空页出现「... / 断开」） ── -->
      <div
        v-if="sessions.length > 0"
        class="d-flex align-center justify-space-between px-4 py-1 shrink-0"
        style="background: rgb(var(--v-theme-surface)); border-bottom: 1px solid rgb(var(--v-theme-surface-variant)); min-height: 32px"
      >
        <div class="d-flex align-center ga-3">
          <span class="text-body-2 font-weight-medium">{{ activeSession?.serverName || '...' }}</span>
          <span v-if="activeSession?.cwd" class="text-caption text-medium-emphasis font-mono" style="max-width: 200px">{{ activeSession.cwd }}</span>
          <v-chip
            v-if="activeSession"
            :color="sessionStatusColor(activeSession)"
            variant="tonal"
            size="x-small"
            label
          >
            <template #prepend>
              <v-icon start size="8">mdi-circle</v-icon>
            </template>
            {{ sessionStatusLabel(activeSession) }}
          </v-chip>
          <span v-if="activeSession?.uptime" class="text-caption text-medium-emphasis">{{ activeSession.uptime }}</span>
          <span v-if="activeSession?.pingMs !== null && activeSession?.pingMs !== undefined" class="text-caption text-medium-emphasis">{{ activeSession.pingMs }}ms</span>
        </div>
        <div class="d-flex align-center ga-2 terminal-toolbar-actions">
          <span class="text-body-2 text-medium-emphasis shrink-0">字号</span>
          <v-btn variant="tonal" size="small" @click="changeFontSize(-1)">缩小</v-btn>
          <span class="text-body-2 font-weight-medium" style="min-width: 1.5rem; text-align: center">{{ fontSize }}</span>
          <v-btn variant="tonal" size="small" @click="changeFontSize(1)">放大</v-btn>
          <v-select
            v-model="scrollback"
            :items="scrollbackOptions"
            item-title="title"
            item-value="value"
            label="回滚行数"
            density="comfortable"
            hide-details
            variant="outlined"
            class="terminal-toolbar-scrollback"
            @update:model-value="changeScrollback"
          />
          <v-btn variant="tonal" size="small" prepend-icon="mdi-fullscreen" @click="toggleFullscreen">
            全屏
          </v-btn>
          <v-btn variant="tonal" size="small" color="error" prepend-icon="mdi-link-off" @click="disconnect">
            断开
          </v-btn>
        </div>
      </div>

      <!-- ── Terminal Area ── -->
      <div ref="termContainer" class="flex-grow-1" style="position: relative; min-height: 0" @contextmenu="onTermContext">
        <div
          v-for="tab in sessions"
          :key="tab.id"
          :ref="(el: any) => setTermRef(el, tab.id)"
          :style="{
            position: 'absolute',
            inset: 0,
            zIndex: tab.id === activeSessionId ? 1 : 0,
            pointerEvents: tab.id === activeSessionId ? 'auto' : 'none',
            opacity: tab.id === activeSessionId ? 1 : 0,
          }"
        />

        <!-- Empty: 内嵌服务器列表（设计 §空状态），不与对话框叠层 -->
        <div
          v-if="sessions.length === 0 && !showDisconnectOverlay && !showServerDialog"
          class="d-flex flex-column align-center justify-center fill-height pa-6"
        >
          <v-icon size="56" color="primary" class="mb-3">mdi-console-network</v-icon>
          <div class="text-h6 mb-1 text-center">WebSSH 终端</div>
          <div class="text-body-2 text-medium-emphasis mb-4 text-center">
            选择一台服务器开始会话，或从服务器页点击「终端」
          </div>
          <v-card border rounded="lg" class="empty-server-picker" max-width="520" width="100%">
            <v-card-text class="pa-3">
              <v-text-field
                v-model="serverSearch"
                prepend-inner-icon="mdi-magnify"
                placeholder="搜索服务器…"
                variant="outlined"
                density="compact"
                hide-details
                class="mb-2"
              />
              <v-list density="compact" class="empty-server-picker__list" nav>
                <v-list-item
                  v-for="s in filteredServers"
                  :key="s.id"
                  :title="s.name"
                  :subtitle="s.domain"
                  rounded="lg"
                  @click="newSession(s.id)"
                >
                  <template #prepend>
                    <v-icon :color="s.is_online ? 'success' : 'grey'" size="8">mdi-circle</v-icon>
                  </template>
                </v-list-item>
                <v-list-item v-if="filteredServers.length === 0" class="text-medium-emphasis">
                  暂无匹配的服务器
                </v-list-item>
              </v-list>
            </v-card-text>
          </v-card>
        </div>

        <!-- Disconnect overlay -->
        <v-overlay
          :model-value="showDisconnectOverlay"
          class="d-flex align-center justify-center"
          persistent
        >
          <div class="text-center">
            <div class="text-h6 mb-2">{{ disconnectMsg }}</div>
            <div v-if="reconnectCountdown" class="text-body-2 text-medium-emphasis mb-4">{{ reconnectCountdown }}</div>
            <div class="d-flex ga-3 justify-center">
              <v-btn color="primary" variant="flat" @click="reconnect">重新连接</v-btn>
              <v-btn variant="outlined" @click="router.push('/servers')">返回列表</v-btn>
            </div>
          </div>
        </v-overlay>
      </div>

      <!-- ── Quick Commands（仅执行；管理见系统设置） ── -->
      <div class="terminal-quick-bar shrink-0">
        <span class="terminal-quick-bar__label">快捷命令</span>
        <div class="d-flex align-center ga-1 flex-grow-1 overflow-x-auto">
          <v-chip
            v-for="cmd in quickCmds"
            :key="cmd.id"
            size="small"
            :variant="cmd.is_builtin ? 'outlined' : 'flat'"
            :color="cmd.is_builtin ? 'default' : 'primary'"
            label
            class="cursor-pointer"
            @click="execQuickCmd(cmd.cmd)"
          >
            {{ cmd.name }}
          </v-chip>
        </div>
        <v-btn
          size="small"
          variant="text"
          prepend-icon="mdi-cog-outline"
          :to="{ path: '/settings', hash: '#terminal-quick-commands' }"
        >
          管理
        </v-btn>
      </div>

      <!-- ── Command Bar（Shell 语法高亮，约 3 倍高度） ── -->
      <div
        class="d-flex align-start ga-2 px-3 py-2 shrink-0 cmd-bar cmd-bar--tall"
        :class="{ 'opacity-50 pointer-events-none': !activeSession?.ws }"
        @contextmenu="onCmdContext"
      >
        <span class="cmd-bar__prompt font-mono text-primary shrink-0">❯</span>
        <div class="flex-grow-1 cmd-bar__input-wrap">
          <ShellHighlightField
            v-model="cmdInput"
            multiline
            :rows="3"
            size="large"
            placeholder="输入命令，Enter 发送，Shift+Enter 换行"
            :disabled="!activeSession?.ws"
            @keydown="onCmdKeydown"
          />
        </div>
        <v-btn size="default" variant="tonal" color="primary" class="mt-1" @click="sendCmd" :disabled="!activeSession?.ws">发送</v-btn>
        <div class="terminal-hotkeys shrink-0 pl-3">
          <div class="text-body-2 font-weight-medium text-medium-emphasis mb-2">快捷键</div>
          <div class="d-flex flex-column ga-2">
            <v-btn size="default" variant="outlined" block @click="sendCtrl('c')">中断 (Ctrl+C)</v-btn>
            <v-btn size="default" variant="outlined" block @click="sendCtrl('d')">退出 (Ctrl+D)</v-btn>
            <v-btn size="default" variant="outlined" block @click="sendKey('\t')">补全 (Tab)</v-btn>
          </div>
        </div>
      </div>
    </div>

    <!-- ── Server Selector Dialog ── -->
    <v-dialog v-model="showServerDialog" max-width="400">
      <v-card border>
        <v-card-title>选择服务器</v-card-title>
        <v-card-text>
          <v-text-field v-model="serverSearch" prepend-inner-icon="mdi-magnify" placeholder="搜索…" variant="outlined" density="compact" class="mb-3" />
          <v-list density="compact" max-height="300" style="overflow-y: auto">
            <v-list-item
              v-for="s in filteredServers"
              :key="s.id"
              :title="s.name"
              :subtitle="s.domain"
              @click="newSession(s.id); showServerDialog = false"
            >
              <template #prepend>
                <v-icon :color="s.is_online ? 'success' : 'grey'" size="8">mdi-circle</v-icon>
              </template>
            </v-list-item>
          </v-list>
        </v-card-text>
      </v-card>
    </v-dialog>

  </v-container>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick, inject, type Ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTheme } from 'vuetify'
import { http } from '@/api'
import { useAuthStore } from '@/stores/auth'
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { WebLinksAddon } from '@xterm/addon-web-links'
import '@xterm/xterm/css/xterm.css'
import { useSnackbar } from '@/composables/useSnackbar'
import { buildWebSocketUrl } from '@/utils/wsUrl'
import ShellHighlightField from '@/components/ShellHighlightField.vue'
import { sortQuickCommands, type QuickCmdItem } from '@/composables/useTerminalQuickCommands'

const route = useRoute()
const router = useRouter()
const theme = useTheme()
const auth = useAuthStore()
const snackbar = useSnackbar()
const nexusDrawer = inject<Ref<boolean>>('nexusDrawer', ref(true))

type SessionStatus = 'idle' | 'connecting' | 'connected' | 'error'

function sanitizeRemotePath(raw: string): string {
  const p = (raw || '').trim()
  return p && /^[^\x00-\x1f\x7f"'`\\]+$/.test(p) ? p : ''
}

// ── localStorage keys ──
const K_FONT = 'nexus_term_fontSize'
const K_SCROLL = 'nexus_term_scrollback'
const K_HIST = 'nexus_term_history'

// ── Session state ──
interface TermSession {
  id: string
  serverId: number
  serverName: string
  ws: WebSocket | null
  term: Terminal | null
  fitAddon: FitAddon | null
  resizeObserver: ResizeObserver | null
  cwd: string
  cmdHistory: string[]
  cmdIdx: number
  // Per-session state (was global before)
  reconnectAttempt: number
  reconnectTimer: ReturnType<typeof setTimeout> | null
  pingTimer: ReturnType<typeof setInterval> | null
  connectionStartTime: number
  uptime: string
  pingMs: number | null
  initialCdSent: boolean
  pendingPath: string
  manualDisconnect: boolean
  status: SessionStatus
}

const sessions = ref<TermSession[]>([])
const activeSessionId = ref('')
const termContainer = ref<HTMLElement | null>(null)
const termRefs = new Map<string, HTMLElement>()

function refitAllTerminals() {
  sessions.value.forEach((s) => {
    try {
      s.fitAddon?.fit()
    } catch {
      /* container size 0 during transition */
    }
  })
}

/** 侧栏开合 → v-main padding 变化 → 动画结束后 refit xterm */
watch(nexusDrawer, () => {
  nextTick(() => {
    refitAllTerminals()
    setTimeout(refitAllTerminals, 220)
  })
})

const activeSession = computed(() =>
  sessions.value.find(s => s.id === activeSessionId.value) || null
)
const activeIdx = computed(() =>
  sessions.value.findIndex(s => s.id === activeSessionId.value)
)

// ── UI state ──
const fontSize = ref(parseInt(localStorage.getItem(K_FONT) || '14'))
const scrollback = ref(parseInt(localStorage.getItem(K_SCROLL) || '1000'))
const showDisconnectOverlay = ref(false)
const disconnectMsg = ref('连接已断开')
const reconnectCountdown = ref('')
const cmdInput = ref('')
const showServerDialog = ref(false)
const serverSearch = ref('')
const servers = ref<{ id: number; name: string; domain: string; is_online: boolean }[]>([])

// ── Context menu ──
const showTermMenu = ref(false)
const menuX = ref(0)
const menuY = ref(0)
const termMenuContext = ref<'terminal' | 'command'>('terminal')
const termMenuLocation = computed(() => (menuY.value > window.innerHeight * 0.55 ? 'top end' : 'bottom end'))

const scrollbackOptions = [
  { title: '1 千行', value: 1000 },
  { title: '5 千行', value: 5000 },
  { title: '1 万行', value: 10000 },
  { title: '5 万行', value: 50000 },
]

// ── Quick commands（只读展示，管理在系统设置） ──
const quickCmds = ref<QuickCmdItem[]>([])
const filteredServers = computed(() => {
  const q = serverSearch.value.toLowerCase().trim()
  return servers.value.filter(s => !q || s.name.toLowerCase().includes(q) || s.domain.toLowerCase().includes(q))
})

function sessionStatusLabel(s: TermSession): string {
  switch (s.status) {
    case 'connecting': return '连接中…'
    case 'connected': return '已连接'
    case 'error': return '连接失败'
    default: return '未连接'
  }
}

function sessionStatusColor(s: TermSession): string {
  switch (s.status) {
    case 'connecting': return 'warning'
    case 'connected': return 'success'
    case 'error': return 'error'
    default: return 'grey'
  }
}

function sendCdToSession(s: TermSession, path: string) {
  const dir = sanitizeRemotePath(path)
  if (!dir || !s.ws || s.ws.readyState !== WebSocket.OPEN) return
  s.ws.send(JSON.stringify({ type: 'DATA', data: `cd ${dir}\r` }))
  s.initialCdSent = true
}

async function applyRouteQuery() {
  const id = Number(route.query.server_id) || 0
  const path = sanitizeRemotePath(String(route.query.path || ''))
  const forceNew = route.query.new_tab === '1' || route.query.new_tab === 'true'

  if (!id) return

  if (!forceNew) {
    const existing = sessions.value.find(s => s.serverId === id)
    if (existing) {
      await switchTab(existing.id)
      if (path) sendCdToSession(existing, path)
      return
    }
  }
  await newSession(id, { path })
}

function onCmdKeydown(e: KeyboardEvent) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    sendCmd()
    return
  }
  if (e.key === 'ArrowUp') {
    e.preventDefault()
    cmdHistoryUp()
    return
  }
  if (e.key === 'ArrowDown') {
    e.preventDefault()
    cmdHistoryDown()
  }
}

// ── Terminal ref management ──
function setTermRef(el: any, sessionId: string) {
  if (el) termRefs.set(sessionId, el as HTMLElement)
}

// ── Create xterm instance ──
function createTerminal(): Terminal {
  const isDark = theme.global.current.value.dark
  return new Terminal({
    cursorBlink: true,
    cursorStyle: 'bar',
    fontSize: fontSize.value,
    scrollback: scrollback.value,
    fontFamily: '"Cascadia Code","Fira Code","JetBrains Mono",Menlo,monospace',
    theme: isDark ? {
      background: '#0b1120',
      foreground: '#e2e8f0',
      cursor: '#7c8bf4',
      selectionBackground: '#334155',
      black: '#1e293b', red: '#f87171', green: '#4ade80', yellow: '#fbbf24',
      blue: '#60a5fa', magenta: '#c084fc', cyan: '#22d3ee', white: '#e2e8f0',
      brightBlack: '#64748b', brightRed: '#fca5a5', brightGreen: '#86efac',
      brightYellow: '#fde047', brightBlue: '#93c5fd', brightMagenta: '#d8b4fe',
      brightCyan: '#67e8f9', brightWhite: '#f8fafc',
    } : {
      background: '#f8fafc',
      foreground: '#0f172a',
      cursor: '#4f46e5',
      selectionBackground: '#bfdbfe',
      black: '#1e293b', red: '#dc2626', green: '#16a34a', yellow: '#ca8a04',
      blue: '#2563eb', magenta: '#9333ea', cyan: '#0891b2', white: '#f1f5f9',
      brightBlack: '#64748b', brightRed: '#ef4444', brightGreen: '#22c55e',
      brightYellow: '#eab308', brightBlue: '#3b82f6', brightMagenta: '#a855f7',
      brightCyan: '#06b6d4', brightWhite: '#ffffff',
    },
    allowProposedApi: true,
    drawBoldTextInBrightColors: true,
  })
}

// ── Load global command history from localStorage ──
function loadGlobalHistory(): string[] {
  try {
    return JSON.parse(localStorage.getItem(K_HIST) || '[]')
  } catch {
    return []
  }
}

// ── Tab management ──
async function switchTab(sessionId: string) {
  activeSessionId.value = sessionId
  await nextTick()
  const s = sessions.value.find(t => t.id === sessionId)
  if (s?.fitAddon) {
    setTimeout(() => { try { s.fitAddon!.fit() } catch { /* size 0 */ } }, 50)
  }
  if (s?.term) s.term.focus()
  updateUptimeDisplay(s)
}

async function closeTab(sessionId: string) {
  const idx = sessions.value.findIndex(s => s.id === sessionId)
  if (idx < 0) return

  const s = sessions.value[idx]
  // Clean up per-session resources
  if (s.reconnectTimer) { clearTimeout(s.reconnectTimer); s.reconnectTimer = null }
  if (s.pingTimer) { clearInterval(s.pingTimer); s.pingTimer = null }
  if (s.ws) { try { s.ws.close(1000) } catch { /* already closed */ } s.ws = null }
  if (s.term) { try { s.term.dispose() } catch { /* already disposed */ } s.term = null }
  if (s.resizeObserver) { try { s.resizeObserver.disconnect() } catch { /* ignore */ } s.resizeObserver = null }
  termRefs.delete(sessionId)
  sessions.value.splice(idx, 1)

  if (sessions.value.length === 0) {
    activeSessionId.value = ''
    showDisconnectOverlay.value = true
    disconnectMsg.value = '所有会话已关闭'
    return
  }
  const newIdx = Math.min(idx, sessions.value.length - 1)
  await switchTab(sessions.value[newIdx].id)
}

async function newSession(serverId?: number, opts?: { path?: string }) {
  if (sessions.value.length >= 10) {
    snackbar('最多10个标签', 'warning')
    return
  }
  if (!serverId) {
    showServerDialog.value = true
    return
  }

  // Check duplicate — offer to create new or switch
  const existing = sessions.value.find(s => s.serverId === serverId)
  if (existing) {
    await switchTab(existing.id)
    return
  }

  const server = servers.value.find(s => s.id === serverId)
  const name = server ? server.name : `#${serverId}`
  const sid = `term_${Date.now()}`

  // Load global command history for this session
  const globalHist = loadGlobalHistory()

  const session: TermSession = {
    id: sid,
    serverId,
    serverName: name,
    ws: null,
    term: null,
    fitAddon: null,
    resizeObserver: null,
    cwd: '',
    cmdHistory: [...globalHist],
    cmdIdx: -1,
    reconnectAttempt: 0,
    reconnectTimer: null,
    pingTimer: null,
    connectionStartTime: 0,
    uptime: '',
    pingMs: null,
    initialCdSent: false,
    pendingPath: sanitizeRemotePath(opts?.path || ''),
    manualDisconnect: false,
    status: 'idle',
  }
  sessions.value.push(session)
  showDisconnectOverlay.value = false

  await switchTab(sid)
  await nextTick()

  // Create terminal and attach to DOM
  const container = termRefs.get(sid)
  if (!container) return

  const term = createTerminal()
  const fitAddon = new FitAddon()
  term.loadAddon(fitAddon)
  term.loadAddon(new WebLinksAddon())
  term.open(container)

  session.term = term
  session.fitAddon = fitAddon

  // Setup terminal events
  term.onData((data) => {
    const s = sessions.value.find(t => t.id === sid)
    if (s?.ws?.readyState === WebSocket.OPEN) {
      s.ws.send(JSON.stringify({ type: 'DATA', data }))
    }
  })

  term.onResize(({ cols, rows }) => {
    const s = sessions.value.find(t => t.id === sid)
    if (s?.ws?.readyState === WebSocket.OPEN) {
      s.ws.send(JSON.stringify({ type: 'RESIZE', cols, rows }))
    }
  })

  term.onTitleChange((title) => {
    const s = sessions.value.find(t => t.id === sid)
    if (s) {
      const m = title.match(/^(\/.+)/)
      if (m) s.cwd = m[1]
    }
  })

  // Observe resize
  const ro = new ResizeObserver(() => { try { fitAddon.fit() } catch { /* size 0 */ } })
  ro.observe(container)
  session.resizeObserver = ro

  // Connect
  await connectSession(sid)

  setTimeout(() => { try { fitAddon.fit() } catch { /* size 0 */ } }, 100)
  term.focus()
}

// ── Connect session ──
async function connectSession(sessionId: string) {
  const s = sessions.value.find(t => t.id === sessionId)
  if (!s) return

  if (s.ws) {
    try { s.ws.close(1000) } catch { /* ignore */ }
    s.ws = null
  }

  s.status = 'connecting'

  // 1. Get WebSSH token
  let websshToken: string
  try {
    const res = await http.post<{ webssh_token: string }>('/auth/webssh-token', {
      server_id: s.serverId,
    })
    websshToken = res.webssh_token
  } catch (e: any) {
    const msg = e?.message || '无法获取终端令牌'
    s.status = 'error'
    s.term?.writeln(`\r\n\x1b[31m⚠ ${msg}\x1b[0m`)
    if (sessionId === activeSessionId.value) {
      showDisconnectOverlay.value = true
      disconnectMsg.value = msg
    }
    return
  }

  // 2. Open WebSocket
  const wsUrl = buildWebSocketUrl(`/ws/terminal/${s.serverId}`, {
    token: websshToken,
  })
  const ws = new WebSocket(wsUrl)
  s.ws = ws

  ws.onopen = () => {
    s.status = 'connected'
    s.manualDisconnect = false
    s.reconnectAttempt = 0
    if (s.reconnectTimer) { clearTimeout(s.reconnectTimer); s.reconnectTimer = null }
    // Only update overlay for active session
    if (sessionId === activeSessionId.value) {
      showDisconnectOverlay.value = false
    }
    startUptimeCounter(s)
    startPing(s)
    s.term?.focus()
  }

  ws.onmessage = (e) => {
    try {
      const msg = JSON.parse(e.data)
      switch (msg.type) {
        case 'TERMINAL_INIT':
          s.serverName = msg.server_name || s.serverName
          const dims = s.fitAddon?.proposeDimensions()
          if (dims && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: 'RESIZE', cols: dims.cols, rows: dims.rows }))
          }
          if (s.pendingPath && !s.initialCdSent && ws.readyState === WebSocket.OPEN) {
            setTimeout(() => sendCdToSession(s, s.pendingPath), 200)
          }
          break
        case 'DATA':
          s.term?.write(msg.data)
          break
        case 'ERROR':
          s.term?.write(`\r\n\x1b[31m⚠ ${msg.message}\x1b[0m\r\n`)
          break
        case 'PONG':
          s.pingMs = Date.now() - (msg.ts || 0)
          break
        case 'CLOSE':
          s.term?.write('\r\n\x1b[33m━━ 连接已关闭 ━━\x1b[0m\r\n')
          break
      }
    } catch {
      s.term?.write(e.data)
    }
  }

  ws.onclose = (ev) => {
    stopPing(s)

    if (ev.code === 4001) {
      s.term?.write('\r\n\x1b[31m⚠ 认证失败，请重新登录\x1b[0m\r\n')
      setTimeout(() => router.push('/login'), 2000)
      return
    }
    if (ev.code === 4003) {
      s.status = 'error'
      s.term?.write(`\r\n\x1b[31m⚠ 授权失败: ${ev.reason || 'SSH凭据未配置'}\x1b[0m\r\n`)
      if (sessionId === activeSessionId.value) {
        showDisconnectOverlay.value = true
        disconnectMsg.value = ev.reason || 'SSH 凭据未配置'
      }
      return
    }

    s.status = 'error'
    s.ws = null

    if (!s.manualDisconnect && ev.code !== 1000 && ev.code !== 4001 && ev.code !== 4003) {
      startReconnect(s)
    } else if (sessionId === activeSessionId.value) {
      showDisconnectOverlay.value = true
      disconnectMsg.value = '连接已断开'
      reconnectCountdown.value = ''
    }
  }

  ws.onerror = () => {
    // onclose will handle UI updates
  }
}

// ── Auto-reconnect (per-session, exponential backoff) ──
function startReconnect(s: TermSession) {
  s.reconnectAttempt++
  const delay = Math.min(1000 * Math.pow(2, s.reconnectAttempt - 1), 30000)
  if (s.id === activeSessionId.value) {
    showDisconnectOverlay.value = true
    disconnectMsg.value = '连接已断开'
    reconnectCountdown.value = `${Math.ceil(delay / 1000)} 秒后自动重连…`
  }
  s.reconnectTimer = setTimeout(() => {
    doReconnect(s)
  }, delay)
}

async function doReconnect(s: TermSession) {
  showDisconnectOverlay.value = false
  reconnectCountdown.value = ''
  s.manualDisconnect = false
  s.reconnectAttempt = 0
  s.term?.clear()
  await connectSession(s.id)
}

function reconnect() {
  const s = activeSession.value
  if (!s) return
  s.reconnectAttempt = 0
  if (s.reconnectTimer) { clearTimeout(s.reconnectTimer); s.reconnectTimer = null }
  doReconnect(s)
}

// ── Disconnect ──
function disconnect() {
  const s = activeSession.value
  if (!s) return
  s.manualDisconnect = true
  if (s.reconnectTimer) { clearTimeout(s.reconnectTimer); s.reconnectTimer = null }
  if (s.ws) {
    if (s.ws.readyState === WebSocket.OPEN) {
      s.ws.send(JSON.stringify({ type: 'CLOSE' }))
      s.ws.close(1000, 'User disconnect')
    }
    s.ws = null
  }
  stopPing(s)
  stopUptimeCounter(s)
  showDisconnectOverlay.value = true
  disconnectMsg.value = '连接已断开'
  reconnectCountdown.value = ''
}

// ── Context menu handlers ──
function openTermMenu(e: MouseEvent, context: 'terminal' | 'command') {
  e.preventDefault()
  termMenuContext.value = context
  menuX.value = e.clientX
  menuY.value = e.clientY
  showTermMenu.value = true
}

function onTermContext(e: MouseEvent) {
  openTermMenu(e, 'terminal')
}

function onCmdContext(e: MouseEvent) {
  openTermMenu(e, 'command')
}

function getCmdInputEl(): HTMLTextAreaElement | null {
  const root = document.querySelector('.cmd-bar--tall .sh-field__input--multi')
  return root instanceof HTMLTextAreaElement ? root : null
}

function termCopy() {
  showTermMenu.value = false
  if (termMenuContext.value === 'command') {
    const el = getCmdInputEl()
    const text = el?.value.substring(el.selectionStart ?? 0, el.selectionEnd ?? 0) || ''
    if (text) {
      navigator.clipboard.writeText(text).catch(() => snackbar('复制失败', 'error'))
      return
    }
  }
  const sel = window.getSelection()?.toString()
  if (sel) {
    navigator.clipboard.writeText(sel).catch(() => snackbar('复制失败', 'error'))
  } else if (activeSession.value?.term) {
    const buffer = activeSession.value.term.getSelection()
    if (buffer) navigator.clipboard.writeText(buffer).catch(() => snackbar('复制失败', 'error'))
  }
}

async function termPaste() {
  showTermMenu.value = false
  try {
    const text = await navigator.clipboard.readText()
    if (!text) return
    if (termMenuContext.value === 'command') {
      const el = getCmdInputEl()
      if (el) {
        const start = el.selectionStart ?? el.value.length
        const end = el.selectionEnd ?? start
        const next = el.value.slice(0, start) + text + el.value.slice(end)
        cmdInput.value = next
        await nextTick()
        el.focus()
        const pos = start + text.length
        el.setSelectionRange(pos, pos)
      }
      return
    }
    if (activeSession.value?.term) {
      activeSession.value.term.paste(text)
    }
  } catch {
    snackbar('剪贴板访问被拒绝', 'warning')
  }
}

function termSelectAll() {
  showTermMenu.value = false
  if (termMenuContext.value === 'command') {
    const el = getCmdInputEl()
    if (el) {
      el.focus()
      el.select()
    }
    return
  }
  if (activeSession.value?.term) {
    activeSession.value.term.selectAll()
  }
}

function termClear() {
  showTermMenu.value = false
  if (activeSession.value?.term) {
    activeSession.value.term.clear()
  }
}

function termDisconnect() {
  showTermMenu.value = false
  if (activeSession.value) {
    closeTab(activeSession.value.id)
  }
}

// ── Uptime (per-session) ──

function startUptimeCounter(s: TermSession) {
  s.connectionStartTime = Date.now()
  stopUptimeCounter(s)
  // Update immediately
  updateUptime(s)
  // Then every second
  const id = setInterval(() => updateUptime(s), 1000) as any
  // Store timer ref on the session object via a closure trick
  // We'll use a separate map for uptime timers since TermSession doesn't have one
  _uptimeTimers.set(s.id, id)
}

function stopUptimeCounter(s: TermSession) {
  const id = _uptimeTimers.get(s.id)
  if (id) { clearInterval(id); _uptimeTimers.delete(s.id) }
  s.uptime = ''
}

function updateUptime(s: TermSession) {
  if (!s.connectionStartTime) { s.uptime = ''; return }
  const sec = Math.floor((Date.now() - s.connectionStartTime) / 1000)
  const h = Math.floor(sec / 3600)
  const m = Math.floor((sec % 3600) / 60)
  const ss = sec % 60
  s.uptime = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(ss).padStart(2, '0')}`
}

function updateUptimeDisplay(s?: TermSession) {
  // When switching tabs, the active session's uptime is already being updated by its timer
  // Just need to make sure the overlay state is correct
  const current = s || activeSession.value
  if (current?.ws) {
    showDisconnectOverlay.value = false
  }
}

const _uptimeTimers = new Map<string, ReturnType<typeof setInterval>>()

// ── Ping (per-session) ──

function startPing(s: TermSession) {
  stopPing(s)
  s.pingMs = null
  s.pingTimer = setInterval(() => {
    if (s.ws?.readyState === WebSocket.OPEN) {
      s.ws.send(JSON.stringify({ type: 'PING', ts: Date.now() }))
    }
  }, 5000) as any
}

function stopPing(s: TermSession) {
  if (s.pingTimer) { clearInterval(s.pingTimer); s.pingTimer = null }
  s.pingMs = null
}

// ── Command bar ──
function sendCmd() {
  const s = activeSession.value
  if (!s?.ws || s.ws.readyState !== WebSocket.OPEN) return
  const cmd = cmdInput.value
  if (!cmd) return
  s.cmdHistory.push(cmd)
  s.cmdIdx = -1
  s.ws.send(JSON.stringify({ type: 'DATA', data: cmd + '\r' }))
  cmdInput.value = ''
  // Persist to localStorage (global history)
  let hist: string[] = loadGlobalHistory()
  if (hist.length >= 200) hist.shift()
  hist.push(cmd)
  localStorage.setItem(K_HIST, JSON.stringify(hist))
}

function cmdHistoryUp() {
  const s = activeSession.value
  if (!s) return
  if (s.cmdIdx < s.cmdHistory.length - 1) {
    s.cmdIdx++
    cmdInput.value = s.cmdHistory[s.cmdHistory.length - 1 - s.cmdIdx]
  }
}

function cmdHistoryDown() {
  const s = activeSession.value
  if (!s) return
  if (s.cmdIdx > 0) {
    s.cmdIdx--
    cmdInput.value = s.cmdHistory[s.cmdHistory.length - 1 - s.cmdIdx]
  } else {
    s.cmdIdx = -1
    cmdInput.value = ''
  }
}

function sendCtrl(key: string) {
  const s = activeSession.value
  if (!s?.ws || s.ws.readyState !== WebSocket.OPEN) return
  s.ws.send(JSON.stringify({ type: 'DATA', data: String.fromCharCode(key.toLowerCase().charCodeAt(0) - 96) }))
}

function sendKey(key: string) {
  const s = activeSession.value
  if (!s?.ws || s.ws.readyState !== WebSocket.OPEN) return
  s.ws.send(JSON.stringify({ type: 'DATA', data: key }))
}

// ── Font size ──
function changeFontSize(delta: number) {
  fontSize.value = Math.max(10, Math.min(24, fontSize.value + delta))
  localStorage.setItem(K_FONT, String(fontSize.value))
  sessions.value.forEach(s => {
    if (s.term) {
      s.term.options.fontSize = fontSize.value
      try { s.fitAddon?.fit() } catch { /* size 0 */ }
    }
  })
}

// ── Scrollback ──
function changeScrollback(val: number) {
  scrollback.value = val
  localStorage.setItem(K_SCROLL, String(val))
  sessions.value.forEach(s => {
    if (s.term) s.term.options.scrollback = val
  })
}

// ── Fullscreen ──
function toggleFullscreen() {
  if (!document.fullscreenElement) {
    termContainer.value?.requestFullscreen?.()
  } else {
    document.exitFullscreen?.()
  }
}

// ── Quick commands (from MySQL API) ──
async function loadQuickCmds() {
  try {
    const res = await http.get<QuickCmdItem[]>('/terminal/quick-commands')
    quickCmds.value = sortQuickCommands(res || [])
  } catch {
    quickCmds.value = sortQuickCommands([
      { id: 1, name: '系统状态', cmd: "systemctl status --no-pager 2>&1 | head -20\r", is_builtin: true, sort_order: 1001 },
      { id: 2, name: '系统日志', cmd: "journalctl -n 30 --no-pager 2>&1\r", is_builtin: true, sort_order: 1002 },
      { id: 3, name: '磁盘使用', cmd: "df -h\r", is_builtin: true, sort_order: 1003 },
      { id: 4, name: '内存使用', cmd: "free -h\r", is_builtin: true, sort_order: 1004 },
      { id: 5, name: '进程列表', cmd: "ps aux --sort=-%mem 2>&1 | head -15\r", is_builtin: true, sort_order: 1005 },
    ])
  }
}

function execQuickCmd(cmd: string) {
  const s = activeSession.value
  if (!s?.ws || s.ws.readyState !== WebSocket.OPEN) {
    snackbar('终端未连接', 'warning')
    return
  }
  s.ws.send(JSON.stringify({ type: 'DATA', data: cmd }))
  s.term?.focus()
}

// ── Load server list ──
async function loadServers() {
  try {
    const res = await http.get<{ items: { id: number; name: string; domain: string; is_online: boolean }[] }>('/servers/', {
      per_page: 200,
    })
    servers.value = res.items || []
  } catch { /* non-critical */ }
}

// ── Keyboard shortcuts ──
function onKeydown(e: KeyboardEvent) {
  // Ctrl+L: clear terminal
  if (e.ctrlKey && e.key === 'l') {
    e.preventDefault()
    const s = activeSession.value
    if (s?.term) {
      s.term.clear()
      if (s.ws?.readyState === WebSocket.OPEN) {
        s.ws.send(JSON.stringify({ type: 'DATA', data: '\x0c' }))
      }
    }
    return
  }
  if (e.ctrlKey && e.key === '=') { e.preventDefault(); changeFontSize(1) }
  if (e.ctrlKey && e.key === '+') { e.preventDefault(); changeFontSize(1) }
  if (e.ctrlKey && e.key === '-') { e.preventDefault(); changeFontSize(-1) }
  if (e.ctrlKey && e.shiftKey && e.key === 'F') { e.preventDefault(); toggleFullscreen() }
}

// ── Lifecycle ──
watch(
  () => [route.query.server_id, route.query.path, route.query.new_tab],
  () => { void applyRouteQuery() },
)

onMounted(async () => {
  document.addEventListener('keydown', onKeydown)
  await loadServers()
  await loadQuickCmds()
  await applyRouteQuery()
})

onBeforeUnmount(() => {
  document.removeEventListener('keydown', onKeydown)
  // Close all sessions
  sessions.value.forEach(s => {
    if (s.reconnectTimer) clearTimeout(s.reconnectTimer)
    if (s.pingTimer) clearInterval(s.pingTimer)
    if (s.ws) { try { s.ws.close(1000) } catch { /* ignore */ } }
    if (s.term) { try { s.term.dispose() } catch { /* ignore */ } }
    if (s.resizeObserver) { try { s.resizeObserver.disconnect() } catch { /* ignore */ } }
  })
  // Clean up uptime timers
  _uptimeTimers.forEach(id => clearInterval(id))
  _uptimeTimers.clear()
})
</script>

<style scoped>
.terminal-root {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  width: 100%;
  max-width: 100%;
  height: 100%;
  box-sizing: border-box;
}
.terminal-root--dark {
  background: #0b1120;
}
.terminal-root--light {
  background: rgb(var(--v-theme-background));
}
.empty-server-picker__list {
  max-height: min(42vh, 320px);
  overflow-y: auto;
}
.fill-height {
  flex: 1 1 auto;
  min-height: 200px;
}
.cursor-pointer { cursor: pointer; }

/* 服务器列表抽屉须高于终端内容，且从主内容区左缘弹出（非全局侧栏位） */
.font-mono { font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, monospace; }
:deep(.xterm) { padding: 4px; height: 100%; }
:deep(.xterm-viewport) { overflow-y: auto !important; }

.terminal-quick-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: rgb(var(--v-theme-surface));
  border-top: 1px solid rgb(var(--v-theme-surface-variant));
}
.terminal-quick-bar__label {
  font-size: 0.9375rem;
  font-weight: 600;
  flex-shrink: 0;
  color: rgb(var(--v-theme-on-surface));
}

.cmd-bar {
  background: rgb(var(--v-theme-surface));
  border-top: 1px solid rgb(var(--v-theme-surface-variant));
}
.cmd-bar--tall {
  min-height: 120px;
}
.cmd-bar__prompt {
  font-size: 1.125rem;
  line-height: 1.55;
  padding-top: 12px;
}
.cmd-bar__input-wrap {
  min-width: 0;
}

.terminal-hotkeys {
  border-left: 1px solid rgb(var(--v-theme-surface-variant));
  min-width: 9.5rem;
}

.terminal-toolbar-scrollback {
  min-width: 8.5rem;
  max-width: 10rem;
}

:deep(.terminal-ctx-menu) {
  min-width: 11.5rem;
}

:deep(.terminal-ctx-menu .v-list-item-title) {
  font-size: 1rem;
  font-weight: 500;
}

:deep(.terminal-ctx-menu .v-icon) {
  font-size: 1.35rem;
}

/* Shell 语法高亮（命令栏 + 快捷命令编辑） */
.terminal-root--dark :deep(.sh-keyword) { color: #60a5fa; font-weight: 500; }
.terminal-root--dark :deep(.sh-builtin) { color: #38bdf8; }
.terminal-root--dark :deep(.sh-sudo) { color: #f87171; font-weight: 600; }
.terminal-root--dark :deep(.sh-pipe),
.terminal-root--dark :deep(.sh-operator) { color: #fbbf24; font-weight: 600; }
.terminal-root--dark :deep(.sh-redirect) { color: #fb923c; }
.terminal-root--dark :deep(.sh-string) { color: #4ade80; }
.terminal-root--dark :deep(.sh-flag) { color: #c084fc; }
.terminal-root--dark :deep(.sh-variable) { color: #f472b6; }
.terminal-root--dark :deep(.sh-backtick) { color: #a78bfa; }
.terminal-root--dark :deep(.sh-comment) { color: #64748b; font-style: italic; }
.terminal-root--dark :deep(.sh-number) { color: #fbbf24; }
.terminal-root--dark :deep(.sh-path) { color: #2dd4bf; }
.terminal-root--dark :deep(.sh-text) { color: #e2e8f0; }

.terminal-root--light :deep(.sh-keyword) { color: #2563eb; font-weight: 500; }
.terminal-root--light :deep(.sh-builtin) { color: #0284c7; }
.terminal-root--light :deep(.sh-sudo) { color: #dc2626; font-weight: 600; }
.terminal-root--light :deep(.sh-pipe),
.terminal-root--light :deep(.sh-operator) { color: #ca8a04; font-weight: 600; }
.terminal-root--light :deep(.sh-redirect) { color: #ea580c; }
.terminal-root--light :deep(.sh-string) { color: #16a34a; }
.terminal-root--light :deep(.sh-flag) { color: #9333ea; }
.terminal-root--light :deep(.sh-variable) { color: #db2777; }
.terminal-root--light :deep(.sh-backtick) { color: #7c3aed; }
.terminal-root--light :deep(.sh-comment) { color: #94a3b8; font-style: italic; }
.terminal-root--light :deep(.sh-number) { color: #d97706; }
.terminal-root--light :deep(.sh-path) { color: #0d9488; }
.terminal-root--light :deep(.sh-text) { color: #0f172a; }
</style>
