<template>
  <v-container fluid class="pa-0 fill-height" style="background: #0b1120">
    <!-- ── Server Sidebar ── -->
    <v-navigation-drawer v-model="showServerSidebar" width="250" location="start" temporary>
      <v-list-subheader>服务器列表</v-list-subheader>
      <v-text-field
        v-model="sidebarSearch"
        prepend-inner-icon="mdi-magnify"
        placeholder="搜索..."
        variant="outlined"
        density="compact"
        hide-details
        class="mx-3 mb-2"
      />
      <v-list density="compact" nav>
        <v-list-item
          v-for="s in sidebarFilteredServers"
          :key="s.id"
          :title="s.name"
          :subtitle="s.domain"
          @click="newSession(s.id); showServerSidebar = false"
        >
          <template #prepend>
            <v-icon :color="s.is_online !== false ? 'success' : 'grey'" size="8">mdi-circle</v-icon>
          </template>
        </v-list-item>
        <v-list-item v-if="sidebarFilteredServers.length === 0" class="text-center text-medium-emphasis">
          暂无服务器
        </v-list-item>
      </v-list>
    </v-navigation-drawer>

    <!-- ── Context Menu ── -->
    <v-menu v-model="showTermMenu" :target="[menuX, menuY]" location="bottom start">
      <v-list density="compact" nav>
        <v-list-item prepend-icon="mdi-content-copy" @click="termCopy">
          <v-list-item-title>复制</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-content-paste" @click="termPaste">
          <v-list-item-title>粘贴</v-list-item-title>
        </v-list-item>
        <v-divider />
        <v-list-item prepend-icon="mdi-delete-sweep" @click="termClear">
          <v-list-item-title>清屏</v-list-item-title>
        </v-list-item>
        <v-list-item prepend-icon="mdi-logout" @click="termDisconnect" :disabled="!activeSession">
          <v-list-item-title class="text-error">断开连接</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>

    <div class="d-flex flex-column" style="height: calc(100vh - 64px)">
      <!-- ── Tab Bar ── -->
      <div class="d-flex align-center px-2 py-1" style="background: rgb(var(--v-theme-surface)); border-bottom: 1px solid rgb(var(--v-theme-surface-variant))">
        <v-btn icon="mdi-server-network" size="small" variant="text" @click="showServerSidebar = !showServerSidebar" class="mr-1" />
        <div class="d-flex align-center ga-1 overflow-x-auto flex-grow-1">
          <v-chip
            v-for="(tab, i) in sessions"
            :key="tab.id"
            :variant="i === activeIdx ? 'flat' : 'outlined'"
            :color="i === activeIdx ? 'primary' : 'default'"
            size="small"
            label
            class="cursor-pointer"
            @click="switchTab(i)"
          >
            <template #prepend>
              <v-icon start size="8" :color="tab.ws ? 'success' : '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(i)">mdi-close</v-icon>
          </v-chip>
          <v-btn icon size="x-small" variant="text" @click="newSession()">
            <v-icon size="14">mdi-plus</v-icon>
          </v-btn>
        </div>
      </div>

      <!-- ── Toolbar ── -->
      <div class="d-flex align-center justify-between px-4 py-1" 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="activeSession.ws ? 'success' : 'grey'"
            variant="tonal"
            size="x-small"
            label
          >
            <template #prepend>
              <v-icon start size="8">mdi-circle</v-icon>
            </template>
            {{ activeSession.ws ? '已连接' : '未连接' }}
          </v-chip>
          <span v-if="uptime" class="text-caption text-medium-emphasis">{{ uptime }}</span>
          <span v-if="pingMs !== null" class="text-caption text-medium-emphasis">{{ pingMs }}ms</span>
        </div>
        <div class="d-flex align-center ga-2">
          <v-btn variant="text" size="x-small" density="compact" @click="changeFontSize(-1)">A−</v-btn>
          <span class="text-caption">{{ fontSize }}</span>
          <v-btn variant="text" size="x-small" density="compact" @click="changeFontSize(1)">A+</v-btn>
          <v-select
            v-model="scrollback"
            :items="[1000, 5000, 10000, 50000]"
            density="compact"
            hide-details
            variant="outlined"
            style="max-width: 90px"
            @update:model-value="changeScrollback"
          />
          <v-btn variant="text" size="x-small" density="compact" @click="toggleFullscreen">
            <v-icon size="14">mdi-fullscreen</v-icon>
          </v-btn>
          <v-btn variant="text" size="x-small" density="compact" color="error" @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, i) in sessions"
          :key="tab.id"
          :ref="el => setTermRef(el, i)"
          :style="{
            position: 'absolute',
            inset: 0,
            zIndex: i === activeIdx ? 1 : 0,
            pointerEvents: i === activeIdx ? 'auto' : 'none',
            opacity: i === activeIdx ? 1 : 0,
          }"
        />

        <!-- 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="d-flex align-center ga-1 px-3 py-1 overflow-x-auto" style="background: rgb(var(--v-theme-surface)); border-top: 1px solid rgb(var(--v-theme-surface-variant))">
        <span class="text-caption shrink-0">⚡</span>
        <v-chip
          v-for="cmd in quickCmds"
          :key="cmd.id"
          size="x-small"
          :variant="cmd.id.startsWith('__') ? 'outlined' : 'tonal'"
          :color="cmd.id.startsWith('__') ? 'default' : 'primary'"
          label
          class="cursor-pointer"
          @click="execQuickCmd(cmd.cmd)"
        >
          {{ cmd.name }}
          <v-icon v-if="!cmd.id.startsWith('__')" end size="10" class="ml-1" @click.stop="deleteQuickCmd(cmd.id)">mdi-close</v-icon>
        </v-chip>
        <v-btn icon size="x-small" variant="text" @click="addQuickCmd">
          <v-icon size="12">mdi-plus</v-icon>
        </v-btn>
      </div>

      <!-- ── Command Bar ── -->
      <div class="d-flex align-center ga-2 px-3 py-1" style="background: rgb(var(--v-theme-surface)); border-top: 1px solid rgb(var(--v-theme-surface-variant))" :class="{ 'opacity-50 pointer-events-none': !activeSession?.ws }">
        <span class="text-body-2 font-mono text-primary shrink-0">❯</span>
        <v-text-field
          v-model="cmdInput"
          placeholder="输入命令，Enter 发送…"
          variant="outlined"
          density="compact"
          hide-details
          class="flex-grow-1 font-mono"
          autocomplete="off"
          @keydown.enter="sendCmd"
          @keydown.up.prevent="cmdHistoryUp"
          @keydown.down.prevent="cmdHistoryDown"
        />
        <v-btn size="small" variant="tonal" color="primary" @click="sendCmd" :disabled="!activeSession?.ws">发送</v-btn>
        <div class="d-flex ga-1 pl-2" style="border-left: 1px solid rgb(var(--v-theme-surface-variant))">
          <v-btn size="x-small" variant="outlined" density="compact" @click="sendCtrl('c')" title="Ctrl+C">^C</v-btn>
          <v-btn size="x-small" variant="outlined" density="compact" @click="sendCtrl('d')" title="Ctrl+D">^D</v-btn>
          <v-btn size="x-small" variant="outlined" density="compact" @click="sendKey('\t')" title="Tab补全">Tab</v-btn>
        </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" start>mdi-circle</v-icon>
              </template>
            </v-list-item>
          </v-list>
        </v-card-text>
      </v-card>
    </v-dialog>

    <!-- ── Quick Cmd Dialog ── -->
    <v-dialog v-model="showQcDialog" max-width="400">
      <v-card border>
        <v-card-title>{{ qcEditId ? '编辑命令' : '添加命令' }}</v-card-title>
        <v-card-text>
          <v-text-field v-model="qcName" label="命令名称" variant="outlined" density="compact" class="mb-3" />
          <v-textarea v-model="qcCmd" label="命令内容（含 \\r 回车）" variant="outlined" density="compact" rows="3" class="font-mono" />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showQcDialog = false">取消</v-btn>
          <v-btn color="primary" variant="flat" @click="saveQuickCmd">保存</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-container>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } 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'

const route = useRoute()
const router = useRouter()
const theme = useTheme()
const auth = useAuthStore()
const snackbar = useSnackbar()

// ── Props from route ──
const initialServerId = computed(() => Number(route.query.server_id) || 0)
const initialPath = computed(() => {
  const p = (route.query.path as string) || ''
  return p && /^[a-zA-Z0-9/._\-]+$/.test(p) ? p : ''
})
let initialCdSent = false

// ── localStorage keys ──
const K_FONT = 'nexus_term_fontSize'
const K_SCROLL = 'nexus_term_scrollback'
const K_HIST = 'nexus_term_history'
const K_CMDS = 'nexus_terminal_cmds'
const K_TABS = 'nexus_terminal_tabs'

const BUILTIN_CMDS = [
  { id: '__sys__', name: '系统状态', cmd: "systemctl status --no-pager 2>&1 | head -20\r" },
  { id: '__log__', name: '系统日志', cmd: "journalctl -n 30 --no-pager 2>&1\r" },
  { id: '__disk__', name: '磁盘使用', cmd: "df -h\r" },
  { id: '__mem__', name: '内存使用', cmd: "free -h\r" },
  { id: '__proc__', name: '进程列表', cmd: "ps aux --sort=-%mem 2>&1 | head -15\r" },
]

// ── 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
}

const sessions = ref<TermSession[]>([])
const activeIdx = ref(-1)
const termContainer = ref<HTMLElement | null>(null)
const termRefs = new Map<number, HTMLElement>()

const activeSession = computed(() => activeIdx.value >= 0 ? sessions.value[activeIdx.value] : null)

// ── Tab persistence ──
function saveTabsToStorage() {
  const tabData = sessions.value.map(s => ({
    id: s.id,
    serverId: s.serverId,
    serverName: s.serverName,
    title: s.serverName,
  }))
  localStorage.setItem(K_TABS, JSON.stringify(tabData))
}

function restoreTabsFromStorage() {
  try {
    const raw = localStorage.getItem(K_TABS)
    if (!raw) return
    const tabData = JSON.parse(raw) as { id: string; serverId: number; serverName: string; title: string }[]
    for (const tab of tabData) {
      if (!sessions.value.find(s => s.id === tab.id)) {
        sessions.value.push({
          id: tab.id,
          serverId: tab.serverId,
          serverName: tab.serverName,
          ws: null,
          term: null,
          fitAddon: null,
          resizeObserver: null,
          cwd: '',
          cmdHistory: [],
          cmdIdx: -1,
        })
      }
    }
    if (sessions.value.length > 0 && activeIdx.value < 0) {
      activeIdx.value = 0
    }
  } catch {
    // Ignore corrupt data
  }
}

// ── UI state ──
const fontSize = ref(parseInt(localStorage.getItem(K_FONT) || '14'))
const scrollback = ref(parseInt(localStorage.getItem(K_SCROLL) || '1000'))
const uptime = ref('')
const pingMs = ref<number | null>(null)
let uptimeTimer: ReturnType<typeof setInterval> | null = null
let pingTimer: ReturnType<typeof setInterval> | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let reconnectAttempt = 0
let manualDisconnect = false
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)

// ── Server sidebar ──
const showServerSidebar = ref(false)
const sidebarSearch = ref('')
const sidebarFilteredServers = computed(() => {
  const q = sidebarSearch.value.toLowerCase().trim()
  return servers.value.filter(s => !q || s.name.toLowerCase().includes(q) || s.domain.toLowerCase().includes(q))
})

// ── Quick commands ──
const qcName = ref('')
const qcCmd = ref('')
const qcEditId = ref('')
const showQcDialog = ref(false)
const quickCmds = ref([...BUILTIN_CMDS])
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))
})

// ── Terminal ref management ──
function setTermRef(el: any, idx: number) {
  if (el) termRefs.set(idx, 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: '#ffffff',
      foreground: '#1e293b',
      cursor: '#7c8bf4',
      selectionBackground: '#bfdbfe',
    },
    allowProposedApi: true,
  })
}

// ── Tab management ──
async function switchTab(idx: number) {
  activeIdx.value = idx
  await nextTick()
  const s = sessions.value[idx]
  if (s?.fitAddon) {
    setTimeout(() => { try { s.fitAddon!.fit() } catch {} }, 50)
  }
  if (s?.term) s.term.focus()
  updateUptimeDisplay()
}

async function closeTab(idx: number) {
  const s = sessions.value[idx]
  if (s.ws) { try { s.ws.close(1000) } catch {} s.ws = null }
  if (s.term) { try { s.term.dispose() } catch {} s.term = null }
  if (s.resizeObserver) { try { s.resizeObserver.disconnect() } catch {} s.resizeObserver = null }
  termRefs.delete(idx)
  sessions.value.splice(idx, 1)

  // Re-map termRefs after splice
  const newRefs = new Map<number, HTMLElement>()
  for (const [k, v] of termRefs) {
    newRefs.set(k > idx ? k - 1 : k, v)
  }
  termRefs.clear()
  for (const [k, v] of newRefs) termRefs.set(k, v)

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

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

  // Check duplicate
  const existing = sessions.value.findIndex(s => s.serverId === serverId)
  if (existing >= 0) {
    await switchTab(existing)
    return
  }

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

  const session: TermSession = {
    id: sid,
    serverId,
    serverName: name,
    ws: null,
    term: null,
    fitAddon: null,
    resizeObserver: null,
    cwd: '',
    cmdHistory: [],
    cmdIdx: -1,
  }
  sessions.value.push(session)

  const idx = sessions.value.length - 1
  activeIdx.value = idx
  await nextTick()

  // Create terminal and attach to DOM
  const container = termRefs.get(idx)
  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[idx]
    if (s?.ws?.readyState === WebSocket.OPEN) {
      s.ws.send(JSON.stringify({ type: 'DATA', data }))
    }
  })

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

  term.onTitleChange((title) => {
    const s = sessions.value[idx]
    if (s && idx === activeIdx.value) {
      const m = title.match(/^(\/.+)/)
      if (m) s.cwd = m[1]
    }
  })

  // Observe resize
  const ro = new ResizeObserver(() => { try { fitAddon.fit() } catch {} })
  ro.observe(container)
  sessions.value[idx].resizeObserver = ro

  // Connect
  await connectSession(idx)

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

// ── Connect session ──
async function connectSession(idx: number) {
  const s = sessions.value[idx]
  if (!s) return

  // 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) {
    s.term?.writeln('\r\n\x1b[31m无法获取终端令牌\x1b[0m')
    return
  }

  // 2. Open WebSocket
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
  const wsUrl = `${proto}//${location.host}/ws/terminal/${s.serverId}?token=${encodeURIComponent(websshToken)}`
  const ws = new WebSocket(wsUrl)
  s.ws = ws

  ws.onopen = () => {
    if (idx !== activeIdx.value) return
    manualDisconnect = false
    reconnectAttempt = 0
    showDisconnectOverlay.value = false
    startUptimeCounter()
    startPing(ws)
    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 }))
          }
          // Auto-cd to initial path
          if (initialPath.value && !initialCdSent && ws.readyState === WebSocket.OPEN) {
            initialCdSent = true
            setTimeout(() => {
              ws.send(JSON.stringify({ type: 'DATA', data: `cd ${initialPath.value}\r` }))
            }, 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':
          if (idx === activeIdx.value) {
            pingMs.value = 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) => {
    if (idx === activeIdx.value) {
      stopUptimeCounter()
      stopPing()
    }

    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.term?.write(`\r\n\x1b[31m⚠ 授权失败: ${ev.reason || 'SSH凭据未配置'}\x1b[0m\r\n`)
      return
    }

    s.ws = null

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

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

// ── Auto-reconnect ──
function startReconnect(idx: number) {
  if (idx !== activeIdx.value) return
  reconnectAttempt++
  const delay = Math.min(reconnectAttempt * 1000, 30000)
  showDisconnectOverlay.value = true
  disconnectMsg.value = '连接已断开'
  reconnectCountdown.value = `${Math.ceil(delay / 1000)} 秒后自动重连…`
  reconnectTimer = setTimeout(() => {
    if (idx !== activeIdx.value) return
    doReconnect(idx)
  }, delay)
}

async function doReconnect(idx: number) {
  const s = sessions.value[idx]
  if (!s) return
  showDisconnectOverlay.value = false
  reconnectCountdown.value = ''
  manualDisconnect = false
  reconnectAttempt = 0
  s.term?.clear()
  await connectSession(idx)
}

function reconnect() {
  if (activeIdx.value < 0) return
  reconnectAttempt = 0
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null }
  doReconnect(activeIdx.value)
}

// ── Disconnect ──
function disconnect() {
  manualDisconnect = true
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null }
  const s = activeSession.value
  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
  }
  showDisconnectOverlay.value = true
  disconnectMsg.value = '连接已断开'
  reconnectCountdown.value = ''
  stopUptimeCounter()
  stopPing()
}

// ── Context menu handlers ──
function onTermContext(e: MouseEvent) {
  e.preventDefault()
  menuX.value = e.clientX
  menuY.value = e.clientY
  showTermMenu.value = true
}

function termCopy() {
  showTermMenu.value = false
  const sel = window.getSelection()?.toString()
  if (sel) navigator.clipboard.writeText(sel)
}

async function termPaste() {
  showTermMenu.value = false
  const text = await navigator.clipboard.readText()
  if (activeSession.value?.term && text) {
    activeSession.value.term.paste(text)
  }
}

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

function termDisconnect() {
  showTermMenu.value = false
  if (activeSession.value) {
    const idx = activeIdx.value
    if (idx >= 0) closeTab(idx)
  }
}

// ── Uptime / Ping ──
let connectionStartTime = 0

function startUptimeCounter() {
  connectionStartTime = Date.now()
  stopUptimeCounter()
  uptimeTimer = setInterval(() => {
    const sec = Math.floor((Date.now() - connectionStartTime) / 1000)
    const h = Math.floor(sec / 3600)
    const m = Math.floor((sec % 3600) / 60)
    const s = sec % 60
    uptime.value = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
  }, 1000)
}

function stopUptimeCounter() {
  if (uptimeTimer) { clearInterval(uptimeTimer); uptimeTimer = null }
  uptime.value = ''
}

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

function stopPing() {
  if (pingTimer) { clearInterval(pingTimer); pingTimer = null }
  pingMs.value = null
}

function updateUptimeDisplay() {
  // Restart uptime display for active session
  if (activeSession.value?.ws) {
    startUptimeCounter()
  } else {
    stopUptimeCounter()
  }
}

// ── 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
  let hist: string[] = []
  try { hist = JSON.parse(localStorage.getItem(K_HIST) || '[]') } catch {}
  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 {}
    }
  })
}

// ── 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 ──
function loadQuickCmds() {
  let userCmds: { id: string; name: string; cmd: string }[] = []
  try { userCmds = JSON.parse(localStorage.getItem(K_CMDS) || '[]') } catch {}
  quickCmds.value = [...userCmds, ...BUILTIN_CMDS]
}

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()
}

function addQuickCmd() {
  qcEditId.value = ''
  qcName.value = ''
  qcCmd.value = ''
  showQcDialog.value = true
}

function saveQuickCmd() {
  if (!qcName.value.trim() || !qcCmd.value.trim()) {
    snackbar('名称和命令不能为空', 'warning')
    return
  }
  let userCmds: { id: string; name: string; cmd: string }[] = []
  try { userCmds = JSON.parse(localStorage.getItem(K_CMDS) || '[]') } catch {}
  if (qcEditId.value) {
    const idx = userCmds.findIndex(c => c.id === qcEditId.value)
    if (idx >= 0) { userCmds[idx].name = qcName.value; userCmds[idx].cmd = qcCmd.value }
  } else {
    userCmds.push({ id: `qc_${Date.now()}`, name: qcName.value, cmd: qcCmd.value })
  }
  localStorage.setItem(K_CMDS, JSON.stringify(userCmds))
  showQcDialog.value = false
  loadQuickCmds()
}

function deleteQuickCmd(id: string) {
  let userCmds: { id: string; name: string; cmd: string }[] = []
  try { userCmds = JSON.parse(localStorage.getItem(K_CMDS) || '[]') } catch {}
  userCmds = userCmds.filter(c => c.id !== id)
  localStorage.setItem(K_CMDS, JSON.stringify(userCmds))
  loadQuickCmds()
}

// ── 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) {
  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() }
}

// ── Tab persistence watcher (watch only metadata, not terminal content) ──
const tabMetadata = computed(() =>
  sessions.value.map(s => ({
    id: s.id,
    serverId: s.serverId,
    serverName: s.serverName,
  }))
)

watch(tabMetadata, () => saveTabsToStorage())

// ── Lifecycle ──
onMounted(async () => {
  document.addEventListener('keydown', onKeydown)
  await loadServers()
  loadQuickCmds()

  // Restore command history
  let hist: string[] = []
  try { hist = JSON.parse(localStorage.getItem(K_HIST) || '[]') } catch {}

  // Restore persisted tabs (disconnected state)
  restoreTabsFromStorage()

  if (initialServerId.value) {
    await newSession(initialServerId.value)
    if (sessions.value.length > 0) {
      sessions.value[0].cmdHistory = hist
    }
  } else if (sessions.value.length === 0) {
    // No restored tabs and no server_id in query — show server selector
    showServerDialog.value = true
  }
})

onBeforeUnmount(() => {
  document.removeEventListener('keydown', onKeydown)
  // Close all sessions
  sessions.value.forEach(s => {
    if (s.ws) { try { s.ws.close(1000) } catch {} }
    if (s.term) { try { s.term.dispose() } catch {} }
    if (s.resizeObserver) { try { s.resizeObserver.disconnect() } catch {} }
  })
  stopUptimeCounter()
  stopPing()
  if (reconnectTimer) clearTimeout(reconnectTimer)
})
</script>

<style scoped>
.cursor-pointer { cursor: pointer; }
.font-mono { font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, monospace; }
:deep(.xterm) { padding: 4px; height: 100%; }
</style>
