<script setup lang="ts">
import { FitAddon } from '@xterm/addon-fit'
import { Terminal } from '@xterm/xterm'
import '@xterm/xterm/css/xterm.css'
import { NSpin } from 'naive-ui'
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'

import { consoleSessionWsUrl, createConsoleSession, deleteConsoleSession } from '@/api/instances'
import { useAuthStore } from '@/stores/auth'
import { useToast } from '@/composables/useToast'

const props = defineProps<{
  show: boolean
  cfgId: number
  instanceId: string
  region?: string
  instanceName?: string
}>()

const emit = defineEmits<{ 'update:show': [boolean] }>()

const message = useToast()
const auth = useAuthStore()

const termEl = ref<HTMLDivElement | null>(null)
const phase = ref<'idle' | 'creating' | 'connecting' | 'connected' | 'closed'>('idle')
const error = ref('')
const minimized = ref(false)
const fullscreen = ref(false)
const height = ref(360)

let term: Terminal | null = null
let fit: FitAddon | null = null
let ws: WebSocket | null = null
let ro: ResizeObserver | null = null
let sessionId = ''
const encoder = new TextEncoder()
// 打开代次:关闭/切换目标即自增。会话创建是长耗时异步,面板可能中途切到别的
// 实例,旧代次的迟到响应只能就地删除其会话,不得覆盖共享状态——否则标题显示
// B 实际连着 A,用户输入发往错误实例
let openSeq = 0

async function open() {
  const seq = ++openSeq
  phase.value = 'creating'
  error.value = ''
  height.value = Math.max(280, Math.round(window.innerHeight * 0.42))
  let created = ''
  try {
    const res = await createConsoleSession(props.cfgId, props.instanceId, 'serial', props.region)
    created = res.sessionId
  } catch (e) {
    if (seq !== openSeq) return
    phase.value = 'closed'
    error.value = e instanceof Error ? e.message : '创建串行会话失败'
    return
  }
  if (seq !== openSeq) {
    void deleteConsoleSession(created).catch(() => undefined)
    return
  }
  sessionId = created
  phase.value = 'connecting'
  await nextTick()
  if (seq !== openSeq) return
  mountTerm()
  connectWs()
}

/** 深色终端主题固定不随明暗切换（对齐 Cloud Shell 观感） */
function newTerminal(): Terminal {
  return new Terminal({
    fontFamily: "ui-monospace, 'SF Mono', Menlo, Consolas, monospace",
    fontSize: 13,
    lineHeight: 1.2,
    cursorBlink: true,
    scrollback: 5000,
    theme: {
      background: '#1c1b1a',
      foreground: '#eceae4',
      cursor: '#d97757',
      selectionBackground: 'rgba(217, 119, 87, 0.35)',
    },
  })
}

function mountTerm() {
  if (term || !termEl.value) return
  term = newTerminal()
  fit = new FitAddon()
  term.loadAddon(fit)
  term.open(termEl.value)
  fit.fit()
  term.onData((d) => wsSendText(d))
  term.onResize(({ cols, rows }) => wsSendCtl({ type: 'resize', cols, rows }))
  // 选中即复制：终端里 Ctrl+C 保留 SIGINT 语义，复制交给选区
  term.onSelectionChange(() => {
    const sel = term?.getSelection()
    if (sel) void navigator.clipboard.writeText(sel).catch(() => undefined)
  })
  term.attachCustomKeyEventHandler(copyKeyHandler)
  ro = new ResizeObserver(() => fit?.fit())
  ro.observe(termEl.value)
}

/** Cmd+C / Ctrl+Shift+C 在有选区时作复制，其余按键交还终端 */
function copyKeyHandler(ev: KeyboardEvent): boolean {
  if (ev.type !== 'keydown') return true
  const combo = (ev.metaKey && !ev.ctrlKey) || (ev.ctrlKey && ev.shiftKey)
  if (combo && ev.key.toLowerCase() === 'c' && term?.hasSelection()) {
    void navigator.clipboard.writeText(term.getSelection()).catch(() => undefined)
    return false
  }
  return true
}

function connectWs() {
  if (!term) return
  const url = consoleSessionWsUrl(sessionId, auth.token, `rows=${term.rows}&cols=${term.cols}`)
  ws = new WebSocket(url)
  ws.binaryType = 'arraybuffer'
  ws.onopen = () => {
    phase.value = 'connected'
    term?.focus()
  }
  ws.onmessage = (ev) => {
    if (ev.data instanceof ArrayBuffer) term?.write(new Uint8Array(ev.data))
  }
  ws.onclose = (ev) => {
    if (phase.value === 'idle' || phase.value === 'closed') return
    phase.value = 'closed'
    if (!error.value) error.value = mapCloseReason(ev.reason)
  }
}

function mapCloseReason(reason: string): string {
  if (!reason) return '连接已断开'
  if (reason.includes('administratively prohibited'))
    return '串行控制台已被占用：同一时刻仅允许一路连接（可能有本地 ssh 或其他会话）'
  return reason
}

function wsSendText(data: string) {
  if (ws?.readyState === WebSocket.OPEN) ws.send(encoder.encode(data))
}

function wsSendCtl(ctl: { type: string; cols: number; rows: number }) {
  if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(ctl))
}

async function pasteFromClipboard() {
  try {
    const text = await navigator.clipboard.readText()
    if (text) term?.paste(text)
  } catch {
    message.warning('无法读取剪贴板，请直接按 Cmd/Ctrl+V 粘贴')
  }
  term?.focus()
}

/** 串口 tty 收不到 SIGWINCH，向 shell 注入 stty 同步行列数（需处于提示符） */
function syncWinsize() {
  if (!term) return
  wsSendText(`stty rows ${term.rows} cols ${term.cols}\r`)
  message.info('已发送 stty 同步命令（需处于 shell 提示符）')
  term.focus()
}

function reconnect() {
  teardown()
  void open()
}

function toggleFullscreen() {
  fullscreen.value = !fullscreen.value
  minimized.value = false
  void nextTick(() => fit?.fit())
}

function toggleMinimize() {
  minimized.value = !minimized.value
  if (!minimized.value) void nextTick(() => fit?.fit())
}

function startDrag(e: MouseEvent) {
  const startY = e.clientY
  const startH = height.value
  const move = (ev: MouseEvent) => {
    height.value = Math.min(Math.max(startH + (startY - ev.clientY), 200), window.innerHeight - 72)
  }
  const up = () => {
    window.removeEventListener('mousemove', move)
    window.removeEventListener('mouseup', up)
  }
  window.addEventListener('mousemove', move)
  window.addEventListener('mouseup', up)
  e.preventDefault()
}

function teardown() {
  openSeq++ // 使在途的 open 失效,其响应到达后自行删除会话
  ro?.disconnect()
  ro = null
  if (ws) {
    ws.onclose = null
    ws.close()
    ws = null
  }
  if (sessionId) {
    void deleteConsoleSession(sessionId).catch(() => undefined)
    sessionId = ''
  }
  term?.dispose()
  term = null
  fit = null
  phase.value = 'idle'
  error.value = ''
}

function close() {
  teardown()
  minimized.value = false
  fullscreen.value = false
  emit('update:show', false)
}

watch(
  () => props.show,
  (v) => {
    if (v) void open()
    else teardown()
  },
)

onBeforeUnmount(teardown)
</script>

<template>
  <Teleport to="body">
    <div
      v-if="show"
      class="fixed inset-x-0 bottom-0 z-[1500] flex flex-col border-t border-[#3a3835] shadow-[0_-6px_24px_rgba(0,0,0,0.35)]"
      :class="fullscreen && !minimized ? 'top-0' : ''"
    >
      <div
        v-if="!fullscreen && !minimized"
        class="h-[3px] shrink-0 cursor-ns-resize bg-[#141413] transition-colors hover:bg-[#d97757]"
        @mousedown="startDrag"
      />
      <!-- 标题栏（Cloud Shell 风格） -->
      <div class="flex h-9 shrink-0 items-center gap-2 bg-[#141413] px-3 text-[12.5px] text-[#a8a69e]">
        <span
          class="inline-block h-2 w-2 rounded-full"
          :style="{ background: phase === 'connected' ? '#8fa574' : phase === 'closed' ? '#d06060' : '#c29135' }"
        />
        <span class="truncate font-medium text-[#eceae4]">
          {{ instanceName || instanceId }} · 串行控制台
        </span>
        <span v-if="phase === 'creating'" class="text-xs">准备连接中…</span>
        <div class="flex-1" />
        <button class="ser-btn" :disabled="phase !== 'connected'" @click="pasteFromClipboard">
          粘贴
        </button>
        <button
          class="ser-btn"
          :disabled="phase !== 'connected'"
          title="向 shell 发送 stty 命令同步终端行列数"
          @click="syncWinsize"
        >
          同步窗口
        </button>
        <button v-if="phase === 'closed'" class="ser-btn text-[#d97757]" @click="reconnect">
          重连
        </button>
        <div class="mx-1 h-4 w-px bg-[#3a3835]" />
        <button class="ser-btn" :title="minimized ? '还原' : '最小化'" @click="toggleMinimize">
          {{ minimized ? '◱' : '─' }}
        </button>
        <button class="ser-btn" :title="fullscreen ? '退出全屏' : '全屏'" @click="toggleFullscreen">
          ⛶
        </button>
        <button class="ser-btn" title="关闭并结束会话" @click="close">✕</button>
      </div>
      <!-- 终端区 -->
      <div
        v-show="!minimized"
        class="relative min-h-0 bg-[#1c1b1a]"
        :class="fullscreen ? 'flex-1' : ''"
        :style="fullscreen ? {} : { height: height + 'px' }"
      >
        <div ref="termEl" class="absolute inset-0 py-1 pl-2" />
        <div
          v-if="phase !== 'connected'"
          class="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-[#1c1b1a]/92 text-[13px] text-[#a8a69e]"
        >
          <template v-if="phase === 'creating' || phase === 'connecting'">
            <NSpin size="small" />
            <div>
              {{
                phase === 'creating'
                  ? '正在清理旧连接并创建控制台连接（约 30-90 秒）…'
                  : '正在建立串行隧道…'
              }}
            </div>
          </template>
          <template v-else-if="phase === 'closed'">
            <div class="max-w-[80%] break-all text-center">{{ error || '连接已关闭' }}</div>
            <div class="max-w-[80%] text-center text-xs leading-relaxed">
              串口登录需实例本地账户密码（SSH 公钥无效）；随机 root 密码可在实例详情
              验证身份后查看。无密码也可查看启动日志或经 GRUB 单用户模式救援。
            </div>
            <button class="ser-btn !border !border-[#3a3835] !px-3 !py-1" @click="reconnect">
              重新连接
            </button>
          </template>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<style scoped>
.ser-btn {
  border-radius: 4px;
  padding: 2px 7px;
  font-size: 12px;
  line-height: 1.4;
  color: inherit;
  transition: background-color 0.15s;
}
.ser-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.1);
  color: #eceae4;
}
.ser-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
</style>
