// Root app, hash router, context provider, Tweaks panel.
// Detects on boot whether a real API is available (i.e. the page is being
// served by our Node.js Hosting backend). If yes → API mode. If no → demo
// mode using localStorage so the design preview keeps working standalone.
//
// [HB-ADD] June 2026: announcement-bar state + /legal routes. Additions are
// marked with [HB-ADD] and are deliberately self-contained (direct fetch,
// guarded component lookups) so they can't break the rest of the app.

// [HB] Shrink big photos in the browser before uploading, so an image is always
// small enough for the database to store (large phone photos can otherwise
// exceed the DB's packet limit and fail to save). Downscales to fit within
// maxDim px and re-encodes as JPEG. Leaves small images, GIFs and SVGs alone,
// and falls back to the original file if anything goes wrong.
async function shrinkImageForUpload(file, maxDim = 1600, maxBytes = 1800000) {
  try {
    if (!file || !/^image\//.test(file.type)) return file;
    if (file.type === "image/gif" || file.type === "image/svg+xml") return file;
    const dataUrl = await new Promise((res, rej) => {
      const r = new FileReader();
      r.onload = () => res(r.result);
      r.onerror = () => rej(new Error("read failed"));
      r.readAsDataURL(file);
    });
    const img = await new Promise((res, rej) => {
      const i = new Image();
      i.onload = () => res(i);
      i.onerror = () => rej(new Error("decode failed"));
      i.src = dataUrl;
    });
    const bigDim = Math.max(img.width, img.height);
    if (bigDim <= maxDim && file.size <= maxBytes) return file; // already fine
    const scale = Math.min(1, maxDim / bigDim);
    const w = Math.max(1, Math.round(img.width * scale));
    const h = Math.max(1, Math.round(img.height * scale));
    const canvas = document.createElement("canvas");
    canvas.width = w; canvas.height = h;
    canvas.getContext("2d").drawImage(img, 0, 0, w, h);
    const blob = await new Promise(res => canvas.toBlob(res, "image/jpeg", 0.85));
    if (!blob || blob.size >= file.size) return file; // no gain — keep original
    const base = (file.name || "image").replace(/\.[^.]+$/, "");
    return new File([blob], base + ".jpg", { type: "image/jpeg" });
  } catch (e) {
    return file; // never block a save because resizing failed
  }
}

const SEED_ITEMS = [
  { id: "seed1", title: "Personalised baby blanket", description: "Soft cotton blanket with stitched name & date. Made fresh per order.", price: "38.00", category: "ready", brand: "hazelbelle", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 5 },
  { id: "seed2", title: "Embroidered linen apron", description: "Sturdy linen, with your name or business stitched in your colour.", price: "32.00", category: "ready", brand: "hazelbelle", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 3 },
  { id: "seed3", title: "Cotton tote — botanical", description: "A neat little tote with a sage-thread leaf motif. Lovely for gifting.", price: "18.00", category: "ready", brand: "hazelbelle", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 1 },
  { id: "seed4", title: "Football club polo set", description: "Made for the Riverside U12s — embroidered crest on chest, names on back.", price: "", category: "example", brand: "hazelbelle", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 12 },
  { id: "seed5", title: "Christening keepsake cushion", description: "Cream linen cushion with hand-finished script and a delicate floral spray.", price: "", category: "example", brand: "hazelbelle", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 18 },
  { id: "seed6", title: "Bakery apron set (×6)", description: "Matching aprons for a small artisan bakery — logo on chest, name on strap.", price: "", category: "example", brand: "hazelbelle", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 22 },
  { id: "ccc1", title: "Couch Cowboy logo tee",      description: "Classic cotton tee with the Couch Cowboy brand stitched on the chest.",                price: "28.00", category: "ready", brand: "couchcowboy", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 2 },
  { id: "ccc2", title: "Couch Cowboy ranch cap",     description: "Six-panel cap with embroidered Couch Cowboy mark. One size, adjustable.",              price: "24.00", category: "ready", brand: "couchcowboy", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 4 },
  { id: "ccc3", title: "Couch Cowboy work hoodie",   description: "Heavyweight pullover hoodie, brand on chest, cattle co. wordmark across the back.",    price: "52.00", category: "ready", brand: "couchcowboy", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 7 },
  { id: "ccc4", title: "Couch Cowboy canvas tote",   description: "Heavy-duty canvas tote with stitched brand mark — built for the feed store run.",     price: "22.00", category: "ready", brand: "couchcowboy", image: "", createdAt: Date.now() - 1000 * 60 * 60 * 24 * 9 },
];

const SEED_UPLOAD_TEMPLATE = {
  name: "Sarah Bramley",
  email: "sarah.b@example.com",
  itemType: "Hoodie / sweatshirt",
  qty: 12,
  size: "Mixed",
  placement: "Chest (left)",
  threadColors: ["Sage", "Cream"],
  notes: "12 hoodies for the office team — logo on the left chest, please. We've attached our usual SVG. Need them by mid-June.",
  files: [{ name: "team-logo.svg", size: 24500, type: "image/svg+xml", dataUrl: "" }],
  status: "stitching",
  approval: "approved",
};

const DEFAULT_TWEAKS = /*EDITMODE-BEGIN*/{
  "facebookUrl": "https://www.facebook.com/61590039444991",
  "contactEmail": "hazelbelleemb@gmail.com",
  "location": "North Fort Myers, Florida",
  "palette": "rose",
  "stitchedBorders": true,
  "fontPair": "garamond"
}/*EDITMODE-END*/;

// [HB-ADD] Announcement bar default shape — also exposed globally because
// the admin "Site settings" tab (pages-admin.jsx) references it.
const HB_ANNOUNCEMENT_DEFAULT = {
  enabled: false,
  text: "",
  link: "",
  bgColor: "#934e5c",
  textColor: "#fdf7ea",
  holidayEnabled: false,
  holiday: "auto",
};
window.HB_ANNOUNCEMENT_DEFAULT = HB_ANNOUNCEMENT_DEFAULT;

function useHashRoute() {
  const [route, setRoute] = useState(() => location.hash.replace(/^#/, "") || "/");
  useEffect(() => {
    const handler = () => setRoute(location.hash.replace(/^#/, "") || "/");
    window.addEventListener("hashchange", handler);
    return () => window.removeEventListener("hashchange", handler);
  }, []);
  return [route, (r) => { location.hash = r; }];
}

// Mix a hex colour toward a target hex by `amt` (0–1). Used to lift the brand
// accents for dark mode so rose/sage stay readable on a dark background.
function hbMixHex(hex, target, amt) {
  const h = hex.replace("#", "");
  const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
  const tr = parseInt(target.slice(1, 3), 16), tg = parseInt(target.slice(3, 5), 16), tb = parseInt(target.slice(5, 7), 16);
  const mix = (a, t) => Math.round(a + (t - a) * amt);
  const to2 = (n) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, "0");
  return "#" + to2(mix(r, tr)) + to2(mix(g, tg)) + to2(mix(b, tb));
}

function applyTweaks(tweaks) {
  const root = document.documentElement;
  const palettes = {
    rose:   { rose: "#b86a78", roseDeep: "#934e5c", sage: "#8a9966", sageDeep: "#6a7a4d" },
    bolder: { rose: "#a04a5a", roseDeep: "#6e2b39", sage: "#7a8a52", sageDeep: "#54632f" },
    soft:   { rose: "#cf8a96", roseDeep: "#aa6571", sage: "#9faa84", sageDeep: "#7c8a62" },
  };
  const p = palettes[tweaks.palette] || palettes.rose;
  // In dark mode the deep accents (used for links, headings, badges) are too
  // dark to read on a dark surface, so lift them toward white.
  const dark = root.getAttribute("data-theme") === "dark";
  const W = "#ffffff";
  const acc = dark ? {
    rose: hbMixHex(p.rose, W, 0.18), roseDeep: hbMixHex(p.roseDeep, W, 0.42),
    sage: hbMixHex(p.sage, W, 0.16), sageDeep: hbMixHex(p.sageDeep, W, 0.40),
  } : p;
  root.style.setProperty("--rose", acc.rose);
  root.style.setProperty("--rose-deep", acc.roseDeep);
  root.style.setProperty("--sage", acc.sage);
  root.style.setProperty("--sage-deep", acc.sageDeep);

  document.body.classList.toggle("no-stitch", !tweaks.stitchedBorders);

  const fonts = {
    garamond: "'Cormorant Garamond', Georgia, serif",
    playfair: "'Playfair Display', Georgia, serif",
    fraunces: "'Fraunces', Georgia, serif",
  };
  document.body.style.fontFamily = fonts[tweaks.fontPair] || fonts.garamond;
}

function HazelbelleTweaks({ tweaks, setTweak }) {
  return (
    <TweaksPanel title="Tweaks · Hazelbelle">
      <TweakSection label="Brand & details">
        <TweakText label="Facebook URL"    value={tweaks.facebookUrl}  onChange={(v) => setTweak("facebookUrl", v)} />
        <TweakText label="Contact email"   value={tweaks.contactEmail} onChange={(v) => setTweak("contactEmail", v)} />
        <TweakText label="Studio location" value={tweaks.location}     onChange={(v) => setTweak("location", v)} />
      </TweakSection>
      <TweakSection label="Look & feel">
        <TweakRadio
          label="Palette"
          value={tweaks.palette}
          options={[
            { value: "rose",   label: "Rose" },
            { value: "bolder", label: "Bolder" },
            { value: "soft",   label: "Softer" },
          ]}
          onChange={(v) => setTweak("palette", v)}
        />
        <TweakRadio
          label="Body font"
          value={tweaks.fontPair}
          options={[
            { value: "garamond", label: "Cormorant" },
            { value: "playfair", label: "Playfair" },
            { value: "fraunces", label: "Fraunces" },
          ]}
          onChange={(v) => setTweak("fontPair", v)}
        />
        <TweakToggle label="Stitched borders" value={tweaks.stitchedBorders} onChange={(v) => setTweak("stitchedBorders", v)} />
      </TweakSection>
      <TweakSection label="Demo data">
        <TweakButton label="+ Seed demo upload" onClick={() => window.__hb_seedDemoUpload?.()} />
        <TweakButton label="Reset all data" secondary onClick={() => { if (confirm("Reset all uploads, messages and items?")) window.__hb_reset?.(); }} />
      </TweakSection>
    </TweaksPanel>
  );
}

function App() {
  const [route, go] = useHashRoute();
  const [tweaks, setTweaksRaw] = useState(() => ({ ...DEFAULT_TWEAKS }));
  // mode: "checking" | "api" | "demo"
  const [mode, setMode] = useState("checking");
  const [items, setItems] = useState([]);
  const [uploads, setUploads] = useState([]);
  const [messages, setMessages] = useState([]);
  const [adminAuthed, setAdminAuthed] = useState(false);
  const [customer, setCustomer] = useState(null); // signed-in customer, or null
  const [toastMsg, setToastMsg] = useState("");
  // [HB-ADD] light/dark theme — persists across visits; respects the OS setting
  // on a first visit. Applied via a data-theme attribute on <html>.
  const [theme, setTheme] = useState(() => {
    try { const s = localStorage.getItem("hb_theme"); if (s === "light" || s === "dark") return s; } catch (e) {}
    try { if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark"; } catch (e) {}
    return "light";
  });
  const toggleTheme = useCallback(() => setTheme(t => (t === "dark" ? "light" : "dark")), []);
  // [HB-ADD] announcement state
  const [announcement, setAnnouncement] = useState(() => ({ ...HB_ANNOUNCEMENT_DEFAULT }));
  // [HB-ADD] editable site content, blog, visit counter, admin settings
  const [siteContent, setSiteContent] = useState({});
  const [blogPosts, setBlogPosts] = useState([]);       // published (public)
  const [faqs, setFaqs] = useState([]);                 // published FAQs (public)
  const [storeStock, setStoreStock] = useState([]);     // in-store inventory (public)
  const [allBlogPosts, setAllBlogPosts] = useState([]); // incl. drafts (admin)
  const [counter, setCounter] = useState(null);
  const [adminSettings, setAdminSettings] = useState({});
  const [adminCustomers, setAdminCustomers] = useState([]); // customer accounts (admin view)

  // [HB-ADD] shopping cart — browser-side, persists across visits via localStorage
  const [cart, setCart] = useState(() => {
    try { return JSON.parse(localStorage.getItem("hb_cart_v1") || "[]"); } catch (e) { return []; }
  });
  useEffect(() => {
    try { localStorage.setItem("hb_cart_v1", JSON.stringify(cart)); } catch (e) {}
  }, [cart]);
  const [adminStoreOrders, setAdminStoreOrders] = useState([]); // store orders (admin view)

  const addToCart = (item, qty = 1) => {
    setCart(prev => {
      const max = Number.isFinite(item.maxQty) ? item.maxQty : 999;
      const i = prev.findIndex(x => x.inventoryId === item.inventoryId);
      if (i >= 0) {
        const next = [...prev];
        next[i] = { ...next[i], maxQty: max, qty: Math.min(max, next[i].qty + qty) };
        return next;
      }
      return [...prev, { inventoryId: item.inventoryId, description: item.description, unitPrice: Number(item.unitPrice) || 0, imageUrl: item.imageUrl || "", maxQty: max, qty: Math.min(max, qty) }];
    });
  };
  const setCartQty = (inventoryId, qty) => setCart(prev =>
    prev.flatMap(x => {
      if (x.inventoryId !== inventoryId) return [x];
      const cap = Number.isFinite(x.maxQty) ? x.maxQty : 999;
      const q = Math.max(0, Math.min(cap, qty));
      return q === 0 ? [] : [{ ...x, qty: q }];
    })
  );
  const removeFromCart = (inventoryId) => setCart(prev => prev.filter(x => x.inventoryId !== inventoryId));
  const clearCart = () => setCart([]);

  const refreshStoreOrders = async () => {
    if (mode !== "api") return;
    try { setAdminStoreOrders(await api.listStoreOrders()); } catch (e) {}
  };

  const setTweak = useCallback((kOrObj, v) => {
    setTweaksRaw(prev => {
      const edits = typeof kOrObj === "string" ? { [kOrObj]: v } : kOrObj;
      try { window.parent.postMessage({ type: "__edit_mode_set_keys", edits }, "*"); } catch (e) {}
      return { ...prev, ...edits };
    });
  }, []);

  useEffect(() => { applyTweaks(tweaks); }, [tweaks]);

  // [HB-ADD] reflect theme on <html>, persist it, and recompute accents
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    try { localStorage.setItem("hb_theme", theme); } catch (e) {}
    applyTweaks(tweaks);
  }, [theme]);

  // [HB-ADD] Per-route SEO / AI metadata. This is a hash-routed SPA, so the
  // static <head> only describes the homepage. JS-rendering crawlers (Google)
  // and AI assistants read the live DOM, so we keep <title>, the description,
  // the Open Graph tags and the robots directive in step with the current view.
  useEffect(() => {
    const SITE = "Hazelbelle Embroidery & Print";
    const p = (route || "/").split("?")[0];
    const seg = "/" + (p.split("/")[1] || "");
    const META = {
      "/":        { t: `${SITE} · Custom embroidery in North Fort Myers, FL`, d: "Hand-finished custom embroidery, monogramming and bespoke print by Joy in North Fort Myers, Florida. Upload your design for a quote, or shop ready-made pieces." },
      "/shop":    { t: `Shop & Examples · ${SITE}`, d: "Browse ready-made embroidered pieces and examples of past custom work from Hazelbelle Embroidery & Print." },
      "/blanks":  { t: `Shop Blanks · ${SITE}`, d: "Choose blank garments — tees, polos, caps, hoodies and more — to be custom embroidered or printed by Hazelbelle." },
      "/quote":   { t: `Get a Quote · ${SITE}`, d: "Estimate your custom embroidery or print order — garment, stitch count, quantity and finishing. A friendly ballpark in seconds." },
      "/upload":  { t: `Upload a Design · ${SITE}`, d: "Start a bespoke order: upload your logo, sketch or artwork and Joy will reply with a quote and timeline within 1–2 working days." },
      "/blog":    { t: `Journal · ${SITE}`, d: "Stories, projects and stitching notes from Joy's embroidery studio in North Fort Myers, Florida." },
      "/about":   { t: `About Joy · ${SITE}`, d: "Meet Joy — the maker behind Hazelbelle Embroidery & Print. Every piece is hand-finished in her North Fort Myers, Florida studio." },
      "/contact": { t: `Contact · ${SITE}`, d: "Get in touch about a custom embroidery or print project in North Fort Myers, Florida." },
      "/cart":    { t: `Your Cart · ${SITE}`, d: "Review the ready-made items in your cart and check out securely." },
      "/faq":     { t: `FAQ · ${SITE}`, d: "Answers about custom embroidery, ordering, turnaround times and shipping from Hazelbelle." },
      "/account": { t: `My Account · ${SITE}`, d: "Sign in to track your Hazelbelle orders, view invoices and manage your details.", noindex: true },
      "/admin":   { t: `Studio · ${SITE}`, d: "Private studio dashboard.", noindex: true },
      "/floor":   { t: `Shop Floor · ${SITE}`, d: "Operator sign-in.", noindex: true },
      "/guest":   { t: `Order Tracking · ${SITE}`, d: "Track your Hazelbelle order.", noindex: true },
    };
    let m = META[p] || META[seg] || META["/"];
    // A single blog post: prefer the real published title/excerpt if we have it.
    if (seg === "/blog" && p.startsWith("/blog/")) {
      const slug = decodeURIComponent(p.split("/")[2] || "");
      const post = (blogPosts || []).find(b => b.slug === slug);
      const nice = slug.replace(/[-_]+/g, " ").replace(/\b\w/g, c => c.toUpperCase());
      m = post
        ? { t: `${post.title} · Journal · ${SITE}`, d: (post.excerpt || post.summary || m.d || "").toString().slice(0, 300) }
        : { t: `${nice || "Journal"} · ${SITE}`, d: m.d };
    }
    const setMeta = (sel, attr, key, val) => {
      if (!val && val !== "") return;
      let el = document.head.querySelector(sel);
      if (!el) { el = document.createElement("meta"); el.setAttribute(attr, key); document.head.appendChild(el); }
      el.setAttribute("content", val);
    };
    document.title = m.t;
    setMeta('meta[name="description"]', "name", "description", m.d);
    setMeta('meta[property="og:title"]', "property", "og:title", m.t);
    setMeta('meta[property="og:description"]', "property", "og:description", m.d);
    setMeta('meta[property="og:url"]', "property", "og:url", "https://hazelbelleemb.com/" + (p === "/" ? "" : "#" + p));
    setMeta('meta[name="twitter:title"]', "name", "twitter:title", m.t);
    setMeta('meta[name="twitter:description"]', "name", "twitter:description", m.d);
    setMeta('meta[name="robots"]', "name", "robots",
      m.noindex ? "noindex, nofollow" : "index, follow, max-image-preview:large, max-snippet:-1");
  }, [route, blogPosts]);

  // Preload extra fonts
  useEffect(() => {
    const id = "extra-fonts";
    if (document.getElementById(id)) return;
    const link = document.createElement("link");
    link.id = id;
    link.rel = "stylesheet";
    link.href = "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600;700&family=Fraunces:wght@500;600&display=swap";
    document.head.appendChild(link);
  }, []);

  const toast = useCallback((msg) => setToastMsg(msg), []);

  // -------- Boot: detect API mode, then load --------
  useEffect(() => {
    (async () => {
      const ok = await api.detect();
      if (ok) {
        setMode("api");
        try {
          const its = await api.listItems();
          setItems(its);
          // [HB-ADD] editable content + published blog posts (non-fatal if either fails)
          try { setSiteContent(await api.listContent()); } catch (e) {}
          try { setBlogPosts(await api.listBlog()); } catch (e) {}
          try { setFaqs(await api.listFaqs()); } catch (e) {}
          try { setStoreStock(await api.listStoreStock()); } catch (e) {}
          // [HB-ADD] count this visit (cookie-throttled server-side; fire and forget)
          api.bumpVisit().catch(() => {});
          // [HB-ADD] load announcement (direct fetch — independent of api.js version)
          try {
            const r = await fetch("/api/settings/announcement", { credentials: "same-origin" });
            if (r.ok) setAnnouncement({ ...HB_ANNOUNCEMENT_DEFAULT, ...(await r.json()) });
          } catch (e) {}
          const me = await api.me();
          if (me.isAdmin) {
            setAdminAuthed(true);
            await refreshAdminData();
          }
          // Restore a signed-in customer session, if any (non-fatal).
          try {
            const acc = await api.accountMe();
            if (acc && acc.customer) setCustomer(acc.customer);
          } catch (e) {}
        } catch (e) {
          console.error("API load failed:", e);
        }
      } else {
        setMode("demo");
        // Load from localStorage (with seed)
        try {
          const its = JSON.parse(localStorage.getItem("hb_items_v2") || "null") || SEED_ITEMS;
          setItems(its);
          setUploads(JSON.parse(localStorage.getItem("hb_uploads_v1") || "[]"));
          setMessages(JSON.parse(localStorage.getItem("hb_messages_v1") || "[]"));
          // [HB-ADD] demo-mode announcement from localStorage
          const savedA = JSON.parse(localStorage.getItem("hb_announcement_v1") || "null");
          if (savedA) setAnnouncement({ ...HB_ANNOUNCEMENT_DEFAULT, ...savedA });
        } catch (e) {}
        // Restore admin flag
        try { setAdminAuthed(sessionStorage.getItem("hb_admin_authed") === "1"); } catch (e) {}
      }
    })();
  }, []);

  const refreshAdminData = async () => {
    try {
      const [u, m] = await Promise.all([api.listUploads(), api.listMessages()]);
      setUploads(u);
      setMessages(m);
      // [HB-ADD] admin extras — each independent, none fatal
      try { setAllBlogPosts(await api.listAllBlog()); } catch (e) {}
      try { setCounter(await api.getCounter()); } catch (e) {}
      try { setAdminSettings(await api.listSettings()); } catch (e) {}
      try { setAdminCustomers(await api.listCustomers()); } catch (e) {}
    } catch (e) {
      if (e.code === 401) setAdminAuthed(false);
      else console.error("Admin load failed:", e);
    }
  };

  // [HB-ADD] re-fetch the customer accounts list (Customers tab)
  const refreshCustomers = async () => {
    try { setAdminCustomers(await api.listCustomers()); } catch (e) {}
  };

  // [HB-ADD] re-fetch published FAQs (after admin edits)
  const refreshFaqs = async () => {
    try { setFaqs(await api.listFaqs()); } catch (e) {}
  };

  // [HB-ADD] re-fetch the public in-store stock (after inventory edits)
  const refreshStoreStock = async () => {
    try { setStoreStock(await api.listStoreStock()); } catch (e) {}
  };

  // [HB-ADD] re-fetch just the visit counter (used by the Orders tab)
  const refreshCounter = async () => {
    try { setCounter(await api.getCounter()); } catch (e) {}
  };

  // -------- Demo persistence --------
  useEffect(() => {
    if (mode !== "demo") return;
    try { localStorage.setItem("hb_items_v2", JSON.stringify(items)); } catch (e) {}
  }, [items, mode]);
  useEffect(() => {
    if (mode !== "demo") return;
    try { localStorage.setItem("hb_uploads_v1", JSON.stringify(uploads)); } catch (e) {}
  }, [uploads, mode]);
  useEffect(() => {
    if (mode !== "demo") return;
    try { localStorage.setItem("hb_messages_v1", JSON.stringify(messages)); } catch (e) {}
  }, [messages, mode]);

  // -------- Mutators (branch on mode) --------
  // Items
  const addItem = async (it, imageFile) => {
    if (mode === "api") {
      const fd = new FormData();
      ["title", "description", "price", "category", "brand", "buyUrl", "inStockNow"].forEach(k => fd.append(k, it[k] ?? ""));
      if (imageFile) fd.append("image", await shrinkImageForUpload(imageFile));
      else if (it.image) fd.append("image", it.image);
      const saved = await api.createItem(fd);
      setItems(prev => [saved, ...prev]);
      return saved;
    }
    const withId = { ...it, id: it.id || (uid() + "-" + uid()), createdAt: Date.now() };
    setItems(prev => [withId, ...prev]);
    return withId;
  };
  const updateItem = async (id, patch, imageFile) => {
    if (mode === "api") {
      const fd = new FormData();
      Object.entries(patch).forEach(([k, v]) => fd.append(k, v ?? ""));
      if (imageFile) fd.append("image", await shrinkImageForUpload(imageFile));
      const saved = await api.updateItem(id, fd);
      setItems(prev => prev.map(i => i.id === id ? saved : i));
      return saved;
    }
    setItems(prev => prev.map(i => i.id === id ? { ...i, ...patch } : i));
  };
  const deleteItem = async (id) => {
    if (mode === "api") await api.deleteItem(id);
    setItems(prev => prev.filter(i => i.id !== id));
  };

  // Uploads
  const addUpload = async (record, fileObjects, savedLogoIds) => {
    if (mode === "api") {
      const fd = new FormData();
      ["name", "email", "orderRef", "service", "itemType", "qty", "size", "placement", "notes", "deadline"].forEach(k => {
        if (record[k] !== undefined) fd.append(k === "orderRef" ? "orderRef" : k, record[k] ?? "");
      });
      fd.append("threadColors", (record.threadColors || []).join(","));
      fd.append("lines", JSON.stringify(record.lines || []));
      fd.append("thumbs", JSON.stringify((record.files || []).map(f => f.thumb || "")));
      fd.append("savedLogoIds", JSON.stringify(savedLogoIds || []));
      fd.append("website", record.website || ""); // [HB] honeypot
      for (const f of (fileObjects || [])) fd.append("files", f);
      const saved = await api.createUpload(fd);
      setUploads(prev => [saved, ...prev]);
      return saved;
    }
    setUploads(prev => [record, ...prev]);
    return record;
  };
  // Saved logo library (signed-in customers).
  const accountLogos = async () => { if (mode !== "api") return []; const r = await api.accountLogos(); return (r && r.logos) || []; };
  const addLogo = async (file, name, thumb) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    const fd = new FormData();
    fd.append("logo", file);
    if (name) fd.append("name", name);
    if (thumb) fd.append("thumb", thumb);
    return api.accountLogoUpload(fd);
  };
  const renameLogo = async (id, name) => { if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG); return api.accountLogoRename(id, name); };
  const deleteLogo = async (id) => { if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG); return api.accountLogoDelete(id); };
  // Saved shipping address book (signed-in customers; surfaced for business accounts).
  const accountAddresses = async () => { if (mode !== "api") return []; const r = await api.accountAddresses(); return (r && r.addresses) || []; };
  const addAddress = async (b) => { if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG); return api.accountAddressCreate(b); };
  const updateAddress = async (id, b) => { if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG); return api.accountAddressUpdate(id, b); };
  const deleteAddress = async (id) => { if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG); return api.accountAddressDelete(id); };
  const updateUpload = async (id, patch) => {
    // Optimistic: reflect the change immediately so stage/approval buttons feel
    // instant. If the server rejects it, roll back and tell the user why.
    const prev = uploads;
    setUploads(prev.map(u => u.id === id ? { ...u, ...patch } : u));
    if (mode === "api") {
      try {
        await api.patchUpload(id, patch);
      } catch (e) {
        setUploads(prev);
        toast(e.message || "Couldn't save that change — please try again.");
      }
    }
  };
  const deleteUpload = async (id) => {
    if (mode === "api") await api.deleteUpload(id);
    setUploads(prev => prev.filter(u => u.id !== id));
  };

  // Messages
  const addMessage = async (m) => {
    if (mode === "api") {
      await api.createMessage({ name: m.name, email: m.email, message: m.message, website: m.website || "" });
      // Don't re-fetch all messages publicly; just keep local copy off public side.
    } else {
      setMessages(prev => [m, ...prev]);
    }
  };
  const deleteMessage = async (id) => {
    if (mode === "api") await api.deleteMessage(id);
    setMessages(prev => prev.filter(m => m.id !== id));
  };

  // [HB-ADD] -------- Site content (editable blocks + images) --------
  const setContent = async (key, value, imageFile) => {
    if (mode !== "api") { toast("Content editing works on the live site only."); return; }
    const fd = new FormData();
    if (value !== undefined) fd.append("value", value);
    if (imageFile) fd.append("image", await shrinkImageForUpload(imageFile));
    const saved = await api.putContent(key, fd);
    setSiteContent(prev => ({ ...prev, [key]: saved }));
    return saved;
  };
  const clearContent = async (key) => {
    if (mode !== "api") return;
    await api.req(`/api/content/${encodeURIComponent(key)}`, { method: "DELETE" });
    setSiteContent(prev => {
      const next = { ...prev };
      delete next[key];
      return next;
    });
  };

  // [HB-ADD] -------- Blog --------
  const addBlogPost = async (form) => {
    const saved = await api.createBlogPost(form);
    setAllBlogPosts(prev => [saved, ...prev]);
    if (saved.published) setBlogPosts(prev => [saved, ...prev]);
    return saved;
  };
  const updateBlogPost = async (id, form) => {
    const saved = await api.updateBlogPost(id, form);
    setAllBlogPosts(prev => prev.map(p => p.id === id ? saved : p));
    setBlogPosts(prev => {
      const without = prev.filter(p => p.id !== id);
      return saved.published ? [saved, ...without].sort((a, b) => b.createdAt - a.createdAt) : without;
    });
    return saved;
  };
  const deleteBlogPost = async (id) => {
    await api.deleteBlogPost(id);
    setAllBlogPosts(prev => prev.filter(p => p.id !== id));
    setBlogPosts(prev => prev.filter(p => p.id !== id));
  };

  // [HB-ADD] -------- Admin settings + counter --------
  const setSetting = async (key, value) => {
    await api.putSetting(key, value);
    setAdminSettings(prev => ({ ...prev, [key]: value }));
  };
  const resetCounter = async () => {
    await api.resetCounter();
    await refreshCounter();
  };

  // [HB-ADD] Announcement bar save (admin) — direct fetch, version-proof.
  const saveAnnouncement = async (next) => {
    const merged = { ...HB_ANNOUNCEMENT_DEFAULT, ...next };
    if (mode === "api") {
      const r = await fetch("/api/settings/announcement", {
        method: "PUT",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(merged),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(data.error || "Could not save announcement.");
      setAnnouncement({ ...HB_ANNOUNCEMENT_DEFAULT, ...data });
      return data;
    }
    try { localStorage.setItem("hb_announcement_v1", JSON.stringify(merged)); } catch (e) {}
    setAnnouncement(merged);
    return merged;
  };

  // -------- Customer accounts --------
  // These need the server (password hashing + email), so in demo mode they
  // surface a friendly notice rather than faking auth in localStorage.
  const DEMO_ACCOUNT_MSG = "Customer accounts run on the live site. This is a design preview.";
  const accountRegister = async (data) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    const c = await api.accountRegister(data);
    setCustomer(c);
    return c;
  };
  const accountLogin = async (email, password) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    const c = await api.accountLogin(email, password);
    setCustomer(c);
    return c;
  };
  const accountLogout = async () => {
    if (mode === "api") { try { await api.accountLogout(); } catch (e) {} }
    setCustomer(null);
    location.hash = "#/";
  };
  const accountUpdate = async (patch) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    const c = await api.accountUpdate(patch);
    setCustomer(c);
    return c;
  };
  const accountForgot = async (email) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    return api.accountForgot(email);
  };
  const accountReset = async (token, password) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    return api.accountReset(token, password);
  };
  const accountDelete = async (password) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    await api.accountDelete(password);
    setCustomer(null);
    location.hash = "#/";
  };
  const accountOrders = async () => {
    if (mode !== "api") return [];
    return api.accountOrders();
  };
  const accountInvoices = async () => {
    if (mode !== "api") return [];
    return api.accountInvoices();
  };
  // Customer signs off on (or requests changes to) a proof. Returns the updated
  // status so the caller can refresh that order.
  const accountProofDecision = async (orderId, decision, feedback) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    return api.accountProofDecision(orderId, decision, feedback);
  };
  // Customer re-places a past order (server clones it + its design files).
  const accountReorder = async (orderId, body) => {
    if (mode !== "api") throw new Error(DEMO_ACCOUNT_MSG);
    return api.accountReorder(orderId, body);
  };
  // Admin: send (or replace) the customer proof for an order. `file` is a File.
  // `meta` optionally carries { stitchCount, threadColors } shown to the customer.
  const sendProof = async (id, file, meta = {}) => {
    if (mode !== "api") { toast("Sending proofs needs the live site."); return; }
    const fd = new FormData();
    fd.append("proof", file);
    if (meta.stitchCount != null && meta.stitchCount !== "") fd.append("stitchCount", String(meta.stitchCount));
    if (Array.isArray(meta.threadColors)) fd.append("threadColors", JSON.stringify(meta.threadColors));
    const saved = await api.uploadProof(id, fd);
    setUploads(prev => prev.map(u => u.id === id ? saved : u));
    return saved;
  };

  // Admin auth
  const adminLogin = async (password) => {
    if (mode === "api") {
      await api.login(password);
      setAdminAuthed(true);
      await refreshAdminData();
      return true;
    }
    // Demo mode: hardcoded passcode
    if (String(password || "").trim().toLowerCase() === "hazel") {
      try { sessionStorage.setItem("hb_admin_authed", "1"); } catch (e) {}
      setAdminAuthed(true);
      return true;
    }
    throw new Error("Incorrect password.");
  };
  const adminLogout = async () => {
    if (mode === "api") {
      try { await api.logout(); } catch (e) {}
    } else {
      try { sessionStorage.removeItem("hb_admin_authed"); } catch (e) {}
    }
    setAdminAuthed(false);
    setUploads([]);
    setMessages([]);
    setAdminCustomers([]);
    location.hash = "#/";
  };

  // Demo helpers (only meaningful in demo mode)
  const seedDemoUpload = () => {
    if (mode === "api") {
      toast("Demo seed not available in live mode — real uploads will appear here.");
      return;
    }
    const record = {
      ...SEED_UPLOAD_TEMPLATE,
      id: uid() + "-" + uid(),
      ref: makeOrderRef(),
      createdAt: Date.now(),
      deadline: new Date(Date.now() + 1000 * 60 * 60 * 24 * 24).toISOString().slice(0, 10),
    };
    setUploads(prev => [record, ...prev]);
    toast("Seeded a demo upload.");
  };
  const resetAll = () => {
    if (mode === "api") {
      toast("Reset not available in live mode — use admin actions instead.");
      return;
    }
    setUploads([]); setMessages([]); setItems(SEED_ITEMS);
    toast("Demo data reset.");
  };

  useEffect(() => {
    window.__hb_seedDemoUpload = seedDemoUpload;
    window.__hb_reset = resetAll;
  });

  const ctx = {
    mode, items, uploads, messages, tweaks, adminAuthed,
    customer, accountRegister, accountLogin, accountLogout, accountUpdate, accountForgot, accountReset, accountDelete, accountOrders, accountInvoices, accountProofDecision, accountReorder, accountLogos, addLogo, renameLogo, deleteLogo,
    accountAddresses, addAddress, updateAddress, deleteAddress, // [HB-ADD] shipping address book
    announcement, saveAnnouncement, // [HB-ADD]
    siteContent, setContent, clearContent, // [HB-ADD]
    blogPosts, allBlogPosts, addBlogPost, updateBlogPost, deleteBlogPost, // [HB-ADD]
    faqs, refreshFaqs, // [HB-ADD] FAQ
    storeStock, refreshStoreStock, // [HB-ADD] in-store inventory
    cart, addToCart, setCartQty, removeFromCart, clearCart, // [HB-ADD] shopping cart
    adminStoreOrders, refreshStoreOrders, // [HB-ADD] store orders (admin)
    counter, refreshCounter, resetCounter, adminSettings, setSetting, // [HB-ADD]
    adminCustomers, refreshCustomers, // [HB-ADD] customer accounts (admin)
    addUpload, updateUpload, deleteUpload, sendProof,
    addItem, updateItem, deleteItem,
    addMessage, deleteMessage,
    seedDemoUpload, resetAll,
    adminLogin, adminLogout,
    toast,
    theme, toggleTheme, // [HB-ADD] light/dark
  };

  // Route segments: /, /shop, /upload, /about, /contact, /admin, /legal/*  [HB-ADD legal]
  const path = route.split("?")[0];
  let page;
  if (path.startsWith("/legal") && typeof LegalPage !== "undefined") {
    // [HB-ADD] guarded: only routes here if pages-legal.jsx is loaded
    page = <LegalPage slug={path.split("/")[2] || "terms"} />;
  } else if (path.startsWith("/guest") && typeof GuestTrackPage !== "undefined") {
    // [HB-ADD] secure tokenised guest order tracking (#/guest?token=…)
    page = <GuestTrackPage />;
  } else if (path.startsWith("/floor") && typeof FloorApp !== "undefined") {
    // [HB-ADD] operator floor sign-in + live board (#/floor)
    page = <FloorApp />;
  } else if (path.startsWith("/blog/") && typeof BlogPostPage !== "undefined") {
    // [HB-ADD] single blog post by slug
    page = <BlogPostPage slug={decodeURIComponent(path.split("/")[2] || "")} />;
  } else if (path === "/blog" && typeof BlogPage !== "undefined") {
    page = <BlogPage />;
  } else if (path === "/faq" && typeof FAQPage !== "undefined") {
    page = <FAQPage />;
  } else if (path.startsWith("/account") && typeof AccountRouter !== "undefined") {
    // Customer area: /account, /account/login, /account/register,
    // /account/forgot, /account/reset (reset reads ?token= from the hash).
    page = <AccountRouter sub={path.split("/")[2] || ""} />;
  } else switch (path) {
    case "/":        page = <HomePage />;    break;
    case "/shop":    page = <ShopPage />;    break;
    case "/blanks":  page = (typeof BlanksPage !== "undefined") ? <BlanksPage /> : <ShopPage />; break;
    case "/cart":    page = <CartPage />;    break;
    case "/upload":  page = <UploadPage />;  break;
    case "/quote":   page = <QuotePage />;   break;
    case "/thank-you": page = <ThankYouPage />; break;
    case "/about":   page = <AboutPage />;   break;
    case "/contact": page = <ContactPage />; break;
    case "/admin":   page = <AdminPage />;   break;
    default:         page = <HomePage />;
  }

  // While checking mode, briefly show the page anyway (items will fill in).
  return (
    <AppCtx.Provider value={ctx}>
      <NavBar route={route} go={go} />
      {page}
      <Footer />
      <Toast msg={toastMsg} onDone={() => setToastMsg("")} />
      <HazelbelleTweaks tweaks={tweaks} setTweak={setTweak} />
    </AppCtx.Provider>
  );
}

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