<script setup lang="ts">
// Tier 2 Block H — day-of check-in. The host opens this on their phone
// at the venue, taps "Start scanning", and points the camera at each
// guest's QR. Live arrivals counter updates in place; walk-ins go in
// via the side button.
//
// QR decoding uses jsQR loaded from a CDN at runtime so we don't bloat
// the regular bundle. The camera is requested only when the scanner
// is open and stopped on close.

interface CheckInRecord {
  id: string
  guest_id: string
  checked_in_at: string
  arrival_count: number
  walk_in: boolean
  notes?: string | null
}

interface Summary {
  arrived_headcount: number
  expected_headcount: number
  guests_checked_in: number
}

interface ListResponse {
  check_ins: CheckInRecord[]
  summary: Summary
}

const props = defineProps<{
  eventId: string
  yourRole?: 'owner' | 'editor' | 'viewer' | null
}>()

const canEdit = computed(() => props.yourRole === 'owner' || props.yourRole === 'editor')

const loading = ref(true)
const error = ref<string | null>(null)
const checkIns = ref<CheckInRecord[]>([])
const summary = ref<Summary>({ arrived_headcount: 0, expected_headcount: 0, guests_checked_in: 0 })

// Toast (success / error per scan).
type Toast = { kind: 'success' | 'warn' | 'error'; text: string }
const toast = ref<Toast | null>(null)
let toastTimer: ReturnType<typeof setTimeout> | null = null
function showToast(t: Toast, ms = 3000) {
  toast.value = t
  if (toastTimer) clearTimeout(toastTimer)
  toastTimer = setTimeout(() => { toast.value = null }, ms)
}

async function refresh() {
  loading.value = true
  try {
    const data = await useApi<ListResponse>(`/events/${props.eventId}/check-ins`)
    checkIns.value = data.check_ins || []
    summary.value = data.summary
  } catch (e: any) {
    error.value = useErrMessage(e, 'Could not load check-ins')
  } finally {
    loading.value = false
  }
}
onMounted(refresh)

// --- camera + scanner ---

const scannerOpen = ref(false)
const videoRef = ref<HTMLVideoElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
let mediaStream: MediaStream | null = null
let scanLoopHandle: number | null = null
const lastScanAt = ref(0)
const recentlyScanned = ref<Set<string>>(new Set())

async function ensureJsQR() {
  // Load jsQR from a CDN once. We attach it to window.jsQR so the
  // scanner loop can call it. If it's already loaded (host opens +
  // closes the scanner repeatedly) reuse the existing copy.
  if (typeof window === 'undefined') return null
  const w = window as any
  if (w.jsQR) return w.jsQR
  await new Promise<void>((resolve, reject) => {
    const s = document.createElement('script')
    s.src = 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js'
    s.onload = () => resolve()
    s.onerror = () => reject(new Error('Could not load QR scanner script'))
    document.head.appendChild(s)
  })
  return w.jsQR
}

async function openScanner() {
  if (!canEdit.value) return
  try {
    const jsQR = await ensureJsQR()
    if (!jsQR) {
      showToast({ kind: 'error', text: 'QR scanner is not available in this browser.' })
      return
    }
    mediaStream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: 'environment' },
      audio: false,
    })
    scannerOpen.value = true
    await nextTick()
    if (videoRef.value) {
      videoRef.value.srcObject = mediaStream
      await videoRef.value.play()
      scanLoop(jsQR)
    }
  } catch (e: any) {
    showToast({ kind: 'error', text: e?.message || 'Could not start the camera.' })
  }
}

function closeScanner() {
  if (scanLoopHandle !== null) {
    cancelAnimationFrame(scanLoopHandle)
    scanLoopHandle = null
  }
  if (mediaStream) {
    mediaStream.getTracks().forEach((t) => t.stop())
    mediaStream = null
  }
  scannerOpen.value = false
  recentlyScanned.value.clear()
}

function scanLoop(jsQR: any) {
  const video = videoRef.value
  const canvas = canvasRef.value
  if (!video || !canvas) return
  const ctx = canvas.getContext('2d', { willReadFrequently: true })
  if (!ctx) return

  const tick = () => {
    if (!scannerOpen.value || !videoRef.value || !canvasRef.value) return
    if (videoRef.value.readyState === videoRef.value.HAVE_ENOUGH_DATA) {
      canvas.width = videoRef.value.videoWidth
      canvas.height = videoRef.value.videoHeight
      ctx.drawImage(videoRef.value, 0, 0, canvas.width, canvas.height)
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
      const code = jsQR(imageData.data, imageData.width, imageData.height, {
        inversionAttempts: 'dontInvert',
      })
      if (code && code.data && shouldProcessScan(code.data)) {
        handleScannedPayload(code.data)
      }
    }
    scanLoopHandle = requestAnimationFrame(tick)
  }
  tick()
}

// shouldProcessScan throttles + dedupes scans. The camera grabs ~30
// frames per second; without a guard we'd POST 30x for one paper QR.
function shouldProcessScan(payload: string): boolean {
  const now = Date.now()
  if (now - lastScanAt.value < 1500) return false
  if (recentlyScanned.value.has(payload)) return false
  lastScanAt.value = now
  recentlyScanned.value.add(payload)
  // Expire from the dedupe set after 10s so a guest who legitimately
  // re-scans (e.g. host re-pointing camera at the same code by accident)
  // doesn't get stuck.
  setTimeout(() => recentlyScanned.value.delete(payload), 10_000)
  return true
}

async function handleScannedPayload(payload: string) {
  try {
    const res = await useApi<{ guest: any; summary: Summary; check_in: CheckInRecord }>(
      `/events/${props.eventId}/check-in`,
      { method: 'POST', body: { qr_payload: payload, arrival_count: 1 } },
    )
    summary.value = res.summary
    checkIns.value = [res.check_in, ...checkIns.value]
    showToast({ kind: 'success', text: `${res.guest?.name || 'Guest'} is in.` })
    if (navigator.vibrate) navigator.vibrate(80)
  } catch (e: any) {
    const status = e?.response?.status ?? e?.statusCode
    if (status === 409) {
      showToast({ kind: 'warn', text: 'Already checked in.' })
    } else if (status === 410) {
      showToast({ kind: 'error', text: 'This code has expired.' })
    } else if (status === 400) {
      showToast({ kind: 'error', text: 'That doesn’t look like one of your guests.' })
    } else {
      showToast({ kind: 'error', text: useErrMessage(e, 'Could not check in') })
    }
  }
}

onUnmounted(closeScanner)

// --- walk-in ---

const walkInOpen = ref(false)
const walkInForm = reactive({
  name: '',
  email: '',
  arrival_count: 1,
  notes: '',
})
const walkInSubmitting = ref(false)

function openWalkIn() {
  walkInForm.name = ''
  walkInForm.email = ''
  walkInForm.arrival_count = 1
  walkInForm.notes = ''
  walkInOpen.value = true
}

async function submitWalkIn() {
  if (!walkInForm.name.trim()) return
  walkInSubmitting.value = true
  try {
    const res = await useApi<{ guest: any; summary: Summary; check_in: CheckInRecord }>(
      `/events/${props.eventId}/walk-ins`,
      {
        method: 'POST',
        body: {
          name: walkInForm.name.trim(),
          email: walkInForm.email.trim() || undefined,
          arrival_count: walkInForm.arrival_count,
          notes: walkInForm.notes.trim() || undefined,
        },
      },
    )
    summary.value = res.summary
    checkIns.value = [res.check_in, ...checkIns.value]
    showToast({ kind: 'success', text: `${res.guest?.name || 'Walk-in'} added.` })
    walkInOpen.value = false
  } catch (e: any) {
    showToast({ kind: 'error', text: useErrMessage(e, 'Could not add walk-in') })
  } finally {
    walkInSubmitting.value = false
  }
}

function fmtTime(iso?: string | null) {
  if (!iso) return ''
  try { return new Date(iso).toLocaleTimeString() } catch { return iso }
}

const arrivalPct = computed(() => {
  const exp = summary.value.expected_headcount
  const arr = summary.value.arrived_headcount
  if (exp === 0) return 0
  return Math.min(100, Math.round((arr / exp) * 100))
})
</script>

<template>
  <section class="card">
    <header class="mb-3 flex items-center justify-between">
      <div>
        <h2 class="text-lg font-semibold">Check-in</h2>
        <p class="text-xs text-zinc-500">Door scanner and live arrivals.</p>
      </div>
      <div v-if="canEdit" class="flex items-center gap-2">
        <button class="btn-ghost text-sm" @click="openWalkIn">+ Walk-in</button>
        <button class="btn-primary text-sm" @click="openScanner">
          <svg class="mr-1 inline-block h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
            <path d="M4 4a1 1 0 011-1h3a1 1 0 010 2H6v2a1 1 0 11-2 0V4zm0 12a1 1 0 011-1h2v-2a1 1 0 112 0v3a1 1 0 01-1 1H5a1 1 0 01-1-1zM16 4a1 1 0 00-1-1h-3a1 1 0 100 2h2v2a1 1 0 102 0V4zm0 12a1 1 0 01-1 1h-3a1 1 0 110-2h2v-2a1 1 0 112 0v3z" />
          </svg>
          Start scanning
        </button>
      </div>
    </header>

    <p v-if="error" class="mb-3 text-sm text-red-400">{{ error }}</p>
    <p v-if="loading" class="text-sm text-zinc-500">Loading…</p>

    <div v-else class="space-y-5">
      <!-- Live arrivals widget. The big number is what a host glances
           at — "are we full yet?" -->
      <div class="rounded-lg border border-brand-700/40 bg-brand-500/[0.06] p-4">
        <div class="flex items-baseline justify-between">
          <div>
            <p class="text-xs font-medium uppercase tracking-widest text-brand-400">Arrived</p>
            <p class="mt-1 text-4xl font-semibold tabular-nums text-brand-200">
              {{ summary.arrived_headcount }}
              <span class="text-base font-normal text-zinc-400">of {{ summary.expected_headcount }}</span>
            </p>
            <p class="mt-1 text-xs text-zinc-400">
              {{ summary.guests_checked_in }}
              {{ summary.guests_checked_in === 1 ? 'guest' : 'guests' }} checked in
            </p>
          </div>
          <div class="text-right">
            <p class="text-3xl font-semibold text-brand-200 tabular-nums">{{ arrivalPct }}%</p>
            <p class="text-xs text-zinc-500">of expected</p>
          </div>
        </div>
        <div class="mt-3 h-2 w-full overflow-hidden rounded-full bg-zinc-900">
          <div class="h-full rounded-full bg-brand-500 transition-all" :style="{ width: arrivalPct + '%' }"></div>
        </div>
      </div>

      <!-- Recent arrivals — keeps a host's confidence that scans are
           actually landing. -->
      <div>
        <p class="mb-2 text-xs font-medium uppercase tracking-wider text-zinc-500">Recent arrivals</p>
        <p v-if="!checkIns.length" class="text-sm text-zinc-500">No one's checked in yet.</p>
        <ul v-else class="space-y-1.5">
          <li
            v-for="c in checkIns.slice(0, 12)"
            :key="c.id"
            class="flex items-center justify-between rounded-md border border-zinc-800 bg-zinc-950 px-3 py-2 text-sm"
          >
            <div class="min-w-0 flex-1">
              <p class="text-zinc-100">
                Guest checked in
                <span v-if="c.walk_in" class="ml-1 rounded-full bg-amber-900/30 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-300">walk-in</span>
              </p>
              <p class="text-xs text-zinc-500">
                {{ fmtTime(c.checked_in_at) }} · party of {{ c.arrival_count }}
              </p>
            </div>
          </li>
        </ul>
      </div>
    </div>

    <!-- Scanner modal. Camera is only mounted while open; closing tears
         down the stream so the camera light goes off. -->
    <Teleport to="body">
      <div
        v-if="scannerOpen"
        class="fixed inset-0 z-50 flex flex-col bg-black"
        @click.self="closeScanner"
      >
        <div class="flex items-center justify-between px-4 py-3 text-white">
          <p class="text-sm font-medium">Scanning…</p>
          <button class="text-sm text-white/70 hover:text-white" @click="closeScanner">Close</button>
        </div>
        <div class="relative flex-1 overflow-hidden">
          <video ref="videoRef" playsinline class="h-full w-full object-cover"></video>
          <canvas ref="canvasRef" class="hidden"></canvas>
          <!-- A simple frame to guide the host. -->
          <div class="pointer-events-none absolute inset-0 flex items-center justify-center">
            <div class="h-64 w-64 rounded-lg border-2 border-brand-500/80 shadow-[0_0_0_9999px_rgba(0,0,0,0.55)]"></div>
          </div>
        </div>
        <div class="px-4 py-3 text-center text-xs text-white/70">
          Hold the guest's code in the frame. Toasts pop when each scan registers.
        </div>
      </div>
    </Teleport>

    <!-- Walk-in modal. -->
    <Teleport to="body">
      <div
        v-if="walkInOpen"
        class="fixed inset-0 z-40 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
        @click.self="walkInOpen = false"
      >
        <div
          role="dialog"
          aria-modal="true"
          class="w-full max-w-md rounded-lg border border-zinc-800 bg-zinc-900 p-5 shadow-2xl"
        >
          <h3 class="mb-1 text-base font-semibold">Walk-in</h3>
          <p class="mb-4 text-xs text-zinc-500">
            Adds them to your guest list and marks them checked in.
          </p>
          <form class="space-y-3" @submit.prevent="submitWalkIn">
            <div>
              <label class="label">Name</label>
              <input v-model="walkInForm.name" class="input" required />
            </div>
            <div>
              <label class="label">Email (optional)</label>
              <input v-model="walkInForm.email" type="email" class="input" />
            </div>
            <div>
              <label class="label">Party size</label>
              <input
                v-model.number="walkInForm.arrival_count"
                type="number"
                min="1"
                max="20"
                class="input"
              />
            </div>
            <div>
              <label class="label">Notes (optional)</label>
              <input v-model="walkInForm.notes" class="input" placeholder="Friend of the bride" />
            </div>
            <div class="flex items-center justify-end gap-2 pt-2">
              <button
                type="button"
                class="text-sm text-zinc-400 hover:text-zinc-200"
                :disabled="walkInSubmitting"
                @click="walkInOpen = false"
              >Cancel</button>
              <button
                type="submit"
                class="btn-primary text-sm"
                :disabled="walkInSubmitting || !walkInForm.name.trim()"
              >{{ walkInSubmitting ? 'Adding…' : 'Add walk-in' }}</button>
            </div>
          </form>
        </div>
      </div>
    </Teleport>

    <!-- Toast. -->
    <Teleport to="body">
      <Transition
        enter-active-class="transition duration-200 ease-out"
        enter-from-class="translate-y-2 opacity-0"
        enter-to-class="translate-y-0 opacity-100"
        leave-active-class="transition duration-200 ease-in"
        leave-from-class="translate-y-0 opacity-100"
        leave-to-class="translate-y-2 opacity-0"
      >
        <button
          v-if="toast"
          type="button"
          class="fixed bottom-6 right-6 z-[60] flex max-w-sm items-center gap-2 rounded-lg border px-4 py-3 text-left text-sm shadow-lg backdrop-blur"
          :class="toast.kind === 'success' ? 'border-brand-700/60 bg-brand-950/90 text-brand-100'
              : toast.kind === 'warn' ? 'border-amber-700/60 bg-amber-950/90 text-amber-100'
              : 'border-red-800/60 bg-red-950/90 text-red-100'"
          @click="toast = null"
        >{{ toast.text }}</button>
      </Transition>
    </Teleport>
  </section>
</template>
