"use client";

import { useEffect, useRef, useState } from "react";
import { Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats } from "html5-qrcode";

type ResultState = "valid" | "used" | "void" | "not-found" | "error";

// What /api/checkin returns about a ticket it found.
type TicketInfo = {
  attendeeName: string | null;
  buyerName: string | null;
  buyerPhone: string | null;
  buyerEmail: string | null;
  ticketType: string | null;
  eventTitle: string | null;
  eventStartsAt: string | null;
  code: string;
  orderRef: string;
  paymentMethod: string | null;
  purchasedAt: string | null;
  orderTickets: number;
  orderCheckedIn: number;
  checkedInAt: string | null;
  gate: string | null;
};

type Result = {
  state: ResultState;
  title: string;
  message?: string;
  ticket?: TicketInfo;
  scanned?: string;
};

const GATES = ["Main Gate", "VIP Gate", "Vendors"];

// A successful check-in clears itself after this long so the queue keeps
// moving; anything else stays on screen until staff tap "Scan next".
const AUTO_NEXT_MS = 6000;
// The camera keeps seeing the same QR while it's still in front of it:
// ignore a repeat of the code just handled for this long (it used to flip a
// green "Valid" into "Already used" two seconds later).
const SAME_CODE_IGNORE_MS = 30_000;

type SearchResult = {
  code: string;
  status: "valid" | "used" | "void";
  attendeeName: string | null;
  checkedInAt: string | null;
  checkedInGate: string | null;
  ticketType: string;
  eventTitle: string;
  buyerName: string | null;
  buyerPhone: string | null;
};

const PAYMENT_LABEL: Record<string, string> = { mpesa: "M-Pesa", card: "Card", paypal: "PayPal", comp: "Complimentary" };

const timeFmt = new Intl.DateTimeFormat("en-KE", {
  day: "numeric",
  month: "short",
  hour: "2-digit",
  minute: "2-digit",
  timeZone: "Africa/Nairobi",
});
// Module-level so the clock read lives outside the component body.
function clockMs() {
  return Date.now();
}
const fmt = (iso: string | null) => (iso ? timeFmt.format(new Date(iso)) : "—");
const dateFmt = new Intl.DateTimeFormat("en-KE", { day: "numeric", month: "short", year: "numeric", timeZone: "Africa/Nairobi" });
const fmtDate = (iso: string | null) => (iso ? dateFmt.format(new Date(iso)) : "—");

export default function CheckinPage() {
  const scannerRef = useRef<Html5Qrcode | null>(null);
  const [scanning, setScanning] = useState(false);
  const [gate, setGate] = useState(GATES[0]);
  const gateRef = useRef(GATES[0]);
  const [result, setResult] = useState<Result | null>(null);
  const [busy, setBusy] = useState(false);
  const busyRef = useRef(false);
  const pausedRef = useRef(false);
  const [cameraError, setCameraError] = useState<string | null>(null);
  const [manualCode, setManualCode] = useState("");
  const lastRef = useRef<{ payload: string; at: number } | null>(null);
  const autoNextRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const resultRef = useRef<HTMLDivElement | null>(null);
  const [query, setQuery] = useState("");
  const [searching, setSearching] = useState(false);
  const [results, setResults] = useState<SearchResult[] | null>(null);

  useEffect(() => {
    return () => {
      if (autoNextRef.current) clearTimeout(autoNextRef.current);
      scannerRef.current?.stop().catch(() => {});
    };
  }, []);

  function pauseCamera() {
    const s = scannerRef.current;
    if (s && s.getState() === Html5QrcodeScannerState.SCANNING) {
      try {
        s.pause(true);
        pausedRef.current = true;
      } catch {
        // not scanning -- nothing to pause
      }
    }
  }

  function scanNext() {
    if (autoNextRef.current) clearTimeout(autoNextRef.current);
    autoNextRef.current = null;
    setResult(null);
    const s = scannerRef.current;
    if (s && s.getState() === Html5QrcodeScannerState.PAUSED) {
      try {
        s.resume();
      } catch {
        // already running
      }
    }
    pausedRef.current = false;
  }

  async function handleScan(payload: string, opts: { manual?: boolean } = {}) {
    // Refs, not state: the camera callback is registered once and would
    // otherwise keep seeing stale values.
    // While a result is on screen the camera is paused; only typed/search
    // check-ins (manual) go through.
    if (busyRef.current || (!opts.manual && pausedRef.current)) return;
    const last = lastRef.current;
    if (!opts.manual && last && last.payload === payload && clockMs() - last.at < SAME_CODE_IGNORE_MS) return;
    lastRef.current = { payload, at: clockMs() };
    busyRef.current = true;
    setBusy(true);
    pauseCamera();
    if (autoNextRef.current) clearTimeout(autoNextRef.current);

    let next: Result;
    try {
      const res = await fetch("/api/checkin", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ payload, gate: gateRef.current }),
      });
      const data = await res.json().catch(() => ({}));
      const ticket = data.ticket as TicketInfo | undefined;

      if (res.ok) {
        next = { state: "valid", title: "VALID — let them in", ticket };
      } else if (res.status === 401) {
        next = { state: "error", title: "Signed out", message: "Sign in again to keep scanning." };
      } else if (res.status === 409 && data.error === "Already used") {
        next = {
          state: "used",
          title: "ALREADY USED",
          message: `This ticket was checked in at ${fmt(ticket?.checkedInAt ?? null)}${ticket?.gate ? ` (${ticket.gate})` : ""}.`,
          ticket,
        };
      } else if (res.status === 409) {
        next = { state: "void", title: "CANCELLED TICKET", message: data.error, ticket };
      } else {
        next = { state: "not-found", title: "NOT ACCEPTED", message: data.error || "Not recognised", scanned: payload };
      }
    } catch {
      // Network drop at the gate: say so, and never leave the scanner stuck.
      next = { state: "error", title: "No connection", message: "Check the network and scan again." };
      lastRef.current = null;
    } finally {
      busyRef.current = false;
      setBusy(false);
    }

    setResult(next);
    // Bring the card into view (it sits right under the camera).
    requestAnimationFrame(() => resultRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }));
    if (next.state === "valid") autoNextRef.current = setTimeout(scanNext, AUTO_NEXT_MS);
  }

  async function startScanning() {
    setCameraError(null);
    setResult(null);
    pausedRef.current = false;
    // QR only (skips trying a dozen barcode formats on every frame), and the
    // browser's native BarcodeDetector where available (Android Chrome) --
    // far better at reading QR codes off a bright phone screen.
    const scanner = new Html5Qrcode("qr-reader", {
      verbose: false,
      formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE],
      useBarCodeDetectorIfSupported: true,
    });
    scannerRef.current = scanner;

    // A fixed pixel qrbox is measured against the rendered viewfinder, so on
    // a narrow phone it can end up larger than the preview and nothing
    // decodes. Size it off the actual viewfinder instead.
    const config = {
      fps: 10,
      qrbox: (viewfinderWidth: number, viewfinderHeight: number) => {
        const edge = Math.floor(Math.min(viewfinderWidth, viewfinderHeight) * 0.7);
        return { width: edge, height: edge };
      },
    };
    const onDecoded = (decodedText: string) => handleScan(decodedText);
    const onDecodeError = () => {};

    try {
      // Prefer the rear camera; fall back to whatever camera exists.
      await scanner.start({ facingMode: "environment" }, config, onDecoded, onDecodeError);
    } catch {
      try {
        const cameras = await Html5Qrcode.getCameras();
        if (!cameras.length) throw new Error("No camera found on this device.");
        await scanner.start(cameras[0].id, config, onDecoded, onDecodeError);
      } catch (err) {
        scannerRef.current = null;
        setCameraError(
          err instanceof Error && /permission|NotAllowed/i.test(err.message)
            ? "Camera access was denied. Allow camera access for this site in your browser settings, then try again."
            : err instanceof Error
              ? err.message
              : "Could not start the camera. Make sure no other app is using it and try again."
        );
        return;
      }
    }

    setScanning(true);
  }

  async function stopScanning() {
    if (autoNextRef.current) clearTimeout(autoNextRef.current);
    try {
      await scannerRef.current?.stop();
    } catch {
      // Already stopped or never started — nothing to clean up.
    }
    scannerRef.current = null;
    pausedRef.current = false;
    setScanning(false);
  }

  async function search(e: React.FormEvent) {
    e.preventDefault();
    const q = query.trim();
    if (q.length < 3) return;
    setSearching(true);
    const res = await fetch(`/api/checkin/search?q=${encodeURIComponent(q)}`).catch(() => null);
    const data = res ? await res.json().catch(() => ({})) : {};
    setSearching(false);
    setResults(res?.ok ? (data.results as SearchResult[]) : []);
  }

  async function checkInFromSearch(code: string) {
    await handleScan(code, { manual: true });
    if (query.trim().length >= 3) {
      const res = await fetch(`/api/checkin/search?q=${encodeURIComponent(query.trim())}`).catch(() => null);
      if (res?.ok) setResults(((await res.json()) as { results: SearchResult[] }).results);
    }
  }

  async function submitManual(e: React.FormEvent) {
    e.preventDefault();
    const code = manualCode.trim();
    if (!code) return;
    await handleScan(code, { manual: true });
    setManualCode("");
  }

  const cardStyle: Record<ResultState, string> = {
    valid: "border-green-500 bg-green-600/15",
    used: "border-amber-500 bg-amber-500/15",
    void: "border-red-500 bg-red-600/15",
    "not-found": "border-red-500 bg-red-600/15",
    error: "border-red-500 bg-red-600/15",
  };
  const titleStyle: Record<ResultState, string> = {
    valid: "text-green-500",
    used: "text-amber-500",
    void: "text-red-500",
    "not-found": "text-red-500",
    error: "text-red-500",
  };

  const t = result?.ticket;

  return (
    <div className="max-w-md">
      <h1 className="text-2xl font-semibold mb-4 text-be-text">Gate scanner</h1>

      <div className="flex gap-2 mb-4">
        {GATES.map((g) => (
          <button
            key={g}
            onClick={() => {
              setGate(g);
              gateRef.current = g;
            }}
            disabled={scanning}
            className={`be-act ${gate === g ? "be-act-primary" : "be-act-ghost"}`}
          >
            {g}
          </button>
        ))}
      </div>

      <div id="qr-reader" className="rounded-lg overflow-hidden border border-be-border" />

      {/* The result sits right under the camera so it's on screen on a phone. */}
      <div ref={resultRef}>
        {busy && !result && <p className="mt-3 text-center text-sm text-be-muted">Checking ticket…</p>}
        {result && (
          <div className={`mt-3 rounded-xl border-2 p-4 ${cardStyle[result.state]}`}>
            <p className={`text-2xl font-black tracking-tight ${titleStyle[result.state]}`}>{result.title}</p>
            {result.message && <p className="mt-1 text-sm text-be-text">{result.message}</p>}

            {t && (
              <dl className="mt-3 grid grid-cols-[7rem_1fr] gap-x-3 gap-y-1.5 text-sm">
                <dt className="text-be-muted">Name</dt>
                <dd className="font-semibold text-be-text">{t.attendeeName || t.buyerName || "Guest"}</dd>
                <dt className="text-be-muted">Ticket</dt>
                <dd className="font-semibold text-be-text">{t.ticketType ?? "—"}</dd>
                <dt className="text-be-muted">Event</dt>
                <dd className="text-be-text">{t.eventTitle ?? "—"}</dd>
                {t.orderTickets > 1 && (
                  <>
                    <dt className="text-be-muted">Group</dt>
                    <dd className="text-be-text">
                      {t.orderCheckedIn} of {t.orderTickets} tickets on this order are in
                    </dd>
                  </>
                )}
                <dt className="text-be-muted">Phone</dt>
                <dd className="text-be-text">{t.buyerPhone || "—"}</dd>
                <dt className="text-be-muted">Bought</dt>
                <dd className="text-be-text">
                  {fmtDate(t.purchasedAt)} · {PAYMENT_LABEL[t.paymentMethod ?? ""] ?? t.paymentMethod ?? "—"}
                </dd>
                <dt className="text-be-muted">Order</dt>
                <dd className="font-mono text-xs text-be-text">#{t.orderRef}</dd>
                {result.state !== "valid" && t.checkedInAt && (
                  <>
                    <dt className="text-be-muted">Checked in</dt>
                    <dd className="text-be-text">
                      {fmt(t.checkedInAt)}
                      {t.gate ? ` · ${t.gate}` : ""}
                    </dd>
                  </>
                )}
                <dt className="text-be-muted">Code</dt>
                <dd className="break-all font-mono text-xs text-be-muted">{t.code}</dd>
              </dl>
            )}

            {result.scanned && (
              <p className="mt-2 break-all font-mono text-xs text-be-muted">Read: {result.scanned.slice(0, 120)}</p>
            )}

            {scanning && (
              <button
                type="button"
                onClick={scanNext}
                className="mt-4 w-full rounded-lg bg-brand-green py-3 text-base font-semibold text-white hover:bg-brand-green-deep"
              >
                Scan next ticket
              </button>
            )}
            {result.state === "valid" && scanning && (
              <p className="mt-1 text-center text-xs text-be-muted">Continues automatically in a few seconds.</p>
            )}
          </div>
        )}
      </div>

      <button
        onClick={scanning ? stopScanning : startScanning}
        className="mt-4 w-full rounded-lg bg-brand-green py-2.5 text-sm font-medium text-white transition-colors hover:bg-brand-green-deep"
      >
        {scanning ? "Stop scanning" : "Start scanning"}
      </button>

      {cameraError && (
        <div className="mt-4 rounded-lg p-4 text-sm pill-danger">
          <p className="font-medium">Couldn&apos;t start the camera</p>
          <p className="text-xs mt-1 opacity-80">{cameraError}</p>
        </div>
      )}

      {/* Fallback for a damaged/dim screen or no camera: the code is printed under every QR. */}
      <form onSubmit={submitManual} className="mt-4 flex gap-2">
        <input
          value={manualCode}
          onChange={(e) => setManualCode(e.target.value)}
          placeholder="Or type the code under the QR"
          autoCapitalize="none"
          autoCorrect="off"
          spellCheck={false}
          className="input flex-1 font-mono text-sm"
        />
        <button type="submit" disabled={busy || !manualCode.trim()} className="be-act be-act-primary">
          Check in
        </button>
      </form>

      {/* Can't scan at all? Find the buyer and check their ticket in by hand. */}
      <form onSubmit={search} className="mt-4 flex gap-2">
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Find by name, phone, email or M-Pesa code"
          className="input flex-1 text-sm"
        />
        <button type="submit" disabled={searching || query.trim().length < 3} className="be-act be-act-ghost">
          {searching ? "…" : "Find"}
        </button>
      </form>
      {results && (
        <div className="mt-2 space-y-2">
          {results.length === 0 && <p className="text-sm text-be-muted">No paid tickets match that.</p>}
          {results.map((r) => (
            <div key={r.code} className="flex items-center justify-between gap-3 rounded-lg border border-be-border p-3 text-sm">
              <div className="min-w-0">
                <p className="font-medium text-be-text">{r.attendeeName || r.buyerName || "Guest"}</p>
                <p className="truncate text-xs text-be-muted">
                  {r.ticketType} · {r.eventTitle}
                  {r.buyerPhone ? ` · ${r.buyerPhone}` : ""}
                </p>
                <p className="font-mono text-[11px] text-be-muted">{r.code}</p>
              </div>
              {r.status === "valid" ? (
                <button
                  type="button"
                  disabled={busy}
                  onClick={() => checkInFromSearch(r.code)}
                  className="be-act be-act-primary shrink-0"
                >
                  Check in
                </button>
              ) : (
                <span className={`shrink-0 rounded-full px-2 py-0.5 text-xs ${r.status === "used" ? "pill-warn" : "pill-muted"}`}>
                  {r.status === "used" ? `Used${r.checkedInGate ? ` · ${r.checkedInGate}` : ""}` : "Cancelled"}
                </span>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
