<template>
  <div class="preview-container">
    <div ref="videoContainer" class="video_container">
      <video
        id="player"
        ref="videoElement"
        autoplay
        muted
        oncontextmenu="return false;"
        playsinline
        poster="../assets/black.jpg"
        @mousedown="startDrag"
        @mousemove="onDrag"
        @mouseup="endDrag"
        @mouseleave="endDrag"
      />
    </div>
  </div>
  <div class="video-control">
    <div class="video-control-left">
      <!--刷新键-->
      <div class="control-btn pull-left" title="刷新">
        <el-icon
          class="control-btn"
          color="inherit"
          @click="refreshWebSocket"
        >
          <refresh />
        </el-icon>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, onUnmounted, readonly, ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'

const props = defineProps({
  dragFlag: {
    type: Boolean,
    default: false
  },
  channel: {
    type: String,
    default: 'ch1'
  }
})

// 获取 WebSocket 服务器地址
const wsHost = import.meta.env.MODE === 'development'
  ? import.meta.env.VITE_HOST_IP
  : window.location.host

const wsVideoPort = 8000
const wsHeartbeatPort = 8001

const emit = defineEmits(['video-error'])

// Refs
const videoContainer = ref<HTMLElement | null>(null)
const videoElement = ref<HTMLVideoElement | null>(null)

// 视频缩放/拖拽状态
const scale = ref(1)
const position = ref({ x: 0, y: 0 })
let dragState = {
  dragging: false,
  startX: 0,
  startY: 0
}

// WebSocket 状态
let wfsObj: WfsInstance | null = null
let heartbeatWs: WebSocket | null = null
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let reconnectAttempts = 0
const maxReconnectAttempts = 5

// mediaError 恢复计数（不占用 reconnectAttempts 配额）
let mediaRecoveryAttempts = 0
const maxMediaRecoveryAttempts = 10

// 防止 handleMediaError 被连续触发多次
let isRecovering = false

// 页面可见性状态
let lastTimeStamp = 0
let lastVideoTime = 0
let isPageHidden = false

// ==================== 工具函数 ====================

const debounce = (func: (...args: any[]) => void, wait: number) => {
  let timeout: ReturnType<typeof setTimeout>
  return (...args: any[]) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => func(...args), wait)
  }
}

// ==================== 缩放和拖拽 ====================

const zoom = (event: WheelEvent) => {
  if (!props.dragFlag) return
  event.preventDefault()

  const zoomSpeed = 0.1
  const delta = event.deltaY < 0 ? zoomSpeed : -zoomSpeed
  scale.value = Math.max(1, Math.min(3, scale.value + delta))
  applyTransform()
}

const startDrag = (event: MouseEvent) => {
  if (!props.dragFlag) return
  dragState.dragging = true
  dragState.startX = event.clientX - position.value.x
  dragState.startY = event.clientY - position.value.y
  event.preventDefault()
}

const onDrag = (event: MouseEvent) => {
  if (!dragState.dragging || !props.dragFlag) return
  position.value.x = event.clientX - dragState.startX
  position.value.y = event.clientY - dragState.startY
  applyTransform()
}

const endDrag = () => {
  dragState.dragging = false
}

const applyTransform = () => {
  if (!videoElement.value || !videoContainer.value || !props.dragFlag) return

  const videoWidth = videoElement.value.offsetWidth * scale.value
  const videoHeight = videoElement.value.offsetHeight * scale.value
  const containerWidth = videoContainer.value.offsetWidth
  const containerHeight = videoContainer.value.offsetHeight

  const maxX = Math.max(0, (videoWidth - containerWidth) / 2)
  const maxY = Math.max(0, (videoHeight - containerHeight) / 2)

  position.value.x = Math.min(maxX, Math.max(-maxX, position.value.x))
  position.value.y = Math.min(maxY, Math.max(-maxY, position.value.y))

  videoElement.value.style.transform =
    `translate(${position.value.x}px, ${position.value.y}px) scale(${scale.value})`
}

// ==================== WFS 视频流管理 ====================

/**
 * 彻底重置 video 元素上的 MediaSource，
 * 防止 WFS 内部 doAppending 访问残留的 mediaElement.error 导致
 * "Cannot read properties of undefined (reading 'error')" 崩溃。
 */
const resetVideoElement = () => {
  const video = videoElement.value
  if (!video) return

  // 暂停播放
  video.pause()

  // 重置缩放和位移状态
  scale.value = 1
  position.value = { x: 0, y: 0 }
  video.style.transform = ''

  // 断开 MediaSource 绑定
  video.removeAttribute('src')
  video.srcObject = null

  // 强制浏览器释放旧的 MediaSource / SourceBuffer
  video.load()
}

const createWebSocket = () => {
  try {
    if (!window.Wfs?.isSupported()) {
      throw new Error('WFS not supported')
    }

    const video = videoElement.value
    if (!video) {
      throw new Error('Video element not found')
    }

    const socketURL = `ws://${wsHost}:${wsVideoPort}/websocket`

    wfsObj = new window.Wfs({
      wsMinPacketInterval: 2000,
      wsMaxPacketInterval: 8000
    })

    // console.log("props.channel",props.channel)

    wfsObj.attachMedia(video, props.channel, 'H264Raw', socketURL)

    wfsObj.on(window.Wfs.Events.ERROR, handleWfsError)
    wfsObj.on(window.Wfs.Events.MANIFEST_PARSED, () => {
      // 连接成功，重置所有恢复计数
      reconnectAttempts = 0
      mediaRecoveryAttempts = 0
      isRecovering = false
    })
  } catch (error) {
    console.error('Failed to create WebSocket:', error)
    emit('video-error', error)
    scheduleReconnect()
  }
}

const handleWfsError = (_eventName: string, data: any) => {
  console.error('WFS Error:', data)

  if (!data.fatal) return

  emit('video-error', data)

  switch (data.type) {
    case window.Wfs.ErrorTypes.MEDIA_ERROR:
      handleMediaError()
      break
    case window.Wfs.ErrorTypes.NETWORK_ERROR:
      console.warn('Network error, scheduling reconnect...')
      scheduleReconnect()
      break
    default:
      console.error('Unrecoverable error, scheduling reconnect...')
      scheduleReconnect()
      break
  }
}

/**
 * mediaError 恢复策略：
 *
 * error code 3 (MEDIA_ERR_DECODE) 表示浏览器解码失败。
 * WFS 内部在连续 3 次 doAppending 检测到 error 后触发 fatal。
 *
 * 恢复方式：完整重建（销毁 WFS + 重置 video + 重建 WebSocket + 重建心跳）
 * 后端会从新的关键帧开始推流，解码器状态干净。
 *
 * mediaError 的恢复不消耗 reconnectAttempts 配额，
 * 因为这不是网络问题，而是解码问题，每次重建都有很大概率成功。
 * 给 10 次机会，持续失败才放弃。
 */
const handleMediaError = () => {
  if (isRecovering) return
  isRecovering = true

  mediaRecoveryAttempts++

  if (mediaRecoveryAttempts <= maxMediaRecoveryAttempts) {
    console.warn(
      `Media error recovery ${mediaRecoveryAttempts}/${maxMediaRecoveryAttempts}, rebuilding...`
    )
    cleanup()
    setTimeout(() => {
      isRecovering = false
      createWebSocket()
      createHeartbeatWs()
    }, 300)
  } else {
    console.error('Media error recovery exhausted')
    mediaRecoveryAttempts = 0
    isRecovering = false
    emit('video-error', { type: 'mediaRecoveryFailed', fatal: true })
  }
}

// ==================== 心跳 WebSocket ====================

const createHeartbeatWs = () => {
  try {
    const socketURL = `ws://${wsHost}:${wsHeartbeatPort}/websocket`
    heartbeatWs = new WebSocket(socketURL)

    heartbeatWs.onopen = () => {
      console.log('Heartbeat WebSocket connected')
      startHeartbeat()
    }

    heartbeatWs.onmessage = (event: MessageEvent) => {
      handleHeartbeatMessage(event)
    }

    heartbeatWs.onclose = () => {
      console.log('Heartbeat WebSocket disconnected')
      stopHeartbeat()
    }

    heartbeatWs.onerror = (error) => {
      console.error('Heartbeat WebSocket error:', error)
    }
  } catch (error) {
    console.error('Failed to create heartbeat WebSocket:', error)
  }
}

const handleHeartbeatMessage = (event: MessageEvent) => {
  try {
    if (event.data instanceof Blob) {
      const reader = new FileReader()
      reader.readAsText(event.data, 'utf-8')
      reader.onload = () => {
        try {
          const data = JSON.parse(reader.result as string)
          processHeartbeatData(data)
        } catch (e) {
          console.error('Failed to parse heartbeat blob:', e)
        }
      }
    } else if (typeof event.data === 'string') {
      if (event.data.includes('pong')) {
        startHeartbeat()
        return
      }
      const data = JSON.parse(event.data)
      processHeartbeatData(data)
    }
  } catch (error) {
    console.error('Failed to parse heartbeat message:', error)
  }
}

const processHeartbeatData = (data: any) => {
  if (data.serialNum && heartbeatWs?.readyState === WebSocket.OPEN) {
    heartbeatWs.send(JSON.stringify({ serialNum: data.serialNum }))
  }
}

// 心跳机制
const heartbeatConfig = {
  timeout: 1000,
  maxRetries: 3,
  retryCount: 0
}

const startHeartbeat = () => {
  stopHeartbeat()

  heartbeatTimer = setTimeout(() => {
    if (heartbeatWs?.readyState === WebSocket.OPEN) {
      heartbeatWs.send('ping')
      heartbeatConfig.retryCount = 0
    } else {
      handleHeartbeatFailure()
    }
  }, heartbeatConfig.timeout)
}

const stopHeartbeat = () => {
  if (heartbeatTimer) {
    clearTimeout(heartbeatTimer)
    heartbeatTimer = null
  }
}

const handleHeartbeatFailure = () => {
  heartbeatConfig.retryCount++
  if (heartbeatConfig.retryCount >= heartbeatConfig.maxRetries) {
    console.warn('Heartbeat failed, reconnecting...')
    scheduleReconnect()
  }
}

// ==================== 重连机制 ====================

const scheduleReconnect = () => {
  if (reconnectAttempts >= maxReconnectAttempts) {
    console.error('Max reconnection attempts reached')
    emit('video-error', { type: 'reconnectFailed', fatal: true })
    return
  }

  // 指数退避，最大 10 秒
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000)
  reconnectAttempts++

  if (reconnectTimer) {
    clearTimeout(reconnectTimer)
  }

  reconnectTimer = setTimeout(() => {
    console.log(`Reconnection attempt ${reconnectAttempts}/${maxReconnectAttempts}`)
    cleanup()
    setTimeout(() => {
      createWebSocket()
      createHeartbeatWs()
    }, 300)
  }, delay)
}

// ==================== 清理 ====================

const destroyWfs = () => {
  if (wfsObj) {
    try {
      // 先移除事件监听，防止 destroy 过程中再触发 error 回调
      wfsObj.off(window.Wfs.Events.ERROR, handleWfsError)
      wfsObj.destroy()
    } catch (error) {
      console.error('Error destroying WFS:', error)
    }
    wfsObj = null
  }
}

const cleanup = () => {
  stopHeartbeat()

  if (reconnectTimer) {
    clearTimeout(reconnectTimer)
    reconnectTimer = null
  }

  destroyWfs()
  resetVideoElement()

  if (heartbeatWs) {
    try {
      heartbeatWs.close()
    } catch (error) {
      console.error('Error closing heartbeat WebSocket:', error)
    }
    heartbeatWs = null
  }
}

// 手动刷新：重置所有计数
const refreshWebSocket = () => {
  cleanup()
  reconnectAttempts = 0
  mediaRecoveryAttempts = 0
  isRecovering = false

  setTimeout(() => {
    createWebSocket()
    createHeartbeatWs()
  }, 500)
}

// ==================== 页面可见性处理 ====================

const handleVisibilityChange = () => {
  const video = videoElement.value
  if (!video) return

  if (document.hidden) {
    isPageHidden = true
    lastTimeStamp = Date.now()
    lastVideoTime = video.currentTime
  } else if (isPageHidden) {
    isPageHidden = false
    const elapsed = (Date.now() - lastTimeStamp) / 1000

    // 如果离开超过 30 秒，直接刷新连接避免 SourceBuffer 积压
    if (elapsed > 30) {
      console.log('Page was hidden for too long, refreshing stream...')
      refreshWebSocket()
    } else {
      video.currentTime = lastVideoTime + elapsed
    }
  }
}

// 防抖缩放（约 60fps）
const debouncedZoom = debounce(zoom, 16)

// ==================== 生命周期 ====================

onMounted(() => {
  if (videoContainer.value) {
    videoContainer.value.addEventListener('wheel', debouncedZoom, { passive: false })
  }

  document.addEventListener('visibilitychange', handleVisibilityChange)
  window.addEventListener('beforeunload', cleanup)

  refreshWebSocket()
})

onUnmounted(() => {
  if (videoContainer.value) {
    videoContainer.value.removeEventListener('wheel', debouncedZoom)
  }
  document.removeEventListener('visibilitychange', handleVisibilityChange)
  window.removeEventListener('beforeunload', cleanup)

  cleanup()
})

defineExpose({
  zoom: debouncedZoom,
  startDrag,
  onDrag,
  endDrag,
  refreshWebSocket,
  scale: readonly(scale),
  position: readonly(position)
})
</script>

<style lang="scss" scoped>
.preview-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.video_container {
  overflow: hidden;
  position: relative;
  width: 100%;
  height: 100%;
  background-color: #000;

  video {
    width: 100%;
    height: 100%;
    object-fit: contain;
    cursor: grab;

    &:active {
      cursor: grabbing;
    }
  }
}

.video-control {
  position: absolute;
  width: 100%;
  height: 32px;
  line-height: 32px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.3));
  left: 0;
  right: 0;
  bottom: 0;
  backdrop-filter: blur(4px);
  transition: opacity 0.3s ease;

  &:hover {
    opacity: 1;
  }

  .video-control-left {
    position: absolute;
    left: 8px;
    height: 32px;
    display: flex;
    align-items: center;
  }

  .control-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    background: rgba(255, 255, 255, 0.1);
    cursor: pointer;
    font-size: 16px;
    color: white;
    border-radius: 4px;
    transition: all 0.2s ease;
    border: none;

    &:hover {
      background: rgba(255, 255, 255, 0.2);
      transform: scale(1.1);
    }

    &:active {
      transform: scale(0.95);
    }
  }
}

@media (max-width: 768px) {
  .video-control {
    height: 40px;
    line-height: 40px;

    .control-btn {
      width: 32px;
      height: 32px;
      font-size: 18px;
    }
  }
}
</style>
