<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { QrCode, X } from 'lucide-vue-next'
import jsQR from 'jsqr'
import {
  cameraConstraintCandidates,
  getCameraAccessErrorMessage,
  shouldTryNextCameraConstraint
} from '@/utils/cameraAccess'
import { parseWriteOffCode, type WriteOffCodePayload } from '@/utils/writeOffCode'

const router = useRouter()
const videoRef = ref<HTMLVideoElement | null>(null)
const loading = ref(false)
const scanning = ref(false)
const scanError = ref('')

let stream: MediaStream | null = null
let detector: { detect: (source: HTMLVideoElement) => Promise<Array<{ rawValue: string }>> } | null = null
let canvas: HTMLCanvasElement | null = null
let canvasContext: CanvasRenderingContext2D | null = null
let frameId = 0
let lastDecodeAt = 0
let lastScanHintAt = 0

const getBarcodeDetector = () => {
  return (window as unknown as {
    BarcodeDetector?: new (options: { formats: string[] }) => {
      detect: (source: HTMLVideoElement) => Promise<Array<{ rawValue: string }>>
    }
  }).BarcodeDetector
}

const stopScan = () => {
  scanning.value = false
  if (frameId) {
    window.cancelAnimationFrame(frameId)
    frameId = 0
  }
  if (stream) {
    stream.getTracks().forEach((track) => track.stop())
    stream = null
  }
  if (videoRef.value) {
    videoRef.value.srcObject = null
  }
  detector = null
  lastDecodeAt = 0
  lastScanHintAt = 0
}

const goConfirm = (payload: WriteOffCodePayload) => {
  router.push({
    path: '/verify/confirm',
    query: {
      orderId: payload.orderId,
      source: 'scan',
      ...(payload.packageName ? { packageName: payload.packageName } : {})
    }
  })
}

const handleCodeValue = (rawValue: string) => {
  const payload = parseWriteOffCode(rawValue)
  stopScan()
  goConfirm(payload)
}

const detectWithBarcodeDetector = async (video: HTMLVideoElement) => {
  if (!detector) return ''
  const results = await detector.detect(video)
  return results[0]?.rawValue || ''
}

const detectWithJsQr = (video: HTMLVideoElement) => {
  const width = video.videoWidth
  const height = video.videoHeight
  if (!width || !height) return ''

  if (!canvas) {
    canvas = document.createElement('canvas')
  }
  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width
    canvas.height = height
    canvasContext = canvas.getContext('2d', { willReadFrequently: true })
  }
  if (!canvasContext) return ''

  canvasContext.drawImage(video, 0, 0, width, height)
  const imageData = canvasContext.getImageData(0, 0, width, height)
  return jsQR(imageData.data, width, height, { inversionAttempts: 'dontInvert' })?.data || ''
}

const detectQrValue = async (video: HTMLVideoElement) => {
  const detectorValue = detector ? await detectWithBarcodeDetector(video).catch(() => '') : ''
  return detectorValue || detectWithJsQr(video)
}

const detectLoop = async () => {
  if (!scanning.value || !videoRef.value) return
  const video = videoRef.value

  try {
    const now = Date.now()
    if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && now - lastDecodeAt > 120) {
      lastDecodeAt = now
      const value = await detectQrValue(video)
      if (value) {
        handleCodeValue(value)
        return
      }
      if (!scanError.value && now - lastScanHintAt > 3000) {
        lastScanHintAt = now
        scanError.value = '暂未识别到二维码，请将二维码完整放入取景框'
      }
    }
  } catch {
    scanError.value = '扫码识别失败，请调整距离或切换角度后重试'
  }

  if (scanning.value) {
    frameId = window.requestAnimationFrame(detectLoop)
  }
}

const openCameraStream = async () => {
  let lastError: unknown = null
  for (const constraints of cameraConstraintCandidates) {
    try {
      return await navigator.mediaDevices.getUserMedia(constraints)
    } catch (error) {
      lastError = error
      if (!shouldTryNextCameraConstraint(error)) break
    }
  }
  throw lastError
}

const startScan = async () => {
  if (scanning.value || loading.value) return
  const BarcodeDetector = getBarcodeDetector()
  if (!navigator.mediaDevices?.getUserMedia) {
    scanError.value = '当前浏览器无法打开摄像头'
    showToast('当前浏览器无法打开摄像头')
    return
  }

  loading.value = true
  scanError.value = ''
  try {
    detector = BarcodeDetector ? new BarcodeDetector({ formats: ['qr_code'] }) : null
    stream = await openCameraStream()
    scanning.value = true
    await nextTick()
    if (!videoRef.value) return
    videoRef.value.srcObject = stream
    await videoRef.value.play()
    frameId = window.requestAnimationFrame(detectLoop)
  } catch (error) {
    stopScan()
    scanError.value = getCameraAccessErrorMessage(error)
    showToast(scanError.value)
  } finally {
    loading.value = false
  }
}

onBeforeUnmount(stopScan)
</script>

<template>
  <section class="page">
    <header class="page-header">
      <p class="page-kicker">商品核销</p>
      <h1 class="page-title">扫码核销</h1>
      <p class="page-subtitle">扫描客人二维码，核对订单信息后再确认核销。</p>
    </header>

    <section class="scan-panel panel">
      <div v-if="scanning" class="scanner-live">
        <video ref="videoRef" muted playsinline />
        <div class="scan-frame" />
        <button class="stop-button" type="button" aria-label="停止扫码" @click="stopScan">
          <X :size="18" />
        </button>
      </div>
      <button v-else class="scan-button" type="button" :disabled="loading" @click="startScan">
        <QrCode :size="54" />
        <strong>{{ loading ? '正在打开摄像头' : '点击进行扫码' }}</strong>
        <span>请将客人的核销二维码置于取景框内</span>
      </button>
      <p v-if="scanError" class="scan-error">{{ scanError }}</p>
    </section>

    <section class="scan-tips">
      <div>
        <strong>核销前确认</strong>
        <span>扫码后仍需核对订单、套餐和可核销数量。</span>
      </div>
      <div>
        <strong>识别不成功</strong>
        <span>调整距离、光线，或请客人提高屏幕亮度。</span>
      </div>
    </section>
  </section>
</template>

<style scoped>
.scan-panel {
  padding: 16px;
  background:
    radial-gradient(circle at 50% 0, rgba(19, 144, 111, 0.16), transparent 190px),
    linear-gradient(135deg, rgba(15, 139, 114, 0.1), rgba(255, 247, 223, 0.78)),
    var(--surface);
}

.scanner-live {
  position: relative;
  overflow: hidden;
  min-height: 260px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--panel-radius);
  background: #111827;
}

.scanner-live video {
  display: block;
  width: 100%;
  height: 260px;
  object-fit: cover;
}

.scan-frame {
  position: absolute;
  inset: 50% auto auto 50%;
  width: min(68vw, 230px);
  height: min(68vw, 230px);
  border: 2px solid rgba(255, 255, 255, 0.92);
  border-radius: 12px;
  box-shadow: 0 0 0 999px rgba(0, 0, 0, 0.22);
  transform: translate(-50%, -50%);
}

.scan-frame::after {
  position: absolute;
  top: 50%;
  right: 14px;
  left: 14px;
  height: 2px;
  background: var(--primary);
  box-shadow: 0 0 16px rgba(15, 139, 114, 0.9);
  content: '';
}

.stop-button {
  position: absolute;
  top: 10px;
  right: 10px;
  display: grid;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 999px;
  background: rgba(17, 24, 39, 0.68);
  color: #fff;
  place-items: center;
}

.scan-button {
  display: grid;
  width: 100%;
  min-height: 174px;
  padding: 20px 16px;
  border: 1px dashed rgba(15, 139, 114, 0.5);
  border-radius: var(--panel-radius);
  background: rgba(255, 255, 255, 0.74);
  color: var(--primary-deep);
  gap: 8px;
  place-items: center;
  transition:
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

.scan-button:active {
  border-color: var(--primary);
  background: rgba(255, 255, 255, 0.9);
  transform: scale(0.99);
}

.scan-button strong {
  color: var(--text-strong);
  font-size: 17px;
  font-weight: 750;
}

.scan-button span {
  max-width: 210px;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.45;
}

.scan-button:disabled {
  opacity: 0.72;
}

.scan-error {
  margin: 10px 0 0;
  color: var(--rose);
  font-size: 12px;
  line-height: 1.5;
}

.scan-tips {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

.scan-tips > div {
  display: grid;
  gap: 4px;
  padding: 12px 13px;
  border: 1px solid rgba(202, 214, 208, 0.78);
  border-radius: var(--panel-radius);
  background: rgba(255, 255, 255, 0.62);
}

.scan-tips strong {
  color: var(--text-strong);
  font-size: 14px;
}

.scan-tips span {
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.5;
}
</style>
