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).
 *
 * iOS Safari only reliably grants the camera from inside a user gesture, and it
 * loses that context across the app's navigation (PIN → name → scanner). So we
 * auto-attempt on mount but always keep a tappable overlay: if auto-start
 * doesn't produce a playing video, one tap starts it in a real gesture context.
 */
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);
  const startingRef = useRef(false);
  const playingRef = useRef(false);
  const onScanRef = useRef(onScan);
  const lastScan = useRef<{ code: string; at: number }>({ code: "", at: 0 });
  const [playing, setPlaying] = useState(false);
  const [error, setError] = useState<string | null>(null);

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

  async function start() {
    if (startingRef.current) return;
    startingRef.current = true;
    setError(null);
    playingRef.current = false;
    setPlaying(false);
    // Drop any previous stream before requesting a new one.
    streamRef.current?.getTracks().forEach((t) => t.stop());
    streamRef.current = null;
    try {
      // Watchdog: if getUserMedia neither resolves nor rejects (seen as a hung
      // "Starting camera…"), reject after 8s so the overlay becomes an
      // actionable "tap to start" instead of hanging forever.
      const stream = await withTimeout(
        navigator.mediaDevices.getUserMedia({
          video: { facingMode: { ideal: "environment" } },
          audio: false,
        }),
        8000,
      );
      streamRef.current = stream;
      const video = videoRef.current;
      if (video) {
        video.srcObject = stream;
        video.setAttribute("playsinline", "true");
        video.muted = true;
        video.play().catch(() => {});
      }
      const detector = new BarcodeDetector({ formats: ["qr_code"] });
      // Offscreen canvas holding just the centered reticle crop — we only run
      // detection on this region so QR codes elsewhere in view are ignored.
      const canvas = document.createElement("canvas");
      const cctx = canvas.getContext("2d", { willReadFrequently: true });
      let busy = false;
      const tick = async () => {
        rafRef.current = requestAnimationFrame(tick);
        const v = videoRef.current;
        if (!v || v.readyState < 2) return;
        if (!playingRef.current) {
          playingRef.current = true;
          setPlaying(true); // camera is actually up
        }
        if (busy || !activeRef.current) return;
        busy = true;
        try {
          // Crop the central square of the frame (matches the on-screen
          // reticle) and detect only within it.
          const vw = v.videoWidth;
          const vh = v.videoHeight;
          let target: HTMLVideoElement | HTMLCanvasElement = v;
          if (cctx && vw && vh) {
            const side = Math.round(Math.min(vw, vh) * 0.62);
            const sx = Math.round((vw - side) / 2);
            const sy = Math.round((vh - side) / 2);
            canvas.width = side;
            canvas.height = side;
            cctx.drawImage(v, sx, sy, side, side, 0, 0, side, side);
            target = canvas;
          }
          const codes = await detector.detect(target);
          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;
        }
      };
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      rafRef.current = requestAnimationFrame(tick);
    } catch (e: any) {
      setError(
        e?.name === "NotAllowedError" || e?.name === "SecurityError"
          ? "Camera permission was denied. Enable camera access for this site, then tap to try again."
          : "Could not open the camera. Make sure no other app is using it, then tap to try again.",
      );
    } finally {
      startingRef.current = false;
    }
  }

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

  // Reject after `ms` if the promise hasn't settled (un-sticks a hung 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)),
    ]);
  }

  return (
    <View style={styles.fill}>
      {/* Raw DOM video element; react-dom renders it inside the RN-Web tree. */}
      <video
        ref={videoRef as any}
        style={{ width: "100%", height: "100%", objectFit: "cover" }}
        muted
        autoPlay
        playsInline
      />
      {!playing && (
        // Whole overlay is a button so a tap (re)starts the camera in a gesture.
        <Pressable style={styles.overlayMsg} onPress={start}>
          {error ? (
            <>
              <Text style={styles.icon}>📷</Text>
              <Text style={styles.msg}>{error}</Text>
              <View style={styles.btn}>
                <Text style={styles.btnText}>Tap to start camera</Text>
              </View>
            </>
          ) : (
            <>
              <Text style={styles.icon}>📷</Text>
              <Text style={styles.msg}>Starting camera…</Text>
              <Text style={styles.hint}>Tap here if it doesn't start</Text>
            </>
          )}
        </Pressable>
      )}
    </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,
  },
  icon: { fontSize: 44, marginBottom: 12 },
  msg: { color: theme.text, fontSize: 17, textAlign: "center", marginBottom: 12, lineHeight: 24 },
  hint: { color: theme.textDim, fontSize: 14, textAlign: "center" },
  btn: {
    backgroundColor: theme.successBright,
    paddingHorizontal: 24,
    paddingVertical: 14,
    borderRadius: 12,
    marginTop: 6,
  },
  btnText: { color: "#06210f", fontSize: 17, fontWeight: "800" },
});
