"use client";

import { useEffect, useRef, useState } from "react";
import jsQR from "jsqr";
import { Button } from "@/components/ui/button";

type ScanResult = {
  success: boolean;
  message: string;
  ticketCode?: string;
  holderName?: string;
  /** Set only for package ticket types (e.g. "Piknik") — the second person sharing this QR. */
  companionName?: string | null;
};

export function QrScanner({ eventId }: { eventId: number }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const scanningRef = useRef(true);

  const [cameraError, setCameraError] = useState<string | null>(null);
  const [result, setResult] = useState<ScanResult | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);

  async function handleToken(token: string) {
    setIsSubmitting(true);

    try {
      const res = await fetch("/api/petugas/checkin/scan", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ event_id: eventId, token }),
      });

      const body = await res.json().catch(() => null);

      setResult({
        success: res.ok,
        message: body?.message ?? "An error occurred.",
        ticketCode: body?.data?.ticket?.ticket_code,
        holderName: body?.data?.ticket?.holder_name,
        companionName: body?.data?.ticket?.companion_name,
      });
    } catch {
      setResult({ success: false, message: "Unable to reach the server. Please check your connection and try again." });
    } finally {
      setIsSubmitting(false);
    }
  }

  useEffect(() => {
    let stream: MediaStream | null = null;
    let rafId: number;
    let cancelled = false;

    async function start() {
      try {
        const acquired = await navigator.mediaDevices.getUserMedia({
          video: { facingMode: "environment" },
        });

        // Effect cleanup already ran (e.g. React dev-mode double-invoke, or
        // the component unmounted) before getUserMedia resolved — stop this
        // now-orphaned stream instead of attaching it to a dead video ref.
        if (cancelled) {
          acquired.getTracks().forEach((track) => track.stop());
          return;
        }

        stream = acquired;
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          await videoRef.current.play();
        }
        if (!cancelled) tick();
      } catch (err) {
        // A stale invocation's play()/getUserMedia call can reject with
        // AbortError once cleanup has run (video element removed, or a
        // second effect invocation took over) — that's not a real camera
        // failure, so only the still-active invocation should report it.
        if (cancelled) return;

        console.error("[QrScanner] getUserMedia failed:", err);

        const messages: Record<string, string> = {
          NotAllowedError:
            "Camera access denied. Check this site's camera permission in your browser, and make sure the camera is also allowed in your OS privacy settings (e.g. Windows Settings > Privacy > Camera), then reload the page.",
          NotFoundError: "No camera detected on this device. Use Manual Search instead.",
          NotReadableError: "The camera is being used by another app/tab. Close that app, then reload the page.",
          OverconstrainedError: "This device's camera doesn't support the requested mode. Use Manual Search instead.",
        };

        const name = err instanceof DOMException ? err.name : null;
        setCameraError(
          (name && messages[name]) ?? "Unable to access the camera. Allow camera access in your browser, or use Manual Search instead.",
        );
      }
    }

    function tick() {
      rafId = requestAnimationFrame(tick);

      const video = videoRef.current;
      const canvas = canvasRef.current;
      if (!video || !canvas || video.readyState !== video.HAVE_ENOUGH_DATA) return;
      if (!scanningRef.current) return;

      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      const ctx = canvas.getContext("2d");
      if (!ctx) return;

      ctx.drawImage(video, 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);

      if (code?.data) {
        scanningRef.current = false;
        void handleToken(code.data);
      }
    }

    start();

    return () => {
      cancelled = true;
      cancelAnimationFrame(rafId);
      stream?.getTracks().forEach((track) => track.stop());
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- start()/tick() close over refs, not state
  }, []);

  function scanAgain() {
    setResult(null);
    scanningRef.current = true;
  }

  return (
    <div className="flex flex-col items-center gap-4">
      {cameraError ? (
        <p className="rounded-xl bg-red-50 px-4 py-6 text-center text-sm text-red-600">{cameraError}</p>
      ) : (
        <div className="relative w-full overflow-hidden rounded-xl bg-black">
          <video ref={videoRef} className="w-full" muted playsInline />
          <canvas ref={canvasRef} className="hidden" />
        </div>
      )}

      {isSubmitting && <p className="text-sm text-kolabora-neutral-dark/70">Processing...</p>}

      {result && (
        <div
          className={`w-full rounded-xl border p-4 text-center ${
            result.success ? "border-green-300 bg-green-50" : "border-red-300 bg-red-50"
          }`}
        >
          <p className={`font-medium ${result.success ? "text-green-700" : "text-red-700"}`}>
            {result.message}
          </p>
          {result.ticketCode && (
            <p className="mt-1 text-sm text-kolabora-neutral-dark/70">
              {result.ticketCode} &middot; {result.holderName}
              {result.companionName && <> + {result.companionName}</>}
            </p>
          )}
          <Button type="button" onClick={scanAgain} className="mt-3">
            Scan Next
          </Button>
        </div>
      )}
    </div>
  );
}
