/* app.jsx — router, state, tweaks. */
const { useState: uS, useEffect: uE } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "inputMode": "guided",
  "accent": "#a78bfa",
  "density": "regular"
}/*EDITMODE-END*/;

function hexToRgba(hex, a) {
  const h = hex.replace("#", "");
  const r = parseInt(h.substring(0, 2), 16), g = parseInt(h.substring(2, 4), 16), b = parseInt(h.substring(4, 6), 16);
  return `rgba(${r},${g},${b},${a})`;
}

/* Render the printable DocPaper off-screen, snapshot it (Thai-correct, exactly as
   on screen) and return a base64 PDF. Returns null on any failure (caller then
   sends the email without an attachment). */
async function makeDocPdf(doc, company, lang) {
  if (!window.html2canvas || !window.jspdf || typeof DocPaper === "undefined") return null;
  const holder = document.createElement("div");
  holder.style.cssText = "position:fixed;left:-10000px;top:0;width:600px;background:#fff;padding:0;z-index:-1;";
  document.body.appendChild(holder);
  const root = ReactDOM.createRoot(holder);
  try {
    await new Promise((res) => { root.render(<DocPaper doc={doc} company={company} lang={lang} approverSigned={true} />); requestAnimationFrame(() => setTimeout(res, 120)); });
    if (document.fonts && document.fonts.ready) { try { await document.fonts.ready; } catch (e) {} }
    await Promise.all(Array.from(holder.querySelectorAll("img")).map((img) =>
      img.complete ? Promise.resolve() : new Promise((r) => { img.onload = img.onerror = () => r(); })));
    const target = holder.querySelector(".paper") || holder;
    // flatten to a clean white sheet for the document (no theme border/shadow)
    target.style.background = "#fff";
    target.style.border = "none";
    target.style.boxShadow = "none";
    target.style.borderRadius = "0";
    const canvas = await window.html2canvas(target, { scale: 2, useCORS: true, backgroundColor: "#ffffff", logging: false });

    const { jsPDF } = window.jspdf;
    const pdf = new jsPDF({ unit: "pt", format: "a4" });
    const A4W = pdf.internal.pageSize.getWidth(), A4H = pdf.internal.pageSize.getHeight();
    const m = 28;                          // ~10mm page margins
    const cw = A4W - m * 2;                 // content width (pt)
    const pageHpx = (A4H - m * 2) * (canvas.width / cw); // max slice height in canvas px

    // candidate break lines = bottoms of rows/blocks, so a page never cuts through one
    const rect = target.getBoundingClientRect();
    const s = canvas.width / rect.width;    // DOM px -> canvas px
    const bps = [];
    target.querySelectorAll(".pp-row, .pp-totals, .pp-cert, .pp-photo-grid figure, .pp-signs").forEach((el) => {
      bps.push((el.getBoundingClientRect().bottom - rect.top) * s);
    });
    bps.push(canvas.height);
    bps.sort((a, b) => a - b);

    let startY = 0, first = true;
    while (startY < canvas.height - 1) {
      let endY = canvas.height;
      if (canvas.height - startY > pageHpx) {
        const limit = startY + pageHpx;
        let chosen = 0;
        for (const b of bps) { if (b > startY + 8 && b <= limit) chosen = b; }
        endY = chosen || limit;           // fall back to a hard cut only if a single block is taller than a page
      }
      const sliceH = Math.round(Math.min(endY, canvas.height) - startY);
      const slice = document.createElement("canvas");
      slice.width = canvas.width; slice.height = sliceH;
      slice.getContext("2d").drawImage(canvas, 0, startY, canvas.width, sliceH, 0, 0, canvas.width, sliceH);
      if (!first) pdf.addPage();
      pdf.addImage(slice.toDataURL("image/jpeg", 0.92), "JPEG", m, m, cw, sliceH / (canvas.width / cw));
      first = false;
      startY = endY;
    }
    // page numbers (e.g. 1/3) bottom-right of every page
    const totalPages = pdf.internal.getNumberOfPages();
    for (let pn = 1; pn <= totalPages; pn++) {
      pdf.setPage(pn);
      pdf.setFontSize(8); pdf.setTextColor(150);
      pdf.text(pn + "/" + totalPages, A4W - m, A4H - 12, { align: "right" });
    }
    return pdf.output("datauristring").split(",")[1]; // base64, no data: prefix
  } catch (e) {
    console.error("pdf gen failed", e);
    return null;
  } finally {
    try { root.unmount(); } catch (e) {}
    holder.remove();
  }
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [data, setData] = uS(null);            // loaded from Supabase after login
  const [loadError, setLoadError] = uS(null);
  const [authChecked, setAuthChecked] = uS(false); // have we checked for an existing session yet?
  const [authError, setAuthError] = uS(null);
  const [session, setSession] = uS(null);
  const [recovering, setRecovering] = uS(false); // arrived via a password-reset link → show "set new password"
  const [lang, setLang] = uS("th");
  const [theme, setTheme] = uS(() => { try { return localStorage.getItem("pettycash-theme") || "system"; } catch (e) { return "system"; } });
  const [route, setRoute] = uS({ name: "login", params: {} });
  const [draft, setDraft] = uS(null);
  const [showLang, setShowLang] = uS(false);
  const [boCompanyId, setBoCompanyId] = uS(null);
  const [locked, setLocked] = uS(false);        // PIN screen shown
  const [pinError, setPinError] = uS(null);
  const [pinBusy, setPinBusy] = uS(false);
  const [showSetPin, setShowSetPin] = uS(false);
  const pwRef = React.useRef("");                // password kept in memory while signed in (to save under a PIN)
  const [pull, setPull] = uS(0);                 // pull-to-refresh drag distance (px)
  const [refreshing, setRefreshing] = uS(false); // reloading data after a pull
  const sessionRef = React.useRef(null), dataRef = React.useRef(null);
  const refreshingRef = React.useRef(false), pullRef = React.useRef(0);

  // on boot: a password-reset link wins (show "set new password"); else if a PIN is
  // set show the PIN screen; otherwise restore any existing session
  uE(() => {
    if (/(?:^|[#&?])type=recovery(?:&|$)/.test(window.location.hash) || /type=recovery/.test(window.location.search)) {
      setRecovering(true); setLocked(false); setAuthChecked(true); return;
    }
    if (window.pinInfo()) { setLocked(true); setAuthChecked(true); return; }
    window.authGetSession().then(({ data: s }) => {
      if (s && s.session) {
        window.__sbToken = s.session.access_token;
        afterAuth().catch((e) => { console.error(e); setLoadError(e.message || String(e)); }).finally(() => setAuthChecked(true));
      } else {
        setAuthChecked(true);
      }
    });
  }, []);

  // the Supabase client also fires PASSWORD_RECOVERY once it parses the link
  uE(() => {
    const { data: sub } = window.sbClient.auth.onAuthStateChange((event) => {
      if (event === "PASSWORD_RECOVERY") { setRecovering(true); setLocked(false); setAuthChecked(true); }
    });
    return () => { try { sub.subscription.unsubscribe(); } catch (e) {} };
  }, []);

  // keep a global pointer for DocPaper lookups
  uE(() => { if (data) window.__data = data; }, [data]);

  // ----- pull-to-refresh: drag down at the top of a scroll area to reload data -----
  uE(() => { sessionRef.current = session; }, [session]);
  uE(() => { dataRef.current = data; }, [data]);
  function setPullVal(v) { pullRef.current = v; setPull(v); }
  // full page reload (like the browser refresh): picks up a new deployed version
  // AND re-runs startup, so the PIN screen reappears. The brief spinner shows,
  // then the page reloads.
  function doRefresh() {
    if (refreshingRef.current) return;
    refreshingRef.current = true; setRefreshing(true);
    try { window.location.reload(); } catch (e) { refreshingRef.current = false; setRefreshing(false); }
  }
  uE(() => {
    const st = { tracking: false, startY: 0, scroller: null, pulling: false };
    function onStart(e) {
      if (!sessionRef.current || !dataRef.current || refreshingRef.current) { st.tracking = false; return; }
      const t = e.touches && e.touches[0];
      if (!t || !e.target.closest || e.target.closest(".sheet")) { st.tracking = false; return; }
      const sc = e.target.closest(".scroll");
      if (!sc || sc.scrollTop > 0) { st.tracking = false; return; }
      st.tracking = true; st.startY = t.clientY; st.scroller = sc; st.pulling = false;
    }
    function onMove(e) {
      if (!st.tracking) return;
      const t = e.touches && e.touches[0]; if (!t) return;
      const dy = t.clientY - st.startY;
      if (dy <= 0 || (st.scroller && st.scroller.scrollTop > 0)) { if (st.pulling) { st.pulling = false; setPullVal(0); } return; }
      st.pulling = true;
      if (e.cancelable) e.preventDefault();          // stop the native rubber-band while pulling
      setPullVal(Math.min(dy * 0.5, 90));            // damped, capped
    }
    function onEnd() {
      if (!st.tracking) return;
      st.tracking = false;
      const fire = st.pulling && pullRef.current >= 60; // released past the threshold
      st.pulling = false; setPullVal(0);
      if (fire) doRefresh();
    }
    document.addEventListener("touchstart", onStart, { passive: true });
    document.addEventListener("touchmove", onMove, { passive: false });
    document.addEventListener("touchend", onEnd, { passive: true });
    document.addEventListener("touchcancel", onEnd, { passive: true });
    return () => {
      document.removeEventListener("touchstart", onStart);
      document.removeEventListener("touchmove", onMove);
      document.removeEventListener("touchend", onEnd);
      document.removeEventListener("touchcancel", onEnd);
    };
  }, []);

  // after a successful sign-in/restore: load data, then map the login to a person by email
  async function afterAuth() {
    const d = await window.loadData();
    setData(d);
    const email = (await window.authEmail()) || "";
    const u = d.users.find((x) => (x.email || "").toLowerCase() === email.toLowerCase());
    if (!u) {
      await window.authSignOut();
      setSession(null);
      setAuthError("บัญชีนี้ยังไม่ได้ผูกกับผู้ใช้ · This login isn't linked to a user yet. Ask your admin to add you with this exact email.");
      return false;
    }
    setSession({ userId: u.id, role: u.role, companyIds: u.companyIds, companyId: u.companyIds[0] });
    nav("home");
    return true;
  }

  async function handleSignIn(email, password) {
    setAuthError(null);
    const { data: r, error } = await window.authSignIn(email, password);
    if (error) {
      // friendlier message when the account exists but the email link wasn't clicked yet
      setAuthError(/not confirmed/i.test(error.message) ? window.t(lang, "emailNotConfirmed") : error.message);
      return;
    }
    pwRef.current = password;
    if (r && r.session) window.__sbToken = r.session.access_token;
    const ok = await afterAuth();
    if (ok && !window.hasPin()) setShowSetPin(true);   // offer to save a quick PIN after a fresh login
  }
  async function handleSignUp(email, password) {
    setAuthError(null);
    const { data: r, error } = await window.authSignUp(email, password);
    if (error) { setAuthError(error.message); return; }
    if (r && r.session) { pwRef.current = password; window.__sbToken = r.session.access_token; const ok = await afterAuth(); if (ok && !window.hasPin()) setShowSetPin(true); return; }
    // no session = email confirmation required: tell the Login screen to show the
    // friendly "check your email" notice (not a red error)
    return "confirm";
  }
  // forgot password: send a reset link to the typed email. Returns true on success
  // so the Login screen can show a "check your email" confirmation.
  async function handleForgot(email) {
    setAuthError(null);
    if (!email || !email.trim()) { setAuthError(window.t(lang, "enterEmailFirst")); return false; }
    const redirectTo = window.location.origin + window.location.pathname;
    const { error } = await window.authResetPassword(email, redirectTo);
    if (error) { setAuthError(error.message); return false; }
    return true;
  }
  // set the new password (we're in a recovery session from the email link)
  async function handleResetPassword(newPassword) {
    setAuthError(null);
    const { error } = await window.authUpdatePassword(newPassword);
    if (error) { setAuthError(error.message); return; }
    pwRef.current = newPassword;
    window.clearPin();                                   // old PIN unlocked the OLD password — drop it
    try { history.replaceState(null, "", window.location.pathname); } catch (e) {} // strip the recovery hash
    setRecovering(false);
    const sess = (await window.authGetSession()).data.session;
    if (sess) window.__sbToken = sess.access_token;
    const ok = await afterAuth();
    if (ok) setShowSetPin(true); else nav("login");      // offer to set a fresh PIN
  }

  // PIN handlers
  async function handleUnlock(pin) {
    setPinError(null); setPinBusy(true);
    try {
      const pw = await window.unlockPin(pin);
      if (pw == null) { setPinError("PIN ไม่ถูกต้อง · Wrong PIN"); return; }
      pwRef.current = pw;
      if (session) { setLocked(false); return; }        // locked over an already-active session
      // reopen: make sure we have a session, signing in with the saved password if needed
      let sess = (await window.authGetSession()).data.session;
      if (!sess) {
        const { data: r, error } = await window.authSignIn(window.pinEmail(), pw);
        if (error) { setPinError("เข้าสู่ระบบไม่สำเร็จ · " + error.message); return; }
        sess = r && r.session;
      }
      if (sess) window.__sbToken = sess.access_token;
      const ok = await afterAuth();
      if (!ok) { setPinError("ใช้อีเมล & รหัสผ่าน · Please use email & password."); return; }
      setLocked(false);
    } finally { setPinBusy(false); }
  }
  function handleUsePassword() {        // escape from the PIN screen back to email/password (forgets the PIN)
    window.authSignOut(); window.clearPin(); pwRef.current = "";
    setLocked(false); setSession(null); setData(null); setPinError(null);
  }
  async function handleSetPin(pin, passwordFromSheet) {
    const email = (await window.authEmail()) || "";
    const pw = passwordFromSheet || pwRef.current || "";
    if (email) { await window.setPin(pin, email, pw); pwRef.current = pw; }
    setShowSetPin(false);
  }

  // apply accent + density
  uE(() => {
    const r = document.documentElement.style;
    r.setProperty("--accent", t.accent);
    r.setProperty("--accent-press", t.accent);
    r.setProperty("--accent-soft", hexToRgba(t.accent, 0.14));
    r.setProperty("--accent-line", hexToRgba(t.accent, 0.38));
    document.body.classList.remove("dense", "comfy");
    if (t.density === "compact") document.body.classList.add("dense");
    if (t.density === "comfy") document.body.classList.add("comfy");
  }, [t.accent, t.density]);

  uE(() => { document.documentElement.lang = lang; }, [lang]);

  // theme: dark | light | system (follow the phone). Per-device (localStorage).
  uE(() => {
    const mq = window.matchMedia("(prefers-color-scheme: dark)");
    const apply = () => {
      const dark = theme === "dark" || (theme === "system" && mq.matches);
      document.documentElement.classList.toggle("light", !dark);
    };
    apply();
    if (theme !== "system") return;
    const h = () => apply();
    mq.addEventListener ? mq.addEventListener("change", h) : mq.addListener(h);
    return () => { mq.removeEventListener ? mq.removeEventListener("change", h) : mq.removeListener(h); };
  }, [theme]);

  function nav(name, params = {}) { setRoute({ name, params }); document.querySelector(".scroll") && (document.querySelector(".scroll").scrollTop = 0); }

  function onSignOut() {
    window.authSignOut(); setSession(null); setData(null); setAuthError(null);
    // keep the PIN: lock back to the PIN screen (re-enter PIN to sign in).
    // To fully remove the PIN / switch user, use "Use email & password" on the PIN screen.
    if (window.pinInfo()) { setLocked(true); } else { setLocked(false); nav("login"); }
  }
  function switchCompany(id) { setSession((s) => ({ ...s, companyId: id })); }

  function pickLang(code) { setLang(code); setShowLang(false); }
  function pickTheme(v) { try { localStorage.setItem("pettycash-theme", v); } catch (e) {} setTheme(v); }

  // ----- back office: company-scoped, live-commit handlers -----
  // shared failure handler for back-office saves: warn, then resync from server.
  // exposed on window so the debounced company write (which runs detached) can use it.
  function dbFail(e) {
    console.error(e);
    alert("บันทึกไม่สำเร็จ · Could not save your change. Reloading the latest data.\n\n" + (e && e.message ? e.message : e));
    window.loadData().then(setData).catch(() => {});
  }
  window.__dbFail = dbFail;

  function patchCompany(id, patch) {
    setData((d) => ({ ...d, companies: d.companies.map((c) => c.id === id ? { ...c, ...patch } : c) }));
    window.dbPatchCompany(id, patch); // debounced; persists the settled value
  }
  function addCompany() {
    const id = "c" + Date.now();
    // companies.code is NOT NULL UNIQUE (<=5, upper) — seed a unique placeholder the user then edits
    const code = "C" + Date.now().toString(36).slice(-4).toUpperCase();
    const blank = {
      id, code, name: "บริษัทใหม่ · New Company", address: "", taxId: "",
      branchCode: "00000", branch: "สำนักงานใหญ่ · Head office",
      opexUpperBound: 5000, approver: "", accountantEmail: "",
      ingredients: [], expenses: [], members: [], approvalMap: {}, seq: { FGR: 0, OPX: 0 },
    };
    setData((d) => ({ ...d, companies: [...d.companies, blank] }));
    window.dbAddCompany(blank).catch(dbFail);
    return id;
  }
  // Add a new branch under an existing legal entity: reuse its code + legal name + tax ID,
  // assign the next free branch code, and start with empty staff / lists / counters.
  function addBranch(fromId) {
    const src = data.companies.find((c) => c.id === fromId);
    if (!src) return addCompany();
    const id = "c" + Date.now();
    // next branch code = max existing code for this legal entity (same code + tax) + 1
    const siblings = data.companies.filter((c) => c.code === src.code && c.taxId === src.taxId);
    const maxNo = siblings.reduce((m, c) => Math.max(m, parseInt(c.branchCode, 10) || 0), 0);
    const branchCode = String(maxNo + 1).padStart(5, "0");
    const blank = {
      id, code: src.code, name: src.name, address: "", taxId: src.taxId,
      branchCode, branch: "สาขาใหม่ · New branch",
      opexUpperBound: src.opexUpperBound, approver: "", accountantEmail: src.accountantEmail || "",
      ingredients: [], expenses: [], members: [], approvalMap: {}, seq: { FGR: 0, OPX: 0 },
    };
    setData((d) => ({ ...d, companies: [...d.companies, blank] }));
    window.dbAddCompany(blank).catch(dbFail);
    return id;
  }
  function deleteCompany(id) {
    setData((d) => {
      const companies = d.companies.filter((c) => c.id !== id);
      return {
        ...d, companies,
        users: d.users.map((u) => ({ ...u, companyIds: u.companyIds.filter((cid) => cid !== id) })),
      };
    });
    setBoCompanyId((cur) => cur === id ? null : cur);
    window.dbDeleteCompany(id).catch(dbFail);
  }
  function upsertListItem(companyId, key, item) {
    setData((d) => ({
      ...d, companies: d.companies.map((c) => {
        if (c.id !== companyId) return c;
        const exists = c[key].some((x) => x.id === item.id);
        return { ...c, [key]: exists ? c[key].map((x) => x.id === item.id ? item : x) : [...c[key], item] };
      }),
    }));
    window.dbUpsertListItem(companyId, key, item).catch(dbFail);
  }
  function deleteListItem(companyId, key, itemId) {
    setData((d) => ({ ...d, companies: d.companies.map((c) => c.id === companyId ? { ...c, [key]: c[key].filter((x) => x.id !== itemId) } : c) }));
    window.dbDeleteListItem(companyId, key, itemId).catch(dbFail);
  }
  function upsertUser(user, isNew) {
    setData((d) => ({ ...d, users: isNew || !d.users.some((u) => u.id === user.id) ? [...d.users, user] : d.users.map((u) => u.id === user.id ? { ...u, ...user } : u) }));
    window.dbUpsertUser(user).catch(dbFail);
  }
  function setMembership(companyId, userId, isMember) {
    setData((d) => ({
      ...d,
      users: d.users.map((u) => u.id === userId
        ? { ...u, companyIds: isMember ? Array.from(new Set([...u.companyIds, companyId])) : u.companyIds.filter((c) => c !== companyId) }
        : u),
      companies: isMember ? d.companies : d.companies.map((c) => {
        if (c.id !== companyId) return c;
        const am = { ...(c.approvalMap || {}) }; delete am[userId];
        return { ...c, approvalMap: am };
      }),
    }));
    window.dbSetMembership(companyId, userId, isMember).catch(dbFail);
  }
  function setApprovers(companyId, creatorId, assignment) {
    setData((d) => ({
      ...d, companies: d.companies.map((c) => c.id === companyId
        ? { ...c, approvalMap: { ...(c.approvalMap || {}), [creatorId]: { peer: assignment.peer || null, supervisor: assignment.supervisor || null } } }
        : c),
    }));
    window.dbSetApprovers(companyId, creatorId, assignment).catch(dbFail);
  }

  // doc flow
  function onReview(d) { setDraft(d); nav("review"); }

  function onSubmit(d, creatorSign) {
    const kind = d.type; // FGR | OPX
    const company = data.companies.find((c) => c.id === d.companyId);
    const { seq } = window.nextDocNo(company, kind);
    const id = "d" + Date.now();
    const newDoc = {
      id, type: d.type, companyId: d.companyId, no: d.docNo,
      date: new Date().toISOString(), status: "pending",
      creatorId: session.userId, creatorSign,
      total: d.total, whtTotal: d.whtTotal || 0, netTotal: d.netTotal != null ? d.netTotal : d.total,
      ...(d.type === "FGR" ? { items: d.items } : { lines: d.lines, evidenceMode: d.evidenceMode || "supplier", sellerId: d.sellerId, workPhoto: d.workPhoto, sellerSign: d.sellerSign }),
    };
    setData((prev) => ({
      ...prev,
      companies: prev.companies.map((c) => c.id === company.id ? { ...c, seq: { ...c.seq, [kind]: seq } } : c),
      documents: [newDoc, ...prev.documents],
    }));
    // persist to Supabase; on failure, warn and resync from the server
    window.dbCreateDocument(newDoc, seq).catch((e) => {
      console.error(e);
      alert("บันทึกเอกสารไม่สำเร็จ · Could not save the document. Please check your connection.\n\n" + e.message);
      window.loadData().then(setData).catch(() => {});
    });
    setDraft(null);
    nav("submitted", { docId: id, docNo: d.docNo });
  }

  function onApprove(docId, approverSign, approverName) {
    setData((prev) => ({
      ...prev,
      documents: prev.documents.map((d) => d.id === docId ? { ...d, status: "approved", approverSign, approverName } : d),
    }));
    // build the approved doc for the PDF snapshot
    const doc0 = data.documents.find((d) => d.id === docId);
    const company = doc0 && data.companies.find((c) => c.id === doc0.companyId);
    const approvedDoc = doc0 ? { ...doc0, status: "approved", approverSign, approverName } : null;
    window.dbApproveDocument(docId, session.userId, approverSign, approverName).then(async () => {
      // attach a PDF of the document if we can build one; email is best-effort either way
      let pdf = null;
      if (approvedDoc && company) { try { pdf = await makeDocPdf(approvedDoc, company, lang); } catch (e) { /* fall back to no attachment */ } }
      const body = pdf ? { docId, pdf, filename: (approvedDoc.no || docId) + ".pdf" } : { docId };
      window.sbClient.functions.invoke("send-approval-email", { body }).catch((e) => console.error("approval email failed", e));
    }).catch((e) => {
      console.error(e);
      alert("บันทึกการอนุมัติไม่สำเร็จ · Could not save the approval. Please check your connection.\n\n" + e.message);
      window.loadData().then(setData).catch(() => {});
    });
    nav("sent", { docId });
  }

  // Resend the approval email for an already-approved doc. Re-reads the current
  // accountant email (flushes the latest value to the DB first, then the edge
  // function reads it live), and re-attaches a fresh PDF. Best-effort with feedback.
  async function onResendEmail(docId) {
    const doc = data.documents.find((d) => d.id === docId);
    const company = doc && data.companies.find((c) => c.id === doc.companyId);
    if (!doc || !company) return false;
    const email = company.accountantEmail || "";
    if (!window.confirm(window.t(lang, "resendConfirm") + "\n\n" + (email || "—"))) return false;
    try {
      // make sure the DB holds the latest accountant email so the function re-reads it
      if (email) { try { await window.dbUpdateCompany(company.id, { accountantEmail: email }); } catch (e) { /* non-fatal */ } }
      let pdf = null;
      try { pdf = await makeDocPdf(doc, company, lang); } catch (e) { /* send without attachment */ }
      const body = pdf ? { docId, pdf, filename: (doc.no || docId) + ".pdf" } : { docId };
      const r = await window.sbClient.functions.invoke("send-approval-email", { body });
      if (r && r.error) throw r.error;
      alert(window.t(lang, "resendSent") + "\n\n" + email);
      return true;
    } catch (e) {
      alert(window.t(lang, "resendFailed") + "\n\n" + (e.message || String(e)));
      return false;
    }
  }

  // ----- boot / auth gates -----
  let screen;
  if (recovering) {
    screen = <ResetPassword lang={lang} onLang={() => setShowLang(true)} onReset={handleResetPassword} authError={authError} />;
  } else if (!authChecked) {
    screen = <div className="app" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}><div style={{ opacity: 0.6 }}>กำลังโหลด… · Loading…</div></div>;
  } else if (locked) {
    screen = <PinUnlock lang={lang} onUnlock={handleUnlock} onUsePassword={handleUsePassword} error={pinError} busy={pinBusy} />;
  } else if (!session) {
    screen = <Login lang={lang} onLang={() => setShowLang(true)} onSignIn={handleSignIn} onSignUp={handleSignUp} onForgot={handleForgot} authError={authError} />;
  } else if (loadError) {
    screen = (
      <div className="app" style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: 24, textAlign: "center" }}>
        <div>
          <div style={{ fontSize: 17, fontWeight: 600, marginBottom: 8 }}>เชื่อมต่อฐานข้อมูลไม่สำเร็จ · Could not load data</div>
          <div style={{ opacity: 0.7, fontSize: 13, marginBottom: 16 }}>{loadError}</div>
          <button onClick={() => { setLoadError(null); window.loadData().then(setData).catch((e) => setLoadError(e.message || String(e))); }}>ลองใหม่ · Retry</button>
          <button style={{ marginLeft: 8 }} onClick={onSignOut}>ออกจากระบบ · Sign out</button>
        </div>
      </div>
    );
  } else if (!data) {
    screen = <div className="app" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}><div style={{ opacity: 0.6 }}>กำลังโหลด… · Loading…</div></div>;
  } else {
    const shared = { data, session, lang, onLang: () => setShowLang(true), nav, route };
    switch (route.name) {
      case "home": screen = <Home {...shared} onSignOut={onSignOut} onSwitchCompany={switchCompany} onLock={() => setLocked(true)} onSetPin={() => setShowSetPin(true)} />; break;
      case "backoffice": screen = <BackOffice {...shared} onSignOut={onSignOut}
        boCompanyId={boCompanyId || session.companyId || data.companies[0].id}
        onSwitchCompany={setBoCompanyId} onAddCompany={addCompany} onAddBranch={addBranch}
        patchCompany={patchCompany} deleteCompany={deleteCompany}
        upsertListItem={upsertListItem} deleteListItem={deleteListItem}
        upsertUser={upsertUser} setMembership={setMembership} setApprovers={setApprovers} />; break;
      case "create-grn": screen = <GRNForm {...shared} inputMode={t.inputMode} onReview={onReview} />; break;
      case "create-opex": screen = <OPEXForm {...shared} inputMode={t.inputMode} onReview={onReview} />; break;
      case "review": screen = <ReviewScreen {...shared} draft={draft} onSubmit={onSubmit} />; break;
      case "submitted": screen = <Submitted {...shared} />; break;
      case "approval-queue": screen = <ApprovalQueue {...shared} />; break;
      case "approval-detail": screen = <ApprovalDetail {...shared} onApprove={onApprove} />; break;
      case "sent": screen = <Sent {...shared} />; break;
      case "doc-view": screen = <DocView {...shared} onResend={onResendEmail} />; break;
      default: screen = <Home {...shared} onSignOut={onSignOut} onSwitchCompany={switchCompany} onLock={() => setLocked(true)} onSetPin={() => setShowSetPin(true)} />;
    }
  }

  return (
    <div className="viewport">
      {(pull > 0 || refreshing) && (
        <div className="ptr" style={{
          opacity: refreshing ? 1 : Math.min(pull / 50, 1),
          transform: `translateX(-50%) translateY(${refreshing ? 14 : Math.min(pull * 0.5, 40) - 6}px)`,
        }}>
          <div className={"ptr-ring" + (refreshing || pull >= 60 ? " spin" : "")} />
        </div>
      )}
      {screen}
      {showLang && <LangSheet lang={lang} theme={theme} onPick={pickLang} onPickTheme={pickTheme} onClose={() => setShowLang(false)} />}
      {showSetPin && session && data && <SetPinSheet lang={lang} requirePassword={!pwRef.current} onSave={handleSetPin} onClose={() => setShowSetPin(false)} />}
      <TweaksPanel>
        <TweakSection label={window.t(lang, "chooseInputMode")} />
        <TweakRadio label="Worker input" value={t.inputMode}
          options={["guided", "form"]} onChange={(v) => setTweak("inputMode", v)} />
        <TweakSection label="Appearance" />
        <TweakColor label="Accent" value={t.accent}
          options={["#a78bfa", "#3b82f6", "#34d399", "#f59e0b", "#e5e5e5"]}
          onChange={(v) => setTweak("accent", v)} />
        <TweakRadio label="Density" value={t.density}
          options={["compact", "regular", "comfy"]} onChange={(v) => setTweak("density", v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
