// Admin · Labels & Scan
// Print a barcode/QR label for a work order, and scan one back to look it up and
// advance its production status. Barcode (Code 128), QR, and the camera scanner
// all load on demand from CDN — same pattern as the rest of the app.
//
// Scanning paths:
//   • USB / Bluetooth scanner — types the ref into the focused box + Enter (no setup).
//   • Camera — html5-qrcode decodes in JS, so it works on desktop Chrome, iOS
//     Safari, and Android (the old native BarcodeDetector only worked on Android/
//     ChromeOS, which is why desktop showed the "can't scan" message before).
//   • Phone camera on the printed QR — opens the order via its deep link.

const PIPELINE = ["placed", "digitizing", "stitching", "qc", "finished"];
const STAGE_LABEL = {
  placed: "Placed", digitizing: "Digitizing", stitching: "Stitching",
  qc: "QC", finished: "Finished",
};
const QR_READER_ID = "labels-qr-reader";

// Load a script from the first CDN that responds. Tries each URL in order, so a
// single blocked/unreachable CDN (e.g. jsdelivr on some sandboxes) can't break it.
function loadScript(srcs, globalName) {
  if (window[globalName]) return Promise.resolve(window[globalName]);
  const list = Array.isArray(srcs) ? srcs : [srcs];
  const cacheKey = "__load_" + globalName;
  if (window[cacheKey]) return window[cacheKey];
  window[cacheKey] = new Promise((resolve, reject) => {
    let i = 0;
    const tryNext = () => {
      if (window[globalName]) return resolve(window[globalName]);
      if (i >= list.length) {
        delete window[cacheKey]; // allow a later retry to attempt again
        return reject(new Error("Couldn't load " + globalName + " from any CDN — check the connection."));
      }
      const s = document.createElement("script");
      s.src = list[i++]; s.async = true;
      s.onload = () => (window[globalName] ? resolve(window[globalName]) : tryNext());
      s.onerror = () => { try { s.remove(); } catch (e) {} tryNext(); };
      document.head.appendChild(s);
    };
    tryNext();
  });
  return window[cacheKey];
}
const loadBarcode = () => loadScript([
  "https://unpkg.com/jsbarcode@3.11.6/dist/JsBarcode.all.min.js",
  "https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js",
], "JsBarcode");
const loadQR = () => loadScript([
  "https://unpkg.com/qrcode-generator@1.4.4/qrcode.js",
  "https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.js",
], "qrcode");
const loadScanner = () => loadScript([
  "https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js",
  "https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js",
], "Html5Qrcode");

function normalizeRef(s) {
  return String(s || "").trim().toUpperCase();
}
function scanParamFromLocation() {
  try {
    const fromSearch = new URLSearchParams(location.search).get("scan");
    if (fromSearch) return fromSearch;
    const h = location.hash || "";
    const qi = h.indexOf("?");
    if (qi >= 0) return new URLSearchParams(h.slice(qi + 1)).get("scan");
  } catch (e) {}
  return null;
}
function deepLinkFor(ref) {
  return location.origin + location.pathname + "#/admin?scan=" + encodeURIComponent(ref);
}
// Pull the ref out of either a raw barcode value or a scanned QR deep link.
function refFromScanText(text) {
  let val = String(text || "");
  const m = val.match(/scan=([^&]+)/);
  if (m) { try { val = decodeURIComponent(m[1]); } catch (e) { val = m[1]; } }
  return val;
}

const LABEL_SIZES = {
  "4x2": { w: "4in", h: "2in", barW: 1.6, barH: 38, qr: 86, px: { w: 384, h: 192 } },
  "4x6": { w: "4in", h: "6in", barW: 2.2, barH: 70, qr: 150, px: { w: 384, h: 576 } },
  "3x1": { w: "3in", h: "1in", barW: 1.1, barH: 22, qr: 56, px: { w: 288, h: 96 } },
};

// Build the full label document (barcode + QR + text). Shared by print and
// preview so what you see is exactly what prints.
async function buildLabel(order, sizeKey) {
  const sz = LABEL_SIZES[sizeKey] || LABEL_SIZES["4x2"];
  const [JsBarcode, qrcodeLib] = await Promise.all([loadBarcode(), loadQR()]);

  const canvas = document.createElement("canvas");
  JsBarcode(canvas, order.ref, { format: "CODE128", displayValue: false, width: sz.barW, height: sz.barH, margin: 0 });
  const barcodeUrl = canvas.toDataURL("image/png");
  const qr = qrcodeLib(0, "M");        // type 0 = auto-size, M = medium error correction
  qr.addData(deepLinkFor(order.ref));
  qr.make();
  const qrUrl = qr.createDataURL(6, 0); // cellSize 6px, no extra margin

  const esc = (s) => String(s == null ? "" : s).replace(/[&<>]/g, (m) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[m]));
  const due = order.deadline ? "Due " + esc(order.deadline) : "";
  const line2 = [order.itemType, order.qty ? "Qty " + order.qty : "", order.size].filter(Boolean).map(esc).join(" · ");

  const html =
    "<!doctype html><html><head><meta charset='utf-8'><title>" + esc(order.ref) + "</title><style>" +
    "@page{size:" + sz.w + " " + sz.h + ";margin:0}" +
    "*{box-sizing:border-box;-webkit-print-color-adjust:exact;print-color-adjust:exact}" +
    "body{margin:0;font-family:Arial,Helvetica,sans-serif;color:#111}" +
    ".label{width:" + sz.w + ";height:" + sz.h + ";padding:6px 8px;display:flex;flex-direction:column;justify-content:space-between}" +
    ".top{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}" +
    ".ref{font-weight:700;font-size:" + (sizeKey === "3x1" ? "13pt" : "16pt") + ";letter-spacing:.5px}" +
    ".name{font-size:10pt}.meta{font-size:8.5pt;color:#333}" +
    ".barcode{width:100%;height:auto;display:block}.qr{width:" + sz.qr + "px;height:" + sz.qr + "px}" +
    ".status{font-size:8pt;text-transform:uppercase;letter-spacing:.5px;color:#555}" +
    "</style></head><body><div class='label'>" +
    "<div class='top'><div><div class='ref'>" + esc(order.ref) + "</div>" +
    "<div class='name'>" + esc(order.name) + "</div>" +
    (line2 ? "<div class='meta'>" + line2 + "</div>" : "") +
    (due ? "<div class='meta'>" + due + "</div>" : "") +
    "</div><img class='qr' src='" + qrUrl + "'></div>" +
    "<div><img class='barcode' src='" + barcodeUrl + "'>" +
    "<div class='status'>" + esc(STAGE_LABEL[order.status] || order.status || "") + "</div></div>" +
    "</div></body></html>";

  return { html, sz };
}

async function printLabel(order, sizeKey) {
  const { html } = await buildLabel(order, sizeKey);

  // Print from a hidden same-page iframe rather than a pop-up window. Pop-ups get
  // blocked (especially when libraries load async after the click), and a blocked
  // window.open returns null and fails silently — the iframe can't be blocked.
  const old = document.getElementById("label-print-frame");
  if (old) old.remove();
  const frame = document.createElement("iframe");
  frame.id = "label-print-frame";
  frame.setAttribute("aria-hidden", "true");
  frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;visibility:hidden";
  document.body.appendChild(frame);

  const doc = frame.contentWindow.document;
  doc.open(); doc.write(html); doc.close();

  // Wait for the label's images to decode before printing, otherwise some
  // browsers print a blank sheet. Fall back to a short timeout if needed.
  const fire = () => {
    try { frame.contentWindow.focus(); frame.contentWindow.print(); }
    catch (e) { /* surfaced by caller */ }
    setTimeout(() => frame.remove(), 1000);
  };
  const imgs = Array.from(doc.images || []);
  if (imgs.length === 0) { setTimeout(fire, 50); return; }
  let pending = imgs.length;
  const done = () => { if (--pending <= 0) fire(); };
  imgs.forEach((img) => {
    if (img.complete) { done(); }
    else { img.addEventListener("load", done); img.addEventListener("error", done); }
  });
  setTimeout(() => { if (pending > 0) { pending = 0; fire(); } }, 4000); // safety net
}

function LabelsAndScan() {
  const app = useApp();
  const live = app.mode === "api";

  const [term, setTerm] = useState("");
  const [order, setOrder] = useState(null);
  const [error, setError] = useState("");
  const [note, setNote] = useState("");
  const [busy, setBusy] = useState(false);
  const [size, setSize] = useState("4x2");
  const [recent, setRecent] = useState([]);
  const [scanning, setScanning] = useState(false);
  const [preview, setPreview] = useState(null); // { html, px } when showing a label preview
  const inputRef = useRef(null);
  const qrInstanceRef = useRef(null);

  const focusInput = () => { if (inputRef.current) inputRef.current.focus(); };

  const showPreview = async () => {
    if (!order) return;
    setBusy(true); setError(""); setNote("");
    try {
      const { html, sz } = await buildLabel(order, size);
      setPreview({ html, px: sz.px });
    } catch (e) {
      setError("Couldn't build the preview: " + ((e && e.message) || e));
    } finally {
      setBusy(false);
    }
  };

  const lookup = async (rawRef) => {
    const ref = normalizeRef(rawRef);
    if (!ref) return;
    setBusy(true); setError(""); setNote(""); setPreview(null);
    try {
      const o = await api.uploadLookup(ref);
      setOrder(o);
      setTerm("");
      setRecent((r) => [ref, ...r.filter((x) => x !== ref)].slice(0, 8));
    } catch (e) {
      setOrder(null);
      setError(e.message === "Not found." || /404/.test(e.message) ? "No order found for " + ref + "." : e.message);
    } finally {
      setBusy(false);
      focusInput();
    }
  };

  useEffect(() => {
    if (!live) return;
    focusInput();
    const fromLink = scanParamFromLocation();
    if (fromLink) lookup(fromLink);
  }, []);

  // Camera scanning via html5-qrcode. The effect runs when `scanning` flips on,
  // by which point the reader <div> is in the DOM. Cleanup stops the camera.
  useEffect(() => {
    if (!scanning) return;
    if (!window.isSecureContext) {
      setScanning(false);
      setError("Camera scanning needs a secure (https) page. Use a USB/Bluetooth scanner or type the ref here.");
      return;
    }
    let cancelled = false;
    loadScanner().then(() => {
      if (cancelled) return;
      const F = window.Html5QrcodeSupportedFormats;
      const inst = new window.Html5Qrcode(QR_READER_ID, {
        formatsToSupport: F ? [F.QR_CODE, F.CODE_128] : undefined,
      });
      qrInstanceRef.current = inst;
      inst.start(
        { facingMode: "environment" },
        { fps: 10, qrbox: { width: 280, height: 180 } },
        (decodedText) => { setScanning(false); lookup(refFromScanText(decodedText)); },
        () => {} // ignore per-frame "not found" noise
      ).catch((e) => {
        if (cancelled) return;
        setScanning(false);
        const msg = e && (e.message || e);
        setError(
          /permission|denied|NotAllowed/i.test(String(msg))
            ? "Camera permission was blocked. Allow it in the browser, or use a scanner / type the ref."
            : "Couldn't start the camera. Use a USB/Bluetooth scanner or type the ref. (" + msg + ")"
        );
      });
    }).catch((e) => { if (!cancelled) { setScanning(false); setError(e.message); } });

    return () => {
      cancelled = true;
      const inst = qrInstanceRef.current;
      qrInstanceRef.current = null;
      if (inst) { try { inst.stop().then(() => inst.clear()).catch(() => {}); } catch (e) {} }
    };
  }, [scanning]);

  const setStatus = async (next) => {
    if (!order) return;
    setBusy(true); setError(""); setNote("");
    try {
      await api.req("/api/uploads/" + order.id, { method: "PATCH", body: JSON.stringify({ status: next }) });
      setOrder({ ...order, status: next });
      setNote("Moved to " + (STAGE_LABEL[next] || next) + ".");
    } catch (e) {
      setError(e.message);
    } finally {
      setBusy(false);
      focusInput();
    }
  };

  if (!live) {
    return (
      <div className="stitched" style={{ padding: 18 }}>
        <h2 style={{ marginTop: 0 }}>Labels &amp; Scan</h2>
        <p style={{ color: "var(--ink-3)" }}>Order lookup and status updates run against the live backend, so this works on the deployed site rather than the local preview.</p>
      </div>
    );
  }

  const idx = order ? PIPELINE.indexOf(order.status) : -1;
  const next = idx >= 0 && idx < PIPELINE.length - 1 ? PIPELINE[idx + 1] : null;

  return (
    <div>
      <div style={{ marginBottom: 16 }}>
        <h2 style={{ margin: 0 }}>Labels &amp; Scan</h2>
        <div style={{ color: "var(--ink-3)", fontSize: 14, marginTop: 4 }}>
          Scan a box or work-order label to pull up the order and move it along, or print a fresh label.
        </div>
      </div>

      <div className="stitched" style={{ padding: 16, marginBottom: 14 }}>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap", alignItems: "flex-end" }}>
          <div className="field" style={{ margin: 0, flex: 1, minWidth: 220 }}>
            <label>Scan or type order ref</label>
            <input ref={inputRef} value={term} placeholder="HB-260623-9F3A"
              onChange={(e) => setTerm(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); lookup(term); } }}
              style={{ fontFamily: "monospace", fontSize: 16 }} autoFocus />
          </div>
          <button className="btn" disabled={busy || !term.trim()} onClick={() => lookup(term)}>Look up</button>
          <button className="btn btn-ghost" disabled={busy} onClick={() => setScanning((s) => !s)}>
            {scanning ? "Stop camera" : "Scan with camera"}
          </button>
        </div>
        {scanning && (
          <div style={{ marginTop: 12 }}>
            <div id={QR_READER_ID} style={{ width: "100%", maxWidth: 360 }} />
            <div style={{ color: "var(--ink-3)", fontSize: 12, marginTop: 6 }}>
              Point the camera at the QR or barcode. On a desktop webcam the QR reads most reliably.
            </div>
          </div>
        )}
        {recent.length > 0 && (
          <div style={{ marginTop: 10, fontSize: 12, color: "var(--ink-3)" }}>
            Recent: {recent.map((r) => (
              <button key={r} className="btn btn-ghost btn-small" style={{ marginRight: 6, fontFamily: "monospace" }}
                onClick={() => lookup(r)}>{r}</button>
            ))}
          </div>
        )}
      </div>

      {error && <div style={{ color: "var(--rose-deep)", marginBottom: 12 }}>{error}</div>}
      {note &&  <div style={{ color: "var(--sage-deep)", marginBottom: 12 }}>{note}</div>}

      {order && (
        <div className="stitched stitched-ink" style={{ padding: 18 }}>
          <div style={{ display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
            <div>
              <div style={{ fontFamily: "monospace", fontSize: 18, fontWeight: 700 }}>{order.ref}</div>
              <div style={{ fontSize: 15 }}>{order.name}</div>
              <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 2 }}>
                {[order.itemType, order.qty ? "Qty " + order.qty : "", order.size, order.placement].filter(Boolean).join(" · ")}
              </div>
              {order.deadline && <div style={{ color: "var(--ink-3)", fontSize: 13 }}>Due {order.deadline}</div>}
              {order.notes && <div style={{ fontSize: 13, marginTop: 8, maxWidth: 520 }}>{order.notes}</div>}
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "flex-end" }}>
              <select value={size} onChange={(e) => { setSize(e.target.value); setPreview(null); }} style={{ width: 150 }}>
                <option value="4x2">Label 4×2 in</option>
                <option value="4x6">Label 4×6 in</option>
                <option value="3x1">Label 3×1 in</option>
              </select>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="btn btn-ghost" disabled={busy} onClick={showPreview}>
                  {preview ? "Refresh preview" : "Preview label"}
                </button>
                <button className="btn" onClick={() => printLabel(order, size).catch((e) => setError("Couldn't prepare the label: " + ((e && e.message) || e)))}>Print label</button>
              </div>
            </div>
          </div>

          {preview && (
            <div style={{ marginTop: 16, display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
              <iframe title="Label preview" srcDoc={preview.html} scrolling="no"
                style={{ width: preview.px.w + "px", height: preview.px.h + "px", border: "1px solid var(--line)",
                         borderRadius: 6, background: "#fff", boxShadow: "var(--shadow)" }} />
              <div style={{ color: "var(--ink-3)", fontSize: 12 }}>
                Actual size ({size.replace("x", "×")} in) — this is exactly what prints.
              </div>
            </div>
          )}

          <div style={{ display: "flex", gap: 6, marginTop: 18, flexWrap: "wrap" }}>
            {PIPELINE.map((st, i) => {
              const isCurrent = i === idx, done = idx >= 0 && i < idx;
              return (
                <button key={st} className="btn btn-small"
                  onClick={() => st !== order.status && setStatus(st)} disabled={busy}
                  style={{
                    opacity: done ? 0.65 : 1,
                    background: isCurrent ? "var(--sage-deep)" : (done ? "var(--sage)" : "transparent"),
                    color: isCurrent || done ? "#fff" : "var(--ink)",
                    border: "1px solid var(--line)",
                  }}>
                  {STAGE_LABEL[st]}
                </button>
              );
            })}
          </div>
          {next && (
            <button className="btn" style={{ marginTop: 12 }} disabled={busy} onClick={() => setStatus(next)}>
              Move forward → {STAGE_LABEL[next]}
            </button>
          )}
          {!next && idx >= 0 && (
            <div style={{ marginTop: 12, color: "var(--sage-deep)", fontSize: 14 }}>This order is finished.</div>
          )}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { LabelsAndScan });
