// Central state store — products / orders / prebookings / customers live
// in Firestore. Cashier writes, kitchen/waitress/display subscribe in real time.

const POSStore = React.createContext(null);
const db = tenantDb(firebase.firestore());
const auth = firebase.auth();
const sv = firebase.firestore.FieldValue.serverTimestamp;
const inc = firebase.firestore.FieldValue.increment;

function POSProvider({ children }) {
  const [user, setUser]               = React.useState(null);
  const [role, setRole]               = React.useState(null);
  const [authReady, setAuthReady]     = React.useState(false);
  const [products, setProducts]       = React.useState([]);
  const [ingredients, setIngredients] = React.useState([]);
  const [orders, setOrders]           = React.useState([]);
  const [prebookings, setPrebookings] = React.useState([]);
  const [customers, setCustomers]     = React.useState([]);
  const [currentShift, setCurrentShift] = React.useState(null);
  const [activeRole, setActiveRole]   = React.useState("cashier");
  const [receiptOrder, setReceiptOrder] = React.useState(null);
  const [toast, setToast]             = React.useState(null);

  // Auth gate + role lookup
  React.useEffect(() => {
    return auth.onAuthStateChanged(async (u) => {
      if (!u) { setUser(null); setRole(null); setAuthReady(true); return; }
      try {
        const snap = await db.collection("users").doc(u.uid).get();
        const r = snap.exists ? snap.data().role : null;
        setUser(u); setRole(r);
        if (r === "kitchen") setActiveRole("kitchen");
        else if (r === "waitress") setActiveRole("waitress");
        else if (r === "display") setActiveRole("display");
        else if (r === "admin") setActiveRole("admin");
        else setActiveRole("cashier");
      } catch (e) { setRole(null); }
      setAuthReady(true);
    });
  }, []);

  // Live settings (theme colors + QRIS payload). Public-readable docs so the
  // customer display and prebook page get them too without auth.
  React.useEffect(() => {
    const unsubTheme = db.collection("settings").doc("theme").onSnapshot((doc) => {
      if (!doc.exists) return;
      const colors = doc.data().colors || {};
      Object.entries(colors).forEach(([k, v]) => {
        if (typeof v === "string") document.documentElement.style.setProperty(`--${k}`, v);
      });
    });
    const unsubQris = db.collection("settings").doc("qris").onSnapshot((doc) => {
      if (!doc.exists) return;
      const sp = doc.data().staticPayload;
      if (sp && window.QRIS_CONFIG) window.QRIS_CONFIG.staticPayload = sp;
    });
    const unsubGeneral = db.collection("settings").doc("general").onSnapshot((doc) => {
      if (!doc.exists) return;
      const g = doc.data();
      if (g.timezone) window.APP_TZ = g.timezone;
      // Store profile (Admin → Settings → Store profile) for receipts.
      window.STORE_TAGLINE = g.tagline || "";
      window.STORE_FOOTER = Array.isArray(g.receiptFooter) ? g.receiptFooter : null;
    });
    return () => { unsubTheme(); unsubQris(); unsubGeneral(); };
  }, []);

  // Firestore subscriptions (only after auth)
  React.useEffect(() => {
    if (!user) { setProducts([]); setIngredients([]); setOrders([]); setPrebookings([]); setCustomers([]); setCurrentShift(null); return; }
    // Display role (customer-facing kiosk) only needs products. Skip the
    // staff-scoped collections entirely — Firestore rules now deny reads
    // on orders/prebookings/customers/shifts for this role, so attempting
    // those subscriptions would noisily fail with permission-denied.
    if (role === 'display') {
      const unsub = db.collection("products").onSnapshot((s) =>
        setProducts(s.docs.map((d) => ({ id: d.id, ...d.data() }))));
      return () => unsub();
    }
    const today = dayKey();
    // Orders come from two queries: today's orders (by dateKey) plus any
    // still-open bills regardless of date. Merging keeps yesterday's
    // unpaid bills visible after midnight or a deploy-triggered reload.
    let todaysOrders = [];
    let openBillOrders = [];
    const mergeOrders = () => {
      const byId = new Map();
      for (const o of todaysOrders) byId.set(o.id, o);
      for (const o of openBillOrders) byId.set(o.id, o);
      setOrders(Array.from(byId.values())
        .sort((a, b) => (b.createdAt?.seconds || 0) - (a.createdAt?.seconds || 0)));
    };
    const unsubs = [
      db.collection("products").onSnapshot((s) =>
        setProducts(s.docs.map((d) => ({ id: d.id, ...d.data() })))),
      db.collection("ingredients").onSnapshot((s) =>
        setIngredients(s.docs.map((d) => ({ id: d.id, ...d.data() })))),
      db.collection("orders").where("dateKey", "==", today).onSnapshot((s) => {
        todaysOrders = s.docs.map((d) => ({ id: d.id, ...d.data() }));
        mergeOrders();
      }),
      db.collection("orders").where("paid", "==", false).onSnapshot((s) => {
        openBillOrders = s.docs.map((d) => ({ id: d.id, ...d.data() }));
        mergeOrders();
      }),
      db.collection("prebookings").onSnapshot((s) =>
        setPrebookings(s.docs
          .map((d) => ({ id: d.id, ...d.data() }))
          .sort((a, b) => (b.createdAt?.seconds || 0) - (a.createdAt?.seconds || 0)))),
      db.collection("customers").onSnapshot((s) =>
        setCustomers(s.docs.map((d) => ({ id: d.id, ...d.data() })))),
      db.collection("shifts")
        .where("cashierUid", "==", user.uid)
        .where("status", "==", "open")
        .onSnapshot((s) => {
          setCurrentShift(s.empty ? null : { id: s.docs[0].id, ...s.docs[0].data() });
        }),
    ];
    return () => unsubs.forEach((u) => u && u());
  }, [user, role]);

  const openShift = async ({ openingCash }) => {
    const today = dayKey();
    await db.collection("shifts").add({
      cashierUid: user.uid,
      cashierEmail: user.email,
      openingCash: Number(openingCash) || 0,
      openedAt: sv(),
      status: "open",
      dateKey: today,
    });
    showToast("Shift opened");
  };

  const closeShift = async ({ closingCash, expectedCash, cashSales, refunds }) => {
    if (!currentShift) return;
    await db.collection("shifts").doc(currentShift.id).update({
      closingCash: Number(closingCash) || 0,
      expectedCash: Number(expectedCash) || 0,
      cashSales: Number(cashSales) || 0,
      refunds: Number(refunds) || 0,
      variance: (Number(closingCash) || 0) - (Number(expectedCash) || 0),
      closedAt: sv(),
      status: "closed",
    });
    showToast("Shift closed");
  };

  // Refund: store a refund doc and stamp the order with the refunded amount/items.
  // Wrapped in a Firestore transaction so two concurrent refunds on the same
  // order can't both base off refundedAmount=0 and over-refund. Customer
  // loyalty + stock restore run after via atomic FieldValue.increment writes.
  const refundOrder = async (order, { items, reason, method }) => {
    const refundTotal = items.reduce((s, i) => s + (i.price + (i.modPrice || 0)) * i.qty, 0);
    const orderRef = db.collection("orders").doc(order.id);
    const refundRef = db.collection("refunds").doc();

    try {
      await db.runTransaction(async (tx) => {
        const orderSnap = await tx.get(orderRef);
        if (!orderSnap.exists) throw new Error("order not found");
        const cur = orderSnap.data();
        const already = Number(cur.refundedAmount) || 0;
        const orderTotal = Number(cur.total) || 0;
        const newRefunded = already + refundTotal;
        if (newRefunded > orderTotal) {
          throw new Error("refund would exceed order total");
        }
        const isFull = newRefunded >= orderTotal;

        tx.set(refundRef, {
          orderId: order.id,
          orderShortId: order.shortId || null,
          customer: order.customer || null,
          items,
          total: refundTotal,
          method: method || (order.paymentMethod || "cash"),
          reason: reason || "",
          cashierUid: user?.uid || null,
          cashierEmail: user?.email || null,
          dateKey: dayKey(),
          createdAt: sv(),
        });
        tx.update(orderRef, {
          refundedAmount: newRefunded,
          refundedAt: sv(),
          status: isFull ? "refunded" : (cur.status || "open"),
        });
      });
    } catch (e) {
      console.warn("Refund failed:", e);
      showToast("Refund failed: " + (e?.message || "unknown"));
      return;
    }

    if (order.customer && order.customer !== "Walk-in") {
      const slug = order.customer.toLowerCase().replace(/\s+/g, "_");
      await db.collection("customers").doc(slug).set({
        totalSpent: inc(-refundTotal),
        points: inc(-Math.floor(refundTotal / 10000)),
      }, { merge: true });
    }
    // Restore stock for each refunded item (skip synthetic items with no id).
    // Atomic increment — race-safe across concurrent sales/refunds. Recipe/
    // combo items (item.recipeSnapshot present) restore their components per
    // the snapshot captured at *sale* time, never a live re-read of
    // product.recipe — a recipe/combo edited after the sale must not change
    // how an already-placed order gets refunded. A combo component is
    // itself a product, so it shares the same `products` collection (and
    // possibly the same doc) as a directly-sold item — see decrementStock's
    // header comment for why those two are merged into one stock write with
    // up to two separate stock_movements entries. Every touched
    // product/ingredient gets a stock_movements entry in the same batch.
    const directProductTotals = {}; // id -> qty (sold directly)
    const comboProductTotals = {};  // id -> { qty, name } (restored as a combo component)
    const ingredientTotals = {};    // id -> { qty, name }
    items.forEach((i) => {
      if (!i) return;
      if (Array.isArray(i.recipeSnapshot) && i.recipeSnapshot.length) {
        i.recipeSnapshot.forEach((r) => {
          if (!r) return;
          const targetType = r.targetType || "ingredient"; // legacy lines (pre-combo) were always an ingredient
          const targetId = r.targetId || r.ingredientId;
          if (!targetId) return;
          const qty = (Number(r.qty) || 0) * (Number(i.qty) || 0);
          const bucket = targetType === "product" ? comboProductTotals : ingredientTotals;
          if (!bucket[targetId]) bucket[targetId] = { qty: 0, name: r.targetName || r.ingredientName || "" };
          bucket[targetId].qty += qty;
        });
      } else if (i.id) {
        directProductTotals[i.id] = (directProductTotals[i.id] || 0) + (Number(i.qty) || 0);
      }
    });
    const productIds = Array.from(new Set([...Object.keys(directProductTotals), ...Object.keys(comboProductTotals)]));
    const ingredientIds = Object.keys(ingredientTotals);
    if (productIds.length || ingredientIds.length) {
      try {
        const [productSnaps, ingredientSnaps] = await Promise.all([
          Promise.all(productIds.map((id) => db.collection("products").doc(id).get())),
          Promise.all(ingredientIds.map((id) => db.collection("ingredients").doc(id).get())),
        ]);
        const batch = db.batch();
        let touched = 0;
        const dateKey = dayKey();
        productSnaps.forEach((snap, idx) => {
          if (!snap.exists) return;
          const p = snap.data();
          const oldStock = Number(p.stock) || 0;
          if (oldStock >= 999) return; // unlimited — leave alone
          const id = productIds[idx];
          const direct = directProductTotals[id] || 0;
          const combo = (comboProductTotals[id] && comboProductTotals[id].qty) || 0;
          const total = direct + combo;
          if (!total) return;
          batch.update(db.collection("products").doc(id), { stock: inc(total) });
          let running = oldStock;
          if (direct) {
            running += direct;
            batch.set(db.collection("stock_movements").doc(), {
              targetType: "product", targetId: id, targetName: p.name || "",
              qtyChange: direct, qtyAfter: running, reason: "refund", note: reason || "",
              unitCost: null, refId: order.id,
              userEmail: user?.email || null, userUid: user?.uid || null,
              dateKey, createdAt: sv(),
            });
          }
          if (combo) {
            running += combo;
            batch.set(db.collection("stock_movements").doc(), {
              targetType: "product", targetId: id, targetName: p.name || "",
              qtyChange: combo, qtyAfter: running, reason: "recipe_consumption_refund", note: reason || "",
              unitCost: null, refId: order.id,
              userEmail: user?.email || null, userUid: user?.uid || null,
              dateKey, createdAt: sv(),
            });
          }
          touched++;
        });
        ingredientSnaps.forEach((snap, idx) => {
          if (!snap.exists) return;
          const oldStock = Number(snap.data().stock) || 0;
          if (oldStock >= 999) return; // unlimited — leave alone
          const id = ingredientIds[idx];
          const qty = ingredientTotals[id].qty;
          if (!qty) return;
          batch.update(db.collection("ingredients").doc(id), { stock: inc(qty) });
          batch.set(db.collection("stock_movements").doc(), {
            targetType: "ingredient", targetId: id, targetName: snap.data().name || ingredientTotals[id].name,
            qtyChange: qty, qtyAfter: oldStock + qty, reason: "recipe_consumption_refund", note: reason || "",
            unitCost: null, refId: order.id,
            userEmail: user?.email || null, userUid: user?.uid || null,
            dateKey, createdAt: sv(),
          });
          touched++;
        });
        if (touched) await batch.commit();
      } catch (e) {
        console.warn("Stock restore failed:", e);
      }
    }
    showToast("Refunded " + (refundTotal ? "Rp " + refundTotal.toLocaleString("id-ID") : ""));
  };

  // Actions
  const showToast = (msg) => {
    const t = { msg, ts: Date.now() };
    setToast(t);
    setTimeout(() => setToast((cur) => (cur && cur.ts === t.ts ? null : cur)), 2600);
  };

  // Decrement stock for items sold. Three kinds of demand, all aggregated
  // across the whole order first:
  //  - plain items: decrement products/{id}.stock directly (original
  //    behavior).
  //  - recipe/combo items (item.recipeSnapshot present, captured at sale
  //    time by cashier.jsx's toItem — never re-read live here): decrement
  //    each component instead — ingredients/{id}.stock for an ingredient
  //    line, products/{id}.stock for a *combo* line (a package made of
  //    other sellable products).
  // A combo component is itself a product, so the same product id can show
  // up in both "sold directly" and "consumed as a combo component" within
  // one order (someone orders the item a la carte AND a combo containing
  // it) — those two demands are merged into a single stock write per
  // product (Firestore batch can't update the same doc twice), but still
  // logged as up to two separate stock_movements entries (reason "sale" vs
  // "recipe_consumption") so the audit trail stays legible.
  // Stock ≥ 999 is treated as unlimited and skipped (admin convention).
  // A low-stock notification fires when a product/ingredient crosses its
  // threshold downwards.
  //
  // Writes use FieldValue.increment(-qty) so two concurrent sales of the last
  // unit can't both write stock=0 (oversell). The unlimited check and the
  // threshold-crossing computation still read the cached value first — those
  // are best-effort (worst case: one missed notification under concurrent
  // sales racing past the threshold); the stock value itself stays correct.
  const decrementStock = async (items, orderId) => {
    if (!Array.isArray(items) || !items.length) return;
    const directProductTotals = {}; // id -> qty (sold directly)
    const comboProductTotals = {};  // id -> { qty, name } (consumed as a combo component)
    const ingredientTotals = {};    // id -> { qty, name }
    items.forEach((i) => {
      if (!i) return;
      if (Array.isArray(i.recipeSnapshot) && i.recipeSnapshot.length) {
        i.recipeSnapshot.forEach((r) => {
          if (!r) return;
          const targetType = r.targetType || "ingredient"; // legacy lines (pre-combo) were always an ingredient
          const targetId = r.targetId || r.ingredientId;
          if (!targetId) return;
          const qty = (Number(r.qty) || 0) * (Number(i.qty) || 0);
          const bucket = targetType === "product" ? comboProductTotals : ingredientTotals;
          if (!bucket[targetId]) bucket[targetId] = { qty: 0, name: r.targetName || r.ingredientName || "" };
          bucket[targetId].qty += qty;
        });
      } else if (i.id) {
        directProductTotals[i.id] = (directProductTotals[i.id] || 0) + (Number(i.qty) || 0);
      }
    });
    const productIds = Array.from(new Set([...Object.keys(directProductTotals), ...Object.keys(comboProductTotals)]));
    const ingredientIds = Object.keys(ingredientTotals);
    if (!productIds.length && !ingredientIds.length) return;
    try {
      const [productSnaps, ingredientSnaps] = await Promise.all([
        Promise.all(productIds.map((id) => db.collection("products").doc(id).get())),
        Promise.all(ingredientIds.map((id) => db.collection("ingredients").doc(id).get())),
      ]);
      const batch = db.batch();
      const notifs = [];
      let touched = 0;
      const dateKey = dayKey();
      productSnaps.forEach((snap, idx) => {
        if (!snap.exists) return;
        const p = snap.data();
        const oldStock = Number(p.stock) || 0;
        if (oldStock >= 999) return; // unlimited
        const id = productIds[idx];
        const direct = directProductTotals[id] || 0;
        const combo = (comboProductTotals[id] && comboProductTotals[id].qty) || 0;
        const qty = direct + combo;
        if (!qty) return;
        batch.update(db.collection("products").doc(id), { stock: inc(-qty) });
        let running = oldStock;
        if (direct) {
          running -= direct;
          batch.set(db.collection("stock_movements").doc(), {
            targetType: "product", targetId: id, targetName: p.name || "",
            qtyChange: -direct, qtyAfter: running, reason: "sale", note: "",
            unitCost: null, refId: orderId || null,
            userEmail: user?.email || null, userUid: user?.uid || null,
            dateKey, createdAt: sv(),
          });
        }
        if (combo) {
          running -= combo;
          batch.set(db.collection("stock_movements").doc(), {
            targetType: "product", targetId: id, targetName: p.name || "",
            qtyChange: -combo, qtyAfter: running, reason: "recipe_consumption", note: "",
            unitCost: null, refId: orderId || null,
            userEmail: user?.email || null, userUid: user?.uid || null,
            dateKey, createdAt: sv(),
          });
        }
        touched++;
        // Default threshold when unset: 5. Products without an explicit
        // stockThreshold value still get "low stock" alerts at 5 remaining.
        const threshold = Number(p.stockThreshold) || 5;
        const projectedNewStock = oldStock - qty;
        // Notify on either: crossing the threshold, OR hitting zero.
        const crossedThreshold = projectedNewStock <= threshold && oldStock > threshold;
        const hitZero = projectedNewStock <= 0 && oldStock > 0;
        if (crossedThreshold || hitZero) {
          notifs.push({ name: p.name, sku: p.sku || "", stock: projectedNewStock, threshold });
        }
      });
      ingredientSnaps.forEach((snap, idx) => {
        if (!snap.exists) return;
        const ing = snap.data();
        const oldStock = Number(ing.stock) || 0;
        if (oldStock >= 999) return; // unlimited
        const id = ingredientIds[idx];
        const qty = ingredientTotals[id].qty;
        if (!qty) return;
        batch.update(db.collection("ingredients").doc(id), { stock: inc(-qty) });
        batch.set(db.collection("stock_movements").doc(), {
          targetType: "ingredient", targetId: id, targetName: ing.name || ingredientTotals[id].name,
          qtyChange: -qty, qtyAfter: oldStock - qty, reason: "recipe_consumption", note: "",
          unitCost: null, refId: orderId || null,
          userEmail: user?.email || null, userUid: user?.uid || null,
          dateKey, createdAt: sv(),
        });
        touched++;
        const threshold = Number(ing.stockThreshold) || 5;
        const projectedNewStock = oldStock - qty;
        const crossedThreshold = projectedNewStock <= threshold && oldStock > threshold;
        const hitZero = projectedNewStock <= 0 && oldStock > 0;
        if (crossedThreshold || hitZero) {
          notifs.push({ name: ing.name, sku: ing.sku || "", stock: projectedNewStock, threshold });
        }
      });
      if (touched) await batch.commit();
      notifs.forEach((n) => {
        fetch("/api/notify-low-stock", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(n),
        }).catch(() => {});
      });
    } catch (e) {
      console.warn("Stock decrement failed:", e);
    }
  };

  const placeOrder = async (order) => {
    const dateKey = dayKey();
    const paid = order.paid !== false; // default true; explicit false for open bills
    const payload = {
      shortId:      order.shortId,
      table:        order.table || "T",
      customer:     order.customer || "Walk-in",
      items:        order.items.map((i) => ({
        id: i.id, sku: i.sku || "", name: i.name, price: i.price, qty: i.qty,
        station: i.station, mods: i.mods || [], modPrice: i.modPrice || 0,
        cogs: i.cogs || 0, notes: i.notes || "",
        status: i.status || "new",
        ...(Array.isArray(i.recipeSnapshot) && i.recipeSnapshot.length ? { recipeSnapshot: i.recipeSnapshot } : {}),
      })),
      subtotal:     order.subtotal,
      depositApplied: order.depositApplied || 0,
      uniqueCode:   order.uniqueCode || 0,
      total:        order.total,
      tip:          order.tip || 0,
      payments:     order.payments || [],
      paymentMethod: order.paymentMethod || null,
      status:       "open",
      paid,
      barStatus:    order.barStatus,
      kitchenStatus: order.kitchenStatus,
      prebookingId: order.prebookingId || null,
      cashierUid:   user?.uid || null,
      cashierEmail: user?.email || null,
      dateKey,
      createdAt:    sv(),
    };
    const ref = await db.collection("orders").add(payload);
    if (paid) setReceiptOrder({ ...payload, id: ref.id, createdAt: Date.now() });
    showToast(paid ? `Order ${order.shortId} placed` : `Bill ${order.shortId} saved (open)`);

    // Fire-and-forget stock decrement — don't block the receipt on this.
    decrementStock(payload.items, ref.id);

    if (paid && payload.customer && payload.customer !== "Walk-in") {
      const slug = payload.customer.toLowerCase().replace(/\s+/g, "_");
      await db.collection("customers").doc(slug).set({
        name: payload.customer,
        lastVisit: sv(),
        totalSpent: inc(payload.total),
        visits: inc(1),
        points: inc(Math.floor(payload.total / 10000)),
      }, { merge: true });
    }
    return ref.id;
  };

  // Update an existing order (used when paying an open bill or editing items).
  const updateOrder = async (orderId, patch) => {
    const order = orders.find((o) => o.id === orderId);
    await db.collection("orders").doc(orderId).update(patch);
    if (patch.paid && order) {
      setReceiptOrder({ ...order, ...patch, id: orderId });
      if (order.customer && order.customer !== "Walk-in") {
        const slug = order.customer.toLowerCase().replace(/\s+/g, "_");
        await db.collection("customers").doc(slug).set({
          name: order.customer,
          lastVisit: sv(),
          totalSpent: inc(patch.total ?? order.total ?? 0),
          visits: inc(1),
          points: inc(Math.floor((patch.total ?? order.total ?? 0) / 10000)),
        }, { merge: true });
      }
      showToast("Bill paid · receipt ready");
    }
  };

  const advance = async (orderId, station, nextStatus) => {
    const key = station === "bar" ? "barStatus" : "kitchenStatus";
    await db.collection("orders").doc(orderId).update({ [key]: nextStatus });
  };

  // Per-item status advance. Updates the `status` field of one item (or many,
  // pass an array of indices) in the order and stamps the transition time.
  // When every item has reached "served", auto-closes the order.
  const advanceItem = async (orderId, indices, nextStatus) => {
    const order = orders.find((o) => o.id === orderId);
    if (!order || !Array.isArray(order.items)) return;
    const idxSet = new Set(Array.isArray(indices) ? indices : [indices]);
    const now = Date.now();
    const nextItems = order.items.map((it, i) => {
      if (!idxSet.has(i)) return it;
      const patch = { status: nextStatus };
      if (nextStatus === "cooking") patch.cookingAt = now;
      if (nextStatus === "ready")   patch.readyAt   = now;
      if (nextStatus === "served")  patch.servedAt  = now;
      return { ...it, ...patch };
    });
    const allServed = nextItems.every((it) => (it.status || "new") === "served");
    const update = { items: nextItems };
    if (allServed) update.status = "closed";
    else if (order.status === "closed") update.status = "open";
    await db.collection("orders").doc(orderId).update(update);
  };

  // Rewind a single item one step backwards (served→ready→cooking→new).
  // Used from the history sheet to undo accidental status presses by crew.
  // Clears the timestamp of the status we're leaving and reopens the order
  // if it was auto-closed.
  const rewindItem = async (orderId, itemIndex) => {
    const order = orders.find((o) => o.id === orderId);
    if (!order || !Array.isArray(order.items)) return;
    const it = order.items[itemIndex];
    if (!it) return;
    const current = it.status || "new";
    const PREV = { served: "ready", ready: "cooking", cooking: "new" };
    const prev = PREV[current];
    if (!prev) return; // can't rewind from "new"
    const nextItems = order.items.map((x, i) => {
      if (i !== itemIndex) return x;
      const patch = { ...x, status: prev };
      if (current === "served")  delete patch.servedAt;
      if (current === "ready")   delete patch.readyAt;
      if (current === "cooking") delete patch.cookingAt;
      return patch;
    });
    const allServed = nextItems.every((x) => (x.status || "new") === "served");
    const update = { items: nextItems };
    if (allServed) update.status = "closed";
    else if (order.status === "closed") update.status = "open";
    await db.collection("orders").doc(orderId).update(update);
  };

  const closeOrder = async (orderId) => {
    await db.collection("orders").doc(orderId).update({ status: "closed" });
    showToast("Order served");
  };

  const consumePrebooking = async (pbId) => {
    await db.collection("prebookings").doc(pbId).update({ status: "checked_in" });
  };

  const addPrebooking = async (pb) => {
    const ref = await db.collection("prebookings").add({ ...pb, createdAt: sv() });
    // Fire-and-forget email notification via our own /api/notify-prebook
    // endpoint (the server POSTs to SendGrid). Best-effort — failures are
    // logged but never block the booking.
    try {
      fetch("/api/notify-prebook", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ id: ref.id, ...pb }),
      }).catch(() => {});
    } catch (_) { /* best-effort */ }
    return ref.id;
  };

  const verifyPrebooking = async (pbId) => {
    await db.collection("prebookings").doc(pbId).update({ status: "verified" });
  };

  // Adjust a product's or ingredient's stock from the admin Inventory tab.
  // `mode: "delta"` applies a known +/- change directly (waste, damage,
  // manual correction). `mode: "count"` is a physical recount: pass the
  // counted total and this computes the delta inside a transaction against
  // the freshest stock value, so a sale slipping in between opening the
  // form and submitting doesn't produce a wrong logged delta.
  const adjustStock = async ({ targetType, targetId, mode, value, reason, note }) => {
    const coll = targetType === "ingredient" ? "ingredients" : "products";
    const ref = db.collection(coll).doc(targetId);
    const dateKey = dayKey();
    let notif = null;
    await db.runTransaction(async (tx) => {
      const snap = await tx.get(ref);
      if (!snap.exists) throw new Error("not found");
      const data = snap.data();
      const oldStock = Number(data.stock) || 0;
      const delta = mode === "count" ? (Number(value) || 0) - oldStock : (Number(value) || 0);
      if (!delta) return;
      const newStock = oldStock + delta;
      tx.update(ref, { stock: newStock });
      tx.set(db.collection("stock_movements").doc(), {
        targetType, targetId, targetName: data.name || "",
        qtyChange: delta, qtyAfter: newStock, reason: reason || "adjustment", note: note || "",
        unitCost: null, refId: null,
        userEmail: user?.email || null, userUid: user?.uid || null,
        dateKey, createdAt: sv(),
      });
      // Default threshold when unset: 5 — same convention as decrementStock.
      const threshold = Number(data.stockThreshold) || 5;
      const crossedThreshold = newStock <= threshold && oldStock > threshold;
      const hitZero = newStock <= 0 && oldStock > 0;
      if ((crossedThreshold || hitZero) && newStock < 999) {
        notif = { name: data.name, sku: data.sku || "", stock: newStock, threshold };
      }
    });
    if (notif) {
      fetch("/api/notify-low-stock", {
        method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(notif),
      }).catch(() => {});
    }
    showToast("Stock adjusted");
  };

  // Record a stock receiving (purchase) from a supplier. Every line updates
  // the target's stock AND recomputes its weighted-average cost (avgCost for
  // ingredients, cogs for products) — a genuine read-modify-write, so this
  // runs in one transaction rather than a batch: two concurrent receivings
  // of the same target would otherwise race on the same pre-image and
  // corrupt the average.
  const receiveStock = async ({ supplierId, supplierName, lines, note }) => {
    if (!Array.isArray(lines) || !lines.length) return;
    const dateKey = dayKey();
    const receivingRef = db.collection("receivings").doc();
    await db.runTransaction(async (tx) => {
      const refs = lines.map((l) => db.collection(l.targetType === "ingredient" ? "ingredients" : "products").doc(l.targetId));
      const snaps = await Promise.all(refs.map((r) => tx.get(r)));
      let total = 0;
      snaps.forEach((snap, idx) => {
        if (!snap.exists) throw new Error("target not found: " + lines[idx].targetName);
        const data = snap.data();
        const l = lines[idx];
        const qty = Number(l.qty) || 0;
        const unitCost = Number(l.unitCost) || 0;
        const oldStock = Number(data.stock) || 0;
        const oldAvg = l.targetType === "ingredient" ? (Number(data.avgCost) || 0) : (Number(data.cogs) || 0);
        const newStock = oldStock >= 999 ? oldStock : oldStock + qty; // leave "unlimited" alone
        const newAvg = (oldStock + qty) > 0 ? ((oldStock * oldAvg) + (qty * unitCost)) / (oldStock + qty) : oldAvg;
        const patch = { stock: newStock };
        if (l.targetType === "ingredient") patch.avgCost = newAvg;
        else patch.cogs = newAvg;
        tx.update(refs[idx], patch);
        tx.set(db.collection("stock_movements").doc(), {
          targetType: l.targetType, targetId: l.targetId, targetName: data.name || l.targetName || "",
          qtyChange: qty, qtyAfter: newStock, reason: "receiving", note: note || "",
          unitCost, refId: receivingRef.id,
          userEmail: user?.email || null, userUid: user?.uid || null,
          dateKey, createdAt: sv(),
        });
        total += qty * unitCost;
      });
      tx.set(receivingRef, {
        supplierId: supplierId || null, supplierName: supplierName || "",
        items: lines.map((l) => ({
          targetType: l.targetType, targetId: l.targetId, targetName: l.targetName,
          qty: Number(l.qty) || 0, unitCost: Number(l.unitCost) || 0,
          subtotal: (Number(l.qty) || 0) * (Number(l.unitCost) || 0),
        })),
        total,
        note: note || "",
        userEmail: user?.email || null,
        dateKey, createdAt: sv(),
      });
    });
    showToast("Stock received");
  };

  // Live display mirror — cashier pushes its current cart so customer-display sees it
  const pushLiveCart = async (cart, totals) => {
    await db.collection("displays").doc("main").set({
      items: cart, ...totals, status: cart.length ? "active" : "idle", updatedAt: sv(),
    });
  };

  const value = {
    user, role, authReady,
    products, ingredients, orders, prebookings, customers,
    activeRole, setActiveRole,
    placeOrder, updateOrder, advance, advanceItem, rewindItem, closeOrder, consumePrebooking, addPrebooking, verifyPrebooking,
    decrementStock, adjustStock, receiveStock,
    refundOrder,
    currentShift, openShift, closeShift: closeShift,
    pushLiveCart,
    receiptOrder, setReceiptOrder, toast,
    showToast,
    db, auth, sv, inc,
  };
  return <POSStore.Provider value={value}>{children}</POSStore.Provider>;
}

const usePOS = () => React.useContext(POSStore);

Object.assign(window, { POSStore, POSProvider, usePOS });
