<template>
  <div class="viewer-wrap" :class="{ embedded }">
    <!-- Toolbar -->
    <div class="viewer-bar">
      <el-button v-if="!embedded" size="small" text @click="router.back()">
        <el-icon><ArrowLeft /></el-icon> 返回
      </el-button>
      <div class="viewer-title">
        <el-icon><component :is="pageIcon" /></el-icon>
        <span>{{ page?.name || '...' }}</span>
        <el-tag v-if="page?.access_mode === 'proxy' || page?.use_proxy" size="small" type="warning" style="margin-left:4px">代理</el-tag>
        <el-tag v-else-if="page?.access_mode === 'remote_browser'" size="small" type="success" style="margin-left:4px">远程浏览器</el-tag>
      </div>
      <div class="viewer-url">{{ remoteSession?.url || page?.url }}</div>
      <div class="viewer-actions">
        <el-tooltip v-if="isRemoteBrowser" content="后退">
          <el-button size="small" text @click="sendRemoteCommand('back')">
            <el-icon><Back /></el-icon>
          </el-button>
        </el-tooltip>
        <el-tooltip v-if="isRemoteBrowser" content="前进">
          <el-button size="small" text @click="sendRemoteCommand('forward')">
            <el-icon><Right /></el-icon>
          </el-button>
        </el-tooltip>
        <el-tooltip content="在新标签页打开">
          <el-button size="small" text @click="openExternal">
            <el-icon><TopRight /></el-icon>
          </el-button>
        </el-tooltip>
        <el-tooltip content="刷新">
          <el-button size="small" text @click="reload">
            <el-icon><Refresh /></el-icon>
          </el-button>
        </el-tooltip>
      </div>
    </div>

    <div class="viewer-body">
      <div v-if="isRemoteBrowser" class="viewer-content viewer-content-remote">
        <div class="viewer-stage" :class="{ 'viewer-stage-error': showRemoteError }">
          <div
            v-if="!showRemoteError"
            ref="remoteFrameRef"
            class="remote-frame"
            tabindex="0"
            @click="focusRemoteFrame"
            @keydown="onRemoteKeydown"
            @paste.prevent="onRemotePaste"
            @wheel.prevent="onRemoteWheel"
            @pointermove="onRemotePointerMove"
            @pointerup="onRemotePointerUp"
            @pointercancel="onRemotePointerCancel"
            @lostpointercapture="onRemotePointerCancel"
            @dblclick="onRemoteDblClick"
            @focus="remoteFrameFocused = true"
            @blur="remoteFrameFocused = false"
          >
            <div
              v-if="remoteFrameFocused && remoteCaret"
              class="remote-caret"
              :style="{ left: remoteCaret.x + 'px', top: remoteCaret.y + 'px', height: remoteCaret.h + 'px' }"
            />
            <img
              v-if="remoteScreenshotUrl"
              :src="remoteScreenshotUrl"
              class="remote-screen"
              alt=""
              draggable="false"
              @load="() => { iframeLoading = false }"
              @error="() => handleRemoteSessionFailure(undefined, '远程浏览器截图加载失败')"
              @pointerdown.stop.prevent="onRemotePointerDown"
              @pointermove.stop.prevent="onRemotePointerMove"
              @pointerup.stop.prevent="onRemotePointerUp"
              @pointercancel.stop.prevent="onRemotePointerCancel"
            />
          </div>

          <div v-if="iframeLoading && !showRemoteError" class="stage-overlay stage-loading">
            <el-icon class="spin" :size="32"><Loading /></el-icon>
            <p>{{ isReconnectingRemoteBrowser ? '正在重连远程浏览器…' : '正在加载页面…' }}</p>
          </div>

          <div v-else-if="showRemoteError && remoteErrorState" class="remote-error-shell">
            <div class="remote-error-card">
              <div class="remote-error-copy">
                <span class="remote-error-kicker">远程浏览器</span>
                <el-icon class="error-icon" :size="36"><Warning /></el-icon>
                <div class="error-title">{{ remoteErrorState.title }}</div>
                <p class="error-description">{{ remoteErrorState.description }}</p>
                <p class="error-hint">{{ remoteErrorState.hint }}</p>
                <p v-if="remoteErrorState.technicalDetail" class="error-detail">
                  技术原因：{{ remoteErrorState.technicalDetail }}
                </p>
              </div>
              <div class="remote-error-actions">
                <el-button type="primary" @click="retryRemoteBrowser()">
                  {{ remoteErrorState.actionLabel }}
                </el-button>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div v-else class="viewer-content viewer-content-iframe">
        <div v-if="iframeLoading" class="iframe-loading">
          <el-icon class="spin" :size="32"><Loading /></el-icon>
          <p>正在加载页面…</p>
        </div>
        <iframe
          v-show="!iframeLoading"
          ref="iframeRef"
          :src="iframeSrc"
          class="page-iframe"
          @load="onLoad"
          @error="onError"
        />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, markRaw } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
  ArrowLeft, TopRight, Refresh, Loading, Back, Right, Warning,
  Link as LinkIcon, Monitor, SetUp, Reading, Cpu, DataLine,
  Grid, Connection, Ticket, Wallet, Key, Tools, Star, House,
} from '@element-plus/icons-vue'
import { browserSessionsApi, customPagesApi, type BrowserEventPayload, type BrowserSessionData, type CustomPageData } from '@/api'
import { useAuthStore } from '@/stores/auth'

const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const props = withDefaults(defineProps<{
  pageId?: number
  active?: boolean
  embedded?: boolean
}>(), {
  active: true,
  embedded: false,
})

const iconMap: Record<string, any> = {
  Link: markRaw(LinkIcon), Monitor: markRaw(Monitor), SetUp: markRaw(SetUp),
  Reading: markRaw(Reading), Cpu: markRaw(Cpu), DataLine: markRaw(DataLine),
  Grid: markRaw(Grid), Connection: markRaw(Connection), Ticket: markRaw(Ticket),
  Wallet: markRaw(Wallet), Key: markRaw(Key), Tools: markRaw(Tools),
  Star: markRaw(Star), House: markRaw(House),
}

const page = ref<CustomPageData | null>(null)
const iframeRef = ref<HTMLIFrameElement>()
const remoteFrameRef = ref<HTMLDivElement>()
const iframeLoading = ref(true)
const remoteSession = ref<BrowserSessionData | null>(null)
const remoteScreenshotUrl = ref('')
const isStartingRemoteBrowser = ref(false)
const isReconnectingRemoteBrowser = ref(false)
const remoteErrorState = ref<RemoteBrowserErrorState | null>(null)
let startRemoteBrowserPromise: Promise<void> | null = null
let screenshotObjectUrl = ''
let mouseMoveTimer: number | undefined
let caretHideTimer: number | undefined

// Caret / cursor overlay
const remoteFrameFocused = ref(false)
const remoteCaret = ref<{ x: number; y: number; h: number } | null>(null)
const remotePointer = ref<{
  id: number
  button: 'left' | 'right' | 'middle'
  dragging: boolean
  lastMoveAt: number
} | null>(null)

// WebSocket state
let ws: WebSocket | null = null
let wsReconnectTimer: number | undefined
let wsReconnectAttempts = 0
const WS_MAX_RECONNECT = 5
const WS_RECONNECT_BASE_MS = 800
const REMOTE_DRAG_MOVE_INTERVAL_MS = 16
const REMOTE_HOVER_MOVE_INTERVAL_MS = 80

type RemoteBrowserErrorState = {
  title: string
  description: string
  hint: string
  actionLabel: string
  technicalDetail: string
}

const pageIcon = computed(() => iconMap[page.value?.icon || 'Link'] || LinkIcon)
const isRemoteBrowser = computed(() => page.value?.access_mode === 'remote_browser')
const effectivePageId = computed(() => props.pageId ?? Number(route.params.id))
const embedded = computed(() => props.embedded)
const showRemoteError = computed(() => Boolean(remoteErrorState.value) && !isStartingRemoteBrowser.value)

// Build the iframe src: use backend proxy if use_proxy=true
// Pass JWT token as query param since iframe can't set Authorization header
const iframeSrc = computed(() => {
  if (!page.value) return ''
  if (page.value.access_mode === 'proxy' || page.value.use_proxy) {
    const token = encodeURIComponent(auth.token || '')
    return `/api/custom-pages/${page.value.id}/proxy/?token=${token}`
  }
  return page.value.url
})

async function loadPage(id: number) {
  iframeLoading.value = true
  clearRemoteError()
  try {
    const res = await customPagesApi.list()
    page.value = res.data.find(p => p.id === id) || null
    if (!page.value) {
      ElMessage.error('页面不存在')
      router.push('/custom-pages')
      return
    }
    if (isRemoteBrowser.value) {
      await startRemoteBrowser()
    }
  } catch {
    ElMessage.error('加载失败')
  }
}

function onLoad() {
  iframeLoading.value = false
}

function onError() {
  iframeLoading.value = false
}

function openExternal() {
  if (page.value?.url) window.open(page.value.url, '_blank', 'noopener')
}

function reload() {
  if (!page.value) return
  if (isRemoteBrowser.value) {
    if (remoteErrorState.value || !remoteSession.value) {
      retryRemoteBrowser()
      return
    }
    sendRemoteCommand('reload')
    return
  }
  if (!iframeRef.value) return
  iframeLoading.value = true
  iframeRef.value.src = iframeSrc.value
}

async function startRemoteBrowser(options: { reconnect?: boolean } = {}) {
  if (startRemoteBrowserPromise) return startRemoteBrowserPromise
  startRemoteBrowserPromise = doStartRemoteBrowser(options).finally(() => {
    startRemoteBrowserPromise = null
  })
  return startRemoteBrowserPromise
}

async function doStartRemoteBrowser(options: { reconnect?: boolean }) {
  if (!page.value || !remoteFrameRef.value) {
    await nextTick()
  }
  if (!page.value) return
  isStartingRemoteBrowser.value = true
  isReconnectingRemoteBrowser.value = !!options.reconnect
  iframeLoading.value = true
  clearRemoteError()
  await nextTick()
  stopRemoteWs()
  await closeRemoteSession()
  try {
    const viewport = remoteViewport()
    const res = await browserSessionsApi.create({
      custom_page_id: page.value.id,
      width: viewport.width,
      height: viewport.height,
    })
    remoteSession.value = res.data
    if (!options.reconnect) wsReconnectAttempts = 0
    if (props.active) {
      connectRemoteWs()
    } else {
      iframeLoading.value = false
    }
    await nextTick()
    remoteFrameRef.value?.focus()
  } catch (e: any) {
    setRemoteError(e.response?.status, e.response?.data?.detail || '无法启动服务端 Chromium')
  } finally {
    isStartingRemoteBrowser.value = false
    isReconnectingRemoteBrowser.value = false
  }
}

/** Connect (or reconnect) WebSocket for the current session. */
function connectRemoteWs() {
  if (!remoteSession.value || !props.active) return
  stopRemoteWs()

  const sessionId = remoteSession.value.id
  const url = browserSessionsApi.wsUrl(sessionId, auth.token)
  const socket = new WebSocket(url)
  ws = socket
  socket.binaryType = 'blob'

  socket.onopen = () => {
    wsReconnectAttempts = 0
  }

  socket.onmessage = (evt) => {
    if (evt.data instanceof Blob) {
      // Binary frame = JPEG screenshot
      const newUrl = URL.createObjectURL(evt.data)
      setRemoteScreenshotUrl(newUrl)
      iframeLoading.value = false
      return
    }
    // Text frame = JSON control message
    try {
      const msg = JSON.parse(evt.data as string)
      if (msg.type === 'init' && msg.session) {
        remoteSession.value = msg.session as BrowserSessionData
        return
      }
      if (msg.error) {
        if (msg.error === 'session_not_found') {
          handleRemoteSessionFailure(404, msg.error)
          return
        }
        handleRemoteSessionFailure(undefined, String(msg.error))
      }
    } catch {
      // ignore malformed JSON
    }
  }

  socket.onerror = () => {
    // onerror is always followed by onclose — handle everything in onclose
  }

  socket.onclose = (evt) => {
    if (ws !== socket) return // already replaced
    ws = null
    // Don't reconnect if we intentionally closed or page is inactive
    if (!props.active || !remoteSession.value) return
    // Abnormal close: try to reconnect with exponential backoff
    if (wsReconnectAttempts < WS_MAX_RECONNECT) {
      const delay = WS_RECONNECT_BASE_MS * 2 ** wsReconnectAttempts
      wsReconnectAttempts++
      wsReconnectTimer = window.setTimeout(connectRemoteWs, delay)
    } else {
      handleRemoteSessionFailure(502, 'WebSocket 连接失败，已超过最大重连次数')
    }
  }
}

function stopRemoteWs() {
  if (wsReconnectTimer !== undefined) {
    window.clearTimeout(wsReconnectTimer)
    wsReconnectTimer = undefined
  }
  if (ws) {
    // Prevent onclose from triggering reconnect
    const old = ws
    ws = null
    old.close()
  }
}

/** Send an event to the remote browser over WebSocket (falls back to HTTP if WS not ready). */
async function sendRemoteEvent(payload: BrowserEventPayload) {
  if (!props.active || isStartingRemoteBrowser.value) return
  if (!remoteSession.value) return
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(payload))
    return
  }
  // Fallback: HTTP POST (e.g. during reconnect window)
  try {
    const res = await browserSessionsApi.event(remoteSession.value.id, payload)
    remoteSession.value = res.data
  } catch (e: any) {
    await handleRemoteSessionFailure(e.response?.status, e.response?.data?.detail || '远程浏览器会话已失效')
  }
}

function sendRemoteCommand(type: 'reload' | 'back' | 'forward') {
  sendRemoteEvent({ type })
}

function remoteViewport() {
  const rect = remoteFrameRef.value?.getBoundingClientRect()
  return {
    width: Math.round(rect?.width || window.innerWidth),
    height: Math.round(rect?.height || window.innerHeight - 44),
  }
}

function eventPoint(event: MouseEvent | PointerEvent, options: { clamp?: boolean } = {}) {
  const image = event.currentTarget instanceof HTMLImageElement
    ? event.currentTarget
    : remoteFrameRef.value?.querySelector<HTMLImageElement>('.remote-screen')
  const rect = image?.getBoundingClientRect()
  if (!image || !rect || !image.naturalWidth || !image.naturalHeight) return null
  if (!options.clamp && (
    event.clientX < rect.left ||
    event.clientX > rect.right ||
    event.clientY < rect.top ||
    event.clientY > rect.bottom
  )) {
    return null
  }
  const clientX = options.clamp ? Math.max(rect.left, Math.min(rect.right, event.clientX)) : event.clientX
  const clientY = options.clamp ? Math.max(rect.top, Math.min(rect.bottom, event.clientY)) : event.clientY
  return {
    x: Math.max(0, Math.min(image.naturalWidth, ((clientX - rect.left) / rect.width) * image.naturalWidth)),
    y: Math.max(0, Math.min(image.naturalHeight, ((clientY - rect.top) / rect.height) * image.naturalHeight)),
  }
}

function showRemoteCaret(event: MouseEvent | PointerEvent) {
  focusRemoteFrame()
  const point = eventPoint(event)
  if (!point) return
  // Show caret overlay at click position (proportional to displayed image)
  const img = event.currentTarget instanceof HTMLImageElement
    ? event.currentTarget
    : remoteFrameRef.value?.querySelector<HTMLImageElement>('.remote-screen')
  const imgRect = img?.getBoundingClientRect()
  const frameRect = remoteFrameRef.value?.getBoundingClientRect()
  if (imgRect && frameRect && img) {
    const scale = imgRect.height / (img.naturalHeight || imgRect.height)
    const caretH = Math.max(14, Math.round(20 * scale))
    remoteCaret.value = {
      x: event.clientX - frameRect.left,
      y: event.clientY - frameRect.top - caretH / 2,
      h: caretH,
    }
    if (caretHideTimer) window.clearTimeout(caretHideTimer)
    caretHideTimer = window.setTimeout(() => { remoteCaret.value = null }, 8000)
  }
}

function onRemoteDblClick(event: MouseEvent) {
  const point = eventPoint(event)
  if (!point) return
  sendRemoteEvent({ type: 'dblclick', ...point, button: mouseButton(event) })
}

function onRemotePointerDown(event: PointerEvent) {
  if (event.pointerType === 'mouse' && event.button > 2) return
  focusRemoteFrame()
  showRemoteCaret(event)
  const point = eventPoint(event)
  if (!point) return
  const button = mouseButton(event)
  remotePointer.value = {
    id: event.pointerId,
    button,
    dragging: true,
    lastMoveAt: performance.now(),
  }
  remoteFrameRef.value?.setPointerCapture?.(event.pointerId)
  sendRemoteEvent({ type: 'mousedown', ...point, button })
}

function onRemotePointerMove(event: PointerEvent) {
  const activePointer = remotePointer.value
  const dragging = Boolean(activePointer?.dragging && activePointer.id === event.pointerId)
  const now = performance.now()
  const throttleMs = dragging ? REMOTE_DRAG_MOVE_INTERVAL_MS : REMOTE_HOVER_MOVE_INTERVAL_MS
  if (dragging && activePointer && now - activePointer.lastMoveAt < throttleMs) return
  if (!dragging && mouseMoveTimer) return
  if (dragging && activePointer) {
    activePointer.lastMoveAt = now
  } else {
    mouseMoveTimer = window.setTimeout(() => { mouseMoveTimer = undefined }, throttleMs)
  }
  const point = eventPoint(event, { clamp: dragging })
  if (!point) return
  sendRemoteEvent({ type: 'mousemove', ...point })
}

function onRemotePointerUp(event: PointerEvent) {
  const activePointer = remotePointer.value
  if (!activePointer || activePointer.id !== event.pointerId) return
  const point = eventPoint(event, { clamp: true })
  remotePointer.value = null
  remoteFrameRef.value?.releasePointerCapture?.(event.pointerId)
  if (!point) return
  sendRemoteEvent({ type: 'mouseup', ...point, button: activePointer.button })
}

function onRemotePointerCancel(event: PointerEvent) {
  const activePointer = remotePointer.value
  if (!activePointer || activePointer.id !== event.pointerId) return
  const point = eventPoint(event, { clamp: true })
  remotePointer.value = null
  remoteFrameRef.value?.releasePointerCapture?.(event.pointerId)
  if (!point) return
  sendRemoteEvent({ type: 'mouseup', ...point, button: activePointer.button })
}

function onRemoteWheel(event: WheelEvent) {
  const point = eventPoint(event)
  if (!point) return
  sendRemoteEvent({ type: 'scroll', delta_x: event.deltaX, delta_y: event.deltaY, ...point })
}

function onRemoteKeydown(event: KeyboardEvent) {
  // Let Ctrl+V / Meta+V propagate so the browser fires the 'paste' event
  // which we handle in onRemotePaste with the actual clipboard text.
  const isPaste = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v'
  if (isPaste) return

  // Prevent browser from handling other keys (scrolling, shortcuts, etc.)
  event.preventDefault()

  if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
    sendRemoteEvent({ type: 'type', text: event.key })
    return
  }
  const key = keyName(event)
  if (key) sendRemoteEvent({ type: 'key', key })
}

/** Paste handler: sends local clipboard text as a 'type' event to the remote browser. */
function onRemotePaste(event: ClipboardEvent) {
  const text = event.clipboardData?.getData('text/plain') || ''
  if (text) sendRemoteEvent({ type: 'type', text })
}

function keyName(event: KeyboardEvent) {
  const parts: string[] = []
  if (event.ctrlKey) parts.push('Control')
  if (event.altKey) parts.push('Alt')
  if (event.metaKey) parts.push('Meta')
  if (event.shiftKey && event.key.length !== 1) parts.push('Shift')
  const key = event.key === ' ' ? 'Space' : event.key
  if (!['Control', 'Alt', 'Meta', 'Shift'].includes(key)) parts.push(key)
  return parts.join('+')
}

function mouseButton(event: MouseEvent | PointerEvent): 'left' | 'right' | 'middle' {
  if (event.button === 1) return 'middle'
  if (event.button === 2) return 'right'
  return 'left'
}

function focusRemoteFrame() {
  remoteFrameRef.value?.focus()
}

async function closeRemoteSession() {
  if (!remoteSession.value) return
  const id = remoteSession.value.id
  remoteSession.value = null
  setRemoteScreenshotUrl('')
  await browserSessionsApi.close(id).catch(() => undefined)
}

function setRemoteScreenshotUrl(url: string) {
  if (screenshotObjectUrl) {
    URL.revokeObjectURL(screenshotObjectUrl)
    screenshotObjectUrl = ''
  }
  remoteScreenshotUrl.value = url
  if (url.startsWith('blob:')) screenshotObjectUrl = url
}

function retryRemoteBrowser() {
  startRemoteBrowser({ reconnect: true })
}

function clearRemoteError() {
  remoteErrorState.value = null
}

function normalizeRemoteErrorDetail(message?: string) {
  return (message || '')
    .trim()
    .replace(/^Browser error:\s*/i, '')
    .replace(/^session error:\s*/i, '')
    .replace(/\s+/g, ' ')
}

function mapRemoteBrowserError(status?: number, message?: string): RemoteBrowserErrorState {
  const detail = normalizeRemoteErrorDetail(message)
  const lowerDetail = detail.toLowerCase()
  const actionLabel = '重新创建会话'

  if (
    status === 503
    || lowerDetail.includes('browserdependencyerror')
    || lowerDetail.includes('playwright is not installed')
    || lowerDetail.includes('unable to start playwright')
  ) {
    return {
      title: '远程浏览器依赖未安装',
      description: '当前服务端缺少 Playwright 或浏览器运行时，暂时无法创建远程浏览器会话。',
      hint: '请先在服务端安装远程浏览器依赖，再返回这里重新创建会话。',
      actionLabel,
      technicalDetail: detail,
    }
  }

  if (
    status === 404
    || lowerDetail.includes('session_not_found')
    || lowerDetail.includes('browser session not found')
    || lowerDetail.includes('会话已失效')
  ) {
    return {
      title: '远程浏览器会话已失效',
      description: '当前远程浏览器会话已经不存在，可能是服务重启、会话过期或会话已被清理。',
      hint: '点击下方按钮可重新创建会话，并继续在当前页面中查看远程内容。',
      actionLabel,
      technicalDetail: detail,
    }
  }

  if (
    status === 409
    || lowerDetail.includes('browser page is closed')
    || lowerDetail.includes('page is closed')
  ) {
    return {
      title: '远程页面已关闭',
      description: '当前远程浏览器页面已经关闭，现有会话不能继续使用。',
      hint: '请重新创建会话，系统会重新打开并连接到目标页面。',
      actionLabel,
      technicalDetail: detail,
    }
  }

  if (status === 502) {
    return {
      title: '远程浏览器连接异常',
      description: '服务端远程浏览器在启动或通信过程中发生异常，当前页面未能成功连接。',
      hint: '可以先重试创建会话；如果持续失败，请检查服务端浏览器环境和目标站点可访问性。',
      actionLabel,
      technicalDetail: detail,
    }
  }

  return {
    title: '远程浏览器暂时不可用',
    description: '当前页面模块未能建立远程浏览器连接，其它后台区域仍可继续使用。',
    hint: '请尝试重新创建会话；如果问题持续存在，再检查服务端日志中的浏览器错误。',
    actionLabel,
    technicalDetail: detail,
  }
}

function setRemoteError(status: number | undefined, message?: string) {
  iframeLoading.value = false
  remoteSession.value = null
  setRemoteScreenshotUrl('')
  remoteCaret.value = null
  remoteErrorState.value = mapRemoteBrowserError(status, message)
}

async function handleRemoteSessionFailure(status: number | undefined, message: string) {
  stopRemoteWs()
  await closeRemoteSession()
  setRemoteError(status, message || '远程浏览器会话已失效')
}

watch(() => route.params.id, (id) => {
  if (!props.pageId && id) {
    stopRemoteWs()
    closeRemoteSession()
    loadPage(Number(id))
  }
}, { immediate: false })

watch(() => props.pageId, (id, oldId) => {
  if (id && id !== oldId) {
    stopRemoteWs()
    closeRemoteSession()
    loadPage(id)
  }
})

watch(() => props.active, (active) => {
  if (!isRemoteBrowser.value) return
  if (active) {
    if (remoteSession.value && (!ws || ws.readyState !== WebSocket.OPEN)) {
      connectRemoteWs()
    }
    nextTick(() => remoteFrameRef.value?.focus())
  } else {
    stopRemoteWs()
  }
})

onMounted(() => {
  loadPage(effectivePageId.value)
})

onBeforeUnmount(() => {
  stopRemoteWs()
  closeRemoteSession()
})
</script>

<style scoped>
.viewer-wrap {
  /* Expand to full viewport minus topbar, ignoring parent padding */
  position: fixed;
  inset: 0;
  top: var(--topbar-height);
  left: var(--sidebar-width);
  display: flex;
  flex-direction: column;
  background: var(--bg-base);
  z-index: 10;
  overflow: hidden;
}

.viewer-wrap.embedded {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: auto;
}

.viewer-body {
  flex: 1;
  min-height: 0;
  position: relative;
}

.viewer-bar {
  height: 34px;
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 10px;
  flex-shrink: 0;
}

.viewer-title {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
}

.viewer-url {
  flex: 1;
  font-size: 11px;
  color: var(--text-muted);
  background: var(--bg-elevated);
  border-radius: 6px;
  padding: 3px 8px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.viewer-actions { display: flex; gap: 2px; }

.viewer-actions :deep(.el-button) {
  min-height: 1.75rem;
  padding: 0 0.45rem;
}

.viewer-content {
  width: 100%;
  height: 100%;
  min-height: 0;
}

.viewer-content-iframe {
  position: relative;
  overflow: hidden;
}

.viewer-content-remote {
  padding: 0.75rem;
  background:
    radial-gradient(circle at top, rgba(217, 139, 66, 0.08), transparent 30%),
    linear-gradient(180deg, rgba(255, 244, 232, 0.03), rgba(10, 8, 7, 0.04));
}

.viewer-stage {
  position: relative;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  border: 1px solid rgba(140, 119, 98, 0.28);
  border-radius: 1.25rem;
  background:
    linear-gradient(180deg, rgba(31, 25, 21, 0.96), rgba(18, 14, 12, 0.98)),
    var(--bg-surface);
  box-shadow: inset 0 0 0 1px rgba(255, 244, 232, 0.03);
}

.viewer-stage.viewer-stage-error {
  background:
    linear-gradient(180deg, rgba(47, 38, 31, 0.96), rgba(24, 19, 16, 0.98)),
    var(--bg-surface);
}

.page-iframe {
  width: 100%;
  height: 100%;
  display: block;
  border: none;
  background: #fff;
}

.remote-frame {
  flex: 1;
  width: 100%;
  min-height: 0;
  background: #f7f8fa;
  display: flex;
  align-items: center;
  justify-content: center;
  outline: none;
  overflow: hidden;
  touch-action: none;
  position: relative;
  padding: 0.6rem;
}

.remote-screen {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  user-select: none;
  cursor: crosshair;
}

.remote-caret {
  position: absolute;
  width: 2px;
  background: #1a73e8;
  border-radius: 1px;
  pointer-events: none;
  z-index: 10;
  transform: translateX(-1px);
  animation: caret-blink 1.1s step-end infinite;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.6);
}
@keyframes caret-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

.stage-overlay,
.iframe-loading {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: rgba(18, 14, 12, 0.68);
  backdrop-filter: blur(4px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: var(--text-muted);
}

.iframe-loading {
  background: var(--bg-base);
  backdrop-filter: none;
}

.spin {
  animation: spin 1s linear infinite;
  color: var(--color-primary);
}
@keyframes spin { to { transform: rotate(360deg); } }

.error-icon { color: #f59e0b; }

.remote-error-shell {
  height: 100%;
  padding: 1rem;
  display: grid;
  place-items: center;
}

.remote-error-card {
  width: min(100%, 34rem);
  padding: 1.5rem;
  display: grid;
  gap: 1.2rem;
  border-radius: 1.25rem;
  border: 1px solid rgba(239, 175, 99, 0.18);
  background:
    linear-gradient(180deg, rgba(255, 244, 232, 0.08), rgba(255, 244, 232, 0.03)),
    rgba(28, 22, 18, 0.92);
  box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.2);
}

.remote-error-copy {
  display: grid;
  gap: 0.7rem;
}

.remote-error-kicker {
  color: var(--text-soft);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.error-title {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text-primary);
}

.error-description,
.error-hint,
.error-detail {
  margin: 0;
  line-height: 1.65;
}

.error-description {
  font-size: 0.95rem;
  color: var(--text-primary);
}

.error-hint {
  font-size: 0.88rem;
  color: var(--text-secondary);
}

.error-detail {
  font-size: 0.82rem;
  color: var(--text-soft);
}

.remote-error-actions {
  display: flex;
  justify-content: flex-start;
}

@media (max-width: 768px) {
  .viewer-wrap {
    left: 0;
  }

  .viewer-bar {
    height: auto;
    min-height: 52px;
    flex-wrap: wrap;
    padding: 8px;
  }

  .viewer-title {
    max-width: calc(100% - 92px);
  }

  .viewer-url {
    order: 4;
    flex-basis: 100%;
  }

  .viewer-content-remote {
    padding: 0.6rem;
  }

  .remote-frame,
  .remote-error-shell {
    padding: 0.75rem;
  }

  .remote-error-card {
    padding: 1.15rem;
  }

  .remote-error-actions {
    width: 100%;
  }

  .remote-error-actions :deep(.el-button) {
    width: 100%;
  }
}
</style>
