import { useCallback, useEffect, useRef, useState } from 'react';
import jsQR from 'jsqr';
import { Camera, Download, RotateCcw } from 'lucide-react';
import { crc32, decodeFrame, reassemble, type MetaFrame } from './protocol';

// Receiver: camera → jsQR → collect frames until every index is present.
//
// Frames arrive in whatever order the camera happens to catch them, and duplicates are constant — the
// sender loops forever. So the state is a Map keyed by sequence number, and progress is its size against
// the total the meta frame declared.
//
// The camera is only available on a secure origin. Over the tailnet with HTTPS that is fine; on plain
// http://<ip> the browser refuses and there is nothing this code can do about it, so it says so.

type Progress = { meta: MetaFrame; have: number };

export const QrReceiver = () => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const chunksRef = useRef(new Map<number, Uint8Array>());
  const metaRef = useRef<MetaFrame | null>(null);
  const rafRef = useRef<number>(0);

  const [scanning, setScanning] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [progress, setProgress] = useState<Progress | null>(null);
  const [done, setDone] = useState<{ url: string; name: string; ok: boolean } | null>(null);

  const reset = useCallback(() => {
    chunksRef.current = new Map();
    metaRef.current = null;
    setProgress(null);
    setDone(null);
  }, []);

  const stop = useCallback(() => {
    cancelAnimationFrame(rafRef.current);
    const stream = videoRef.current?.srcObject as MediaStream | null;
    stream?.getTracks().forEach((t) => t.stop());
    if (videoRef.current) videoRef.current.srcObject = null;
    setScanning(false);
  }, []);

  const handleFrame = useCallback(
    (text: string) => {
      const frame = decodeFrame(text);
      if (!frame) return;

      if (frame.kind === 'meta') {
        // A different session means the sender switched files — start over rather than mixing two.
        if (metaRef.current && metaRef.current.sid !== frame.sid) {
          chunksRef.current = new Map();
        }
        metaRef.current = frame;
        setProgress({ meta: frame, have: chunksRef.current.size });
        return;
      }

      const meta = metaRef.current;
      // Data before meta is unusable — we would not know how many frames to expect. It comes round again.
      if (!meta || frame.sid !== meta.sid) return;

      if (!chunksRef.current.has(frame.seq)) {
        chunksRef.current.set(frame.seq, frame.bytes);
        setProgress({ meta, have: chunksRef.current.size });

        const rebuilt = reassemble(chunksRef.current, meta.total);
        if (rebuilt) {
          // The end-to-end check: every frame passed its own decode, but only this proves the file.
          const ok = crc32(rebuilt) === meta.crc32;
          const blob = new Blob([rebuilt as BlobPart], { type: meta.mime });
          setDone({ url: URL.createObjectURL(blob), name: meta.name, ok });
          stop();
        }
      }
    },
    [stop],
  );

  const tick = useCallback(() => {
    const video = videoRef.current;
    const canvas = canvasRef.current;
    if (video && canvas && video.readyState === video.HAVE_ENOUGH_DATA) {
      const ctx = canvas.getContext('2d', { willReadFrequently: true });
      if (ctx) {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        const image = ctx.getImageData(0, 0, canvas.width, canvas.height);
        // dontInvert: our codes are always dark-on-light, and trying both doubles the work per frame.
        const found = jsQR(image.data, image.width, image.height, { inversionAttempts: 'dontInvert' });
        if (found?.data) handleFrame(found.data);
      }
    }
    rafRef.current = requestAnimationFrame(tick);
  }, [handleFrame]);

  const start = useCallback(async () => {
    setError(null);
    reset();
    if (!navigator.mediaDevices?.getUserMedia) {
      setError('Camera needs a secure origin (https). Plain http will not work.');
      return;
    }
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } },
      });
      if (videoRef.current) {
        videoRef.current.srcObject = stream;
        await videoRef.current.play();
      }
      setScanning(true);
      rafRef.current = requestAnimationFrame(tick);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not open the camera');
    }
  }, [reset, tick]);

  useEffect(() => stop, [stop]);

  const pct = progress ? Math.round((progress.have / progress.meta.total) * 100) : 0;

  return (
    <div className="flex h-full w-full flex-col items-center gap-3 overflow-y-auto p-3">
      {!scanning && !done && (
        <button
          type="button"
          onClick={start}
          className="flex cursor-pointer items-center gap-2 rounded-md border border-duck-dark/20 px-3 py-2 text-sm text-duck-dark/80 hover:border-duck-teal hover:text-duck-teal"
        >
          <Camera className="h-4 w-4" />
          Start scanning
        </button>
      )}

      {error && <p className="text-center text-xs text-red-500">{error}</p>}

      <video ref={videoRef} playsInline muted className={scanning ? 'max-w-full rounded' : 'hidden'} />
      <canvas ref={canvasRef} className="hidden" />

      {progress && !done && (
        <div className="w-full text-xs text-duck-dark/70">
          <div className="mb-1 flex justify-between">
            <span className="truncate">{progress.meta.name}</span>
            <span>
              {progress.have} / {progress.meta.total} ({pct}%)
            </span>
          </div>
          <div className="h-1.5 w-full overflow-hidden rounded bg-duck-dark/10">
            <div className="h-full bg-duck-teal transition-all" style={{ width: `${pct}%` }} />
          </div>
        </div>
      )}

      {done && (
        <div className="flex flex-col items-center gap-2 text-sm">
          <p className={done.ok ? 'text-emerald-600' : 'text-red-500'}>
            {done.ok ? 'Complete — checksum matches' : 'Complete, but the checksum does NOT match'}
          </p>
          <a
            href={done.url}
            download={done.name}
            className="flex items-center gap-2 rounded-md bg-duck-teal/15 px-3 py-2 text-duck-teal hover:bg-duck-teal/25"
          >
            <Download className="h-4 w-4" />
            {done.name}
          </a>
          <button
            type="button"
            onClick={reset}
            className="flex cursor-pointer items-center gap-1 text-xs text-duck-dark/50 hover:text-duck-teal"
          >
            <RotateCcw className="h-3.5 w-3.5" />
            Receive another
          </button>
        </div>
      )}

      {scanning && (
        <button
          type="button"
          onClick={stop}
          className="cursor-pointer text-xs text-duck-dark/50 hover:text-red-500"
        >
          Stop
        </button>
      )}
    </div>
  );
};
