import { useEffect, useRef, useState } from "react";
import { StyleSheet, View, Text, Pressable } from "react-native";
import { BarcodeDetector } from "barcode-detector/ponyfill";
import { theme } from "../lib/theme";
import type { QRScannerProps } from "./QRScanner";

/** Web/PWA scanner using getUserMedia + the BarcodeDetector ponyfill (zxing-wasm). */
export default function QRScanner({ onScan, active }: QRScannerProps) {
  const videoRef = useRef<HTMLVideoElement | null>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const rafRef = useRef<number | null>(null);
  const activeRef = useRef(active);
  // Route onScan through a ref so the long-lived camera loop (started once in
  // useEffect) always calls the LATEST handler — otherwise a mode switch
  // (e.g. to Banquet) keeps hitting the mount-time handler.
  const onScanRef = useRef(onScan);
  const lastScan = useRef<{ code: string; at: number }>({ code: "", at: 0 });
  const [error, setError] = useState<string | null>(null);
  const [starting, setStarting] = useState(true);

  activeRef.current = active;
  onScanRef.current = onScan;

  async function start() {
    setError(null);
    setStarting(true);
    // Stop any previous stream before requesting a new one (Retry / remounts).
    streamRef.current?.getTracks().forEach((t) => t.stop());
    streamRef.current = null;
    try {
      // Watchdog: on iOS, getUserMedia can hang indefinitely when it isn't tied
      // to a user gesture (we navigate here from the name screen). If it doesn't
      // resolve, surface an error + Retry button (Retry IS a fresh gesture).
      const stream = await withTimeout(
        navigator.mediaDevices.getUserMedia({
          video: { facingMode: { ideal: "environment" } },
          audio: false,
        }),
        12000,
      );
      streamRef.current = stream;
      const video = videoRef.current;
      if (video) {
        video.srcObject = stream;
        video.setAttribute("playsinline", "true");
        video.muted = true;
        // Fire-and-forget: awaiting play() can itself hang on iOS. We only need
        // the stream attached; the tick loop waits for readyState.
        video.play().catch(() => {});
      }
      const detector = new BarcodeDetector({ formats: ["qr_code"] });
      let busy = false;
      const tick = async () => {
        rafRef.current = requestAnimationFrame(tick);
        const v = videoRef.current;
        if (!v || v.readyState < 2 || busy || !activeRef.current) return;
        busy = true;
        try {
          const codes = await detector.detect(v);
          if (codes && codes.length) {
            const data = codes[0].rawValue;
            const now = Date.now();
            if (!(data === lastScan.current.code && now - lastScan.current.at < 3000)) {
              lastScan.current = { code: data, at: now };
              onScanRef.current(data);
            }
          }
        } catch {
          /* transient decode error; keep scanning */
        } finally {
          busy = false;
        }
      };
      rafRef.current = requestAnimationFrame(tick);
      setStarting(false);
    } catch (e: any) {
      setStarting(false);
      setError(
        e?.name === "NotAllowedError"
          ? "Camera permission was denied. Allow camera access, then tap Retry."
          : e?.message === "timeout"
            ? "Camera didn't start. Tap Retry."
            : "Could not open the camera. Make sure you're on HTTPS and no other app is using it.",
      );
    }
  }

  // Reject after `ms` if the promise hasn't settled (used to un-stick getUserMedia).
  function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
    return Promise.race([
      p,
      new Promise<T>((_, reject) => setTimeout(() => reject(new Error("timeout")), ms)),
    ]);
  }

  useEffect(() => {
    start();
    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      streamRef.current?.getTracks().forEach((t) => t.stop());
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <View style={styles.fill}>
      {/* Raw DOM video element; react-dom renders it inside the RN-Web div tree. */}
      <video
        ref={videoRef as any}
        style={{ width: "100%", height: "100%", objectFit: "cover" }}
        muted
        autoPlay
        playsInline
      />
      {starting && !error && (
        <View style={styles.overlayMsg}>
          <Text style={styles.msg}>Starting camera…</Text>
        </View>
      )}
      {error && (
        <View style={styles.overlayMsg}>
          <Text style={styles.msg}>{error}</Text>
          <Pressable style={styles.btn} onPress={start}>
            <Text style={styles.btnText}>Retry</Text>
          </Pressable>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  fill: { flex: 1, width: "100%", height: "100%", backgroundColor: "#000" },
  overlayMsg: {
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    alignItems: "center",
    justifyContent: "center",
    padding: 24,
    backgroundColor: theme.bg,
  },
  msg: { color: theme.text, fontSize: 16, textAlign: "center", marginBottom: 16 },
  btn: { backgroundColor: theme.primary, paddingHorizontal: 20, paddingVertical: 12, borderRadius: 10 },
  btnText: { color: "#fff", fontSize: 16, fontWeight: "600" },
});
