<template>
  <div class="webrtc-wrapper">
    <video
      ref="videoRef"
      autoplay
      muted
      playsinline
      class="video-player"
    ></video>

    <div v-if="loading" class="loading-overlay">
      <span class="spinner"></span>
      <span>{{ loadingText }}</span>
    </div>

    <div v-if="error" class="error-overlay">{{ error }}</div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'

const props = defineProps({
  src: { type: String, required: true }
})

const emit = defineEmits(['connection-status'])

const videoRef = ref(null)
const error = ref('')
const loading = ref(false)
const loadingText = ref('连接中...')

let pc = null
let sessionUrl = ''
let queuedCandidates = []
let offerData = null
let retryTimer = null
let retryCount = 0
const MAX_RETRIES = 30
const RETRY_INTERVAL = 2000
let destroyed = false

const whepUrl = () => {
  const base = props.src.replace(/\/+$/, '')
  return base + '/whep'
}

function parseOffer(sdp) {
  const ret = { iceUfrag: '', icePwd: '', medias: [] }
  for (const line of sdp.split('\r\n')) {
    if (line.startsWith('m=')) ret.medias.push(line.slice('m='.length))
    else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) ret.iceUfrag = line.slice('a=ice-ufrag:'.length)
    else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) ret.icePwd = line.slice('a=ice-pwd:'.length)
  }
  return ret
}

function generateSdpFragment(od, candidates) {
  const byMedia = {}
  for (const c of candidates) {
    const mid = c.sdpMLineIndex
    if (!byMedia[mid]) byMedia[mid] = []
    byMedia[mid].push(c)
  }
  let frag = 'a=ice-ufrag:' + od.iceUfrag + '\r\n' + 'a=ice-pwd:' + od.icePwd + '\r\n'
  let mid = 0
  for (const media of od.medias) {
    if (byMedia[mid]) {
      frag += 'm=' + media + '\r\n' + 'a=mid:' + mid + '\r\n'
      for (const c of byMedia[mid]) frag += 'a=' + c.candidate + '\r\n'
    }
    mid++
  }
  return frag
}

function linkToIceServers(links) {
  if (!links) return []
  return links.split(', ').map((link) => {
    const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i)
    const ret = { urls: [m[1]] }
    if (m[3] !== undefined) {
      ret.username = JSON.parse(`"${m[3]}"`)
      ret.credential = JSON.parse(`"${m[4]}"`)
      ret.credentialType = 'password'
    }
    return ret
  })
}

function sendLocalCandidates(candidates) {
  fetch(sessionUrl, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/trickle-ice-sdpfrag', 'If-Match': '*' },
    body: generateSdpFragment(offerData, candidates)
  }).catch(() => {})
}

function cleanupConnection() {
  if (pc) { pc.close(); pc = null }
  if (videoRef.value) videoRef.value.srcObject = null
  sessionUrl = ''
  queuedCandidates = []
  offerData = null
}

function cancelRetry() {
  if (retryTimer) {
    clearTimeout(retryTimer)
    retryTimer = null
  }
}

async function connect() {
  if (!props.src || destroyed) return
  cancelRetry()
  cleanupConnection()

  loading.value = true
  loadingText.value = retryCount > 0 ? `连接中... (第 ${retryCount} 次重试)` : '连接中...'
  error.value = ''

  try {
    const optionsRes = await fetch(whepUrl(), { method: 'OPTIONS' })
    const iceServers = linkToIceServers(optionsRes.headers.get('Link'))

    pc = new RTCPeerConnection({ iceServers, sdpSemantics: 'unified-plan' })
    pc.addTransceiver('video', { direction: 'sendrecv' })
    pc.addTransceiver('audio', { direction: 'sendrecv' })

    pc.ontrack = (event) => {
      if (videoRef.value && event.streams[0]) {
        videoRef.value.srcObject = event.streams[0]
        emit('connection-status', true)
        retryCount = 0
        loading.value = false
        error.value = ''
      }
    }

    pc.onicecandidate = (evt) => {
      if (evt.candidate !== null) {
        if (sessionUrl) sendLocalCandidates([evt.candidate])
        else queuedCandidates.push(evt.candidate)
      }
    }

    const offer = await pc.createOffer()
    offerData = parseOffer(offer.sdp)
    await pc.setLocalDescription(offer)

    const res = await fetch(whepUrl(), {
      method: 'POST',
      headers: { 'Content-Type': 'application/sdp' },
      body: offer.sdp
    })

    if (!res.ok) {
      const errBody = await res.text().catch(() => '')
      const isNotPublishing = errBody.includes('no one is publishing')
      if (isNotPublishing) throw new Error('NOT_PUBLISHING')
      throw new Error(errBody || `HTTP ${res.status}`)
    }

    const location = res.headers.get('location')
    if (location) sessionUrl = new URL(location, whepUrl()).toString()

    const answer = await res.text()
    await pc.setRemoteDescription({ type: 'answer', sdp: answer })

    if (queuedCandidates.length > 0 && sessionUrl) {
      sendLocalCandidates(queuedCandidates)
      queuedCandidates = []
    }

    retryCount = 0
  } catch (e) {
    const isNotPublishing = e.message === 'NOT_PUBLISHING'
    cleanupConnection()

    if (isNotPublishing && retryCount < MAX_RETRIES && !destroyed) {
      retryCount++
      loadingText.value = `等待推流中... (${retryCount}/${MAX_RETRIES})`
      retryTimer = setTimeout(() => connect(), RETRY_INTERVAL)
    } else {
      error.value = isNotPublishing ? '视频流尚未就绪，请稍后重试' : '连接失败，请检查视频地址'
      emit('connection-status', false)
      loading.value = false
      console.error('[WebRTC]', e.message === 'NOT_PUBLISHING' ? 'stream not publishing' : e)
    }
  }
}

watch(() => props.src, (newSrc, oldSrc) => {
  if (newSrc && newSrc !== oldSrc) {
    retryCount = 0
    cancelRetry()
    connect()
  }
})

onMounted(() => connect())

onUnmounted(() => {
  destroyed = true
  cancelRetry()
  cleanupConnection()
})
</script>

<style scoped>
.webrtc-wrapper { width: 100%; height: 100%; position: relative; background: #000; }
.video-player { width: 100%; height: 100%; object-fit: contain; }
.error-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; }

.loading-overlay {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: rgba(255, 255, 255, 0.85);
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.spinner {
  width: 20px;
  height: 20px;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: #409eff;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}
</style>
