/**
 * 管理后台 WebRTC 开播（观众端始终单路视频）
 * - camera：仅摄像头
 * - screen_only：仅共享屏幕 + 麦克风
 * - screen_pip：屏幕与摄像头 Canvas 合成；小窗位置由 getPipRect() 归一化矩形 (nx,ny,nw,nh) 决定，与预览拖动一致
 * - 直播中可 switchMode 切换，无需结束直播
 */
const apiBase = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '')

const LIVE_CAPTURE_QUALITY_STORAGE_KEY = 'yh_live_capture_quality'

const QUALITY_MEDIA = {
  source: { video: true, audio: true },
  high: {
    video: {
      width: { ideal: 1280 },
      height: { ideal: 720 },
      frameRate: { ideal: 30 }
    },
    audio: true
  },
  mid: {
    video: {
      width: { ideal: 854 },
      height: { ideal: 480 },
      frameRate: { ideal: 24 }
    },
    audio: true
  },
  low: {
    video: {
      width: { ideal: 640 },
      height: { ideal: 360 },
      frameRate: { ideal: 20 }
    },
    audio: true
  }
}

function effectivePublishQualityKey() {
  try {
    const v = localStorage.getItem(LIVE_CAPTURE_QUALITY_STORAGE_KEY)
    if (v && QUALITY_MEDIA[v]) return v
  } catch (_) {}
  return 'source'
}

function liveWsURLPublish(token) {
  const q = effectivePublishQualityKey()
  const path = `/api/web/live/ws?role=publish&token=${encodeURIComponent(token)}&quality=${encodeURIComponent(q)}`
  if (apiBase) {
    const base = apiBase.replace(/\/$/, '')
    const wsOrigin = base.replace(/^https:/i, 'wss:').replace(/^http:/i, 'ws:')
    return `${wsOrigin}${path}`
  }
  const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
  return `${proto}//${window.location.host}${path}`
}

const defaultIce = [{ urls: 'stun:stun.l.google.com:19302' }]

const MAX_SIGNAL_RECONNECT = 15

const CANVAS_W = 1280
const CANVAS_H = 720

function healthCheckUrl() {
  if (apiBase) return `${apiBase}/api/health`
  if (typeof window !== 'undefined') return `${window.location.origin}/api/health`
  return '/api/health'
}

function buildCameraConstraints(publishKey, videoDeviceId) {
  const preset = QUALITY_MEDIA[publishKey] || QUALITY_MEDIA.source
  const dev = videoDeviceId ? { deviceId: { exact: videoDeviceId } } : {}
  if (preset.video === true) {
    return {
      audio: true,
      video: videoDeviceId ? { deviceId: { exact: videoDeviceId } } : true
    }
  }
  return {
    audio: true,
    video: { ...preset.video, ...dev }
  }
}

function clamp(v, lo, hi) {
  return Math.min(hi, Math.max(lo, v))
}

/** @param {() => { nx?: number, ny?: number, nw?: number, nh?: number } | null | undefined} getPipRect */
function readPipRect(getPipRect) {
  const d = typeof getPipRect === 'function' ? getPipRect() : null
  const nw = clamp(Number(d?.nw) || 0.24, 0.08, 0.55)
  const nh = clamp(Number(d?.nh) || 0.24, 0.08, 0.55)
  const defNx = 1 - nw - 10 / CANVAS_W
  const defNy = 1 - nh - 10 / CANVAS_H
  const nx = clamp(Number(d?.nx) || defNx, 0, 1 - nw)
  const ny = clamp(Number(d?.ny) || defNy, 0, 1 - nh)
  return { nx, ny, nw, nh }
}

function humanizeGetUserMediaError(err) {
  const name = err && err.name
  const raw = ((err && err.message) || '').toLowerCase()
  if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {
    return '已拒绝摄像头或麦克风权限。'
  }
  if (name === 'NotFoundError' || name === 'DevicesNotFoundError') {
    return '未检测到摄像头。'
  }
  if (
    name === 'NotReadableError' ||
    raw.includes('could not start video source') ||
    raw.includes('failed to start video source') ||
    raw.includes('video source')
  ) {
    return '摄像头被占用或不可用，请关闭其它使用相机的程序后重试。'
  }
  if (name === 'OverconstrainedError') {
    return '当前摄像头不满足画质约束，请在官网「直播」页改为「原画」或换摄像头。'
  }
  if (name === 'AbortError') {
    return '采集被中断，请重试。'
  }
  return (err && err.message) || '无法打开摄像头'
}

export function startPublishing(opts = {}) {
  const {
    token = '',
    captureMode: initialMode = 'camera',
    videoDeviceId: initialDeviceId = '',
    onStatus = () => {},
    onLocalStream = () => {},
    onActiveModeChange = () => {},
    getPipRect = null
  } = opts

  if (!token) {
    onStatus('未登录，无法开播')
    return { stop: () => {}, switchMode: async () => {} }
  }

  const publishKey = effectivePublishQualityKey()
  const wsUrl = liveWsURLPublish(token)

  let activeMode = initialMode
  let deviceIdState = initialDeviceId

  let closedByLocal = false
  let stream = null
  let ws = null
  let pc = null
  let reconnectTimer = null
  let reconnectAttempt = 0
  let wsGen = 0
  let reconnectStopped = false
  let switchBusy = false

  let rafId = null
  let vScreen = null
  let vCam = null
  let canvasEl = null
  let screenShareTrack = null

  function teardownComposite() {
    if (rafId != null) {
      cancelAnimationFrame(rafId)
      rafId = null
    }
    if (vScreen) {
      try {
        const so = vScreen.srcObject
        if (so) so.getTracks().forEach((t) => t.stop())
      } catch (_) {}
      try {
        vScreen.srcObject = null
      } catch (_) {}
      vScreen = null
    }
    if (vCam) {
      try {
        const so = vCam.srcObject
        if (so) so.getTracks().forEach((t) => t.stop())
      } catch (_) {}
      try {
        vCam.srcObject = null
      } catch (_) {}
      vCam = null
    }
    canvasEl = null
    screenShareTrack = null
  }

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

  const send = (o) => {
    if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(o))
  }

  async function acquireDisplayStream() {
    let display
    try {
      display = await navigator.mediaDevices.getDisplayMedia({
        video: true,
        audio: false
      })
    } catch (e) {
      const name = e && e.name
      throw new Error(name === 'NotAllowedError' ? '已取消或未允许屏幕共享' : (e && e.message) || '屏幕共享失败')
    }
    const sTr = display.getVideoTracks()[0]
    if (!sTr) {
      display.getTracks().forEach((t) => t.stop())
      throw new Error('未获得屏幕画面')
    }
    screenShareTrack = sTr
    sTr.addEventListener('ended', () => {
      if (!closedByLocal) {
        onStatus('屏幕共享已结束')
        stop()
      }
    })
    return display
  }

  async function buildPublishStream() {
    teardownComposite()
    if (activeMode === 'screen_only') {
      const display = await acquireDisplayStream()
      const sTr = display.getVideoTracks()[0]
      let micStream
      try {
        micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      } catch (e) {
        sTr.stop()
        display.getTracks().forEach((t) => t.stop())
        throw e
      }
      const previewVid = new MediaStream([sTr])
      onLocalStream({ layout: 'screen_only', main: previewVid })
      return new MediaStream([sTr, ...micStream.getAudioTracks()])
    }
    if (activeMode === 'screen_pip') {
      const display = await acquireDisplayStream()
      const sTr = display.getVideoTracks()[0]
      let cam
      try {
        cam = await navigator.mediaDevices.getUserMedia(buildCameraConstraints(publishKey, deviceIdState))
      } catch (e) {
        sTr.stop()
        display.getTracks().forEach((t) => t.stop())
        throw e
      }
      vScreen = document.createElement('video')
      vCam = document.createElement('video')
      vScreen.muted = true
      vCam.muted = true
      vScreen.playsInline = true
      vCam.playsInline = true
      vScreen.srcObject = display
      vCam.srcObject = cam
      await vScreen.play().catch(() => {})
      await vCam.play().catch(() => {})

      canvasEl = document.createElement('canvas')
      canvasEl.width = CANVAS_W
      canvasEl.height = CANVAS_H
      const ctx = canvasEl.getContext('2d')

      function tick() {
        if (closedByLocal || !canvasEl) return
        ctx.fillStyle = '#000'
        ctx.fillRect(0, 0, CANVAS_W, CANVAS_H)
        if (vScreen && vScreen.readyState >= 2) {
          try {
            ctx.drawImage(vScreen, 0, 0, CANVAS_W, CANVAS_H)
          } catch (_) {}
        }
        if (vCam && vCam.readyState >= 2) {
          const { nx, ny, nw, nh } = readPipRect(getPipRect)
          const pw = Math.round(CANVAS_W * nw)
          const ph = Math.round(CANVAS_H * nh)
          const px = Math.round(CANVAS_W * nx)
          const py = Math.round(CANVAS_H * ny)
          ctx.strokeStyle = 'rgba(64,158,255,0.9)'
          ctx.lineWidth = 3
          ctx.strokeRect(px, py, pw, ph)
          try {
            ctx.drawImage(vCam, px, py, pw, ph)
          } catch (_) {}
        }
        rafId = requestAnimationFrame(tick)
      }
      tick()

      const cap = canvasEl.captureStream(30)
      const outV = cap.getVideoTracks()[0]
      if (!outV) {
        teardownComposite()
        sTr.stop()
        cam.getTracks().forEach((t) => t.stop())
        throw new Error('画布采集失败')
      }
      const mic = cam.getAudioTracks()
      const publish = new MediaStream([outV, ...mic])
      onLocalStream({
        layout: 'screen_pip',
        screen: display,
        cam: new MediaStream([cam.getVideoTracks()[0]])
      })
      return publish
    }
    const s = await navigator.mediaDevices.getUserMedia(buildCameraConstraints(publishKey, deviceIdState))
    onLocalStream({ layout: 'camera', main: s })
    return s
  }

  async function ensureStreamAndAttach() {
    const needNew =
      !stream ||
      !stream.getTracks().length ||
      stream.getTracks().some((t) => t.readyState !== 'live')
    if (needNew) {
      stream?.getTracks().forEach((t) => {
        try {
          t.stop()
        } catch (_) {}
      })
      teardownComposite()
      try {
        stream = await buildPublishStream()
      } catch (e) {
        const msg =
          e && typeof e.message === 'string' && e.message
            ? e.message
            : humanizeGetUserMediaError(e)
        onStatus(msg)
        throw e
      }
    }
    if (pc) {
      try {
        pc.close()
      } catch (_) {}
      pc = null
    }
    pc = new RTCPeerConnection({ iceServers: defaultIce })
    pc.onicecandidate = (e) => {
      if (e.candidate) send({ type: 'ice', candidate: e.candidate.toJSON() })
    }
    stream.getTracks().forEach((t) => {
      if (t.readyState === 'live') pc.addTrack(t, stream)
    })
    const offer = await pc.createOffer()
    await pc.setLocalDescription(offer)
    send({ type: 'offer', sdp: offer.sdp })
    onStatus('协商中…')
  }

  async function switchMode(mode, camDeviceId) {
    if (closedByLocal || switchBusy || !pc || pc.signalingState === 'closed') return
    switchBusy = true
    if (typeof camDeviceId === 'string') deviceIdState = camDeviceId
    const switchingTo = mode
    activeMode = mode
    onStatus('切换画面中…')
    try {
      stream?.getTracks().forEach((t) => {
        try {
          t.stop()
        } catch (_) {}
      })
      stream = null
      teardownComposite()
      try {
        stream = await buildPublishStream()
      } catch (e1) {
        if (switchingTo === 'screen_pip' || switchingTo === 'screen_only') {
          activeMode = 'camera'
          try {
            onActiveModeChange('camera')
          } catch (_) {}
          onStatus(
            e1?.message ? `${e1.message}，已切回仅摄像头` : '屏幕共享未就绪，已切回仅摄像头'
          )
          stream = await buildPublishStream()
        } else {
          throw e1
        }
      }
      const vT = stream.getVideoTracks()[0]
      const aT = stream.getAudioTracks()[0]
      const vSender = pc.getSenders().find((s) => s.track?.kind === 'video')
      const aSender = pc.getSenders().find((s) => s.track?.kind === 'audio')
      if (vSender && vT) {
        await vSender.replaceTrack(vT)
      } else if (vT) {
        pc.addTrack(vT, stream)
      }
      if (aSender && aT) {
        await aSender.replaceTrack(aT)
      } else if (aT) {
        pc.addTrack(aT, stream)
      }
      const offer = await pc.createOffer({ iceRestart: false })
      await pc.setLocalDescription(offer)
      send({ type: 'offer', sdp: offer.sdp })
      onStatus('已切换，协商中…')
    } catch (e) {
      onStatus(e?.message || humanizeGetUserMediaError(e))
    } finally {
      switchBusy = false
    }
  }

  function onSocketMessage(ev) {
    let msg
    try {
      msg = JSON.parse(ev.data)
    } catch {
      return
    }
    if (msg.type === 'answer' && msg.sdp && pc) {
      pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp }).then(
        () => {
          reconnectAttempt = 0
          clearReconnectTimer()
          onStatus('直播中')
        },
        (e) => {
          onStatus(e.message || '协商失败')
        }
      )
    }
    if (msg.type === 'ice' && msg.candidate && pc) {
      try {
        pc.addIceCandidate(msg.candidate)
      } catch (_) {}
    }
    if (msg.type === 'error') {
      onStatus(msg.message || '服务端错误')
    }
  }

  function scheduleReconnect() {
    if (closedByLocal || reconnectStopped) return
    clearReconnectTimer()
    reconnectAttempt += 1
    if (reconnectAttempt > MAX_SIGNAL_RECONNECT) {
      reconnectStopped = true
      onStatus(
        `信令已重试 ${MAX_SIGNAL_RECONNECT} 次仍失败。多为 API 502 或未启动。修复后刷新本页再开播。`
      )
      return
    }
    const delay = Math.min(2000 * Math.pow(1.45, reconnectAttempt - 1), 28000)
    onStatus(`信令断开，约 ${Math.round(delay / 1000)} 秒后重试（${reconnectAttempt}/${MAX_SIGNAL_RECONNECT}）…`)
    reconnectTimer = window.setTimeout(async () => {
      reconnectTimer = null
      if (closedByLocal || reconnectStopped) return
      try {
        const r = await fetch(healthCheckUrl(), { method: 'GET', cache: 'no-store' })
        if (!r.ok) {
          onStatus(`API 不可用（HTTP ${r.status}）`)
        }
      } catch (_) {
        onStatus('无法访问健康检查接口')
      }
      if (closedByLocal || reconnectStopped) return
      openSignalingSocket()
    }, delay)
  }

  function openSignalingSocket() {
    if (closedByLocal || reconnectStopped) return
    const myGen = ++wsGen
    clearReconnectTimer()
    if (ws) {
      try {
        ws.close()
      } catch (_) {}
      ws = null
    }
    if (pc) {
      try {
        pc.close()
      } catch (_) {}
      pc = null
    }
    try {
      ws = new WebSocket(wsUrl)
    } catch (_) {
      onStatus('无法连接信令')
      scheduleReconnect()
      return
    }
    ws.onopen = async () => {
      if (closedByLocal || myGen !== wsGen) return
      onStatus('采集中…')
      try {
        await ensureStreamAndAttach()
      } catch (err) {
        if (!closedByLocal) {
          onStatus(err?.message || humanizeGetUserMediaError(err))
          stop()
        }
      }
    }
    ws.onmessage = onSocketMessage
    ws.onerror = () => {
      if (!closedByLocal) onStatus('信令异常')
    }
    ws.onclose = () => {
      if (myGen !== wsGen) return
      ws = null
      if (pc) {
        try {
          pc.close()
        } catch (_) {}
        pc = null
      }
      if (closedByLocal) return
      scheduleReconnect()
    }
  }

  function stop() {
    closedByLocal = true
    reconnectStopped = true
    wsGen += 1
    clearReconnectTimer()
    teardownComposite()
    if (ws) {
      try {
        ws.close()
      } catch (_) {}
      ws = null
    }
    if (pc) {
      try {
        pc.close()
      } catch (_) {}
      pc = null
    }
    stream?.getTracks().forEach((t) => {
      try {
        t.stop()
      } catch (_) {}
    })
    stream = null
  }

  openSignalingSocket()

  return {
    stop,
    switchMode: (mode, camDeviceId) => switchMode(mode, camDeviceId)
  }
}
