/* ============================================================
   Carzello — auction engine + global store
   - True proxy bidding (eBay model: high bidder = highest max,
     price = 2nd-highest max + increment, capped at winner's max)
   - Simulated competing dealers with hidden proxy maxes
   - Anti-snipe extensions, Buy Now, reserve handling
   - Lifecycle: live → ended → won/lost
   - Persistence (watchlist, purchases, shipTo) via localStorage
   ============================================================ */

const LS_KEY = "sayarah.clear.v1";
const rnd = (a, b) => a + Math.random() * (b - a);
const rndInt = (a, b) => Math.floor(rnd(a, b + 1));
const uid = () => Math.random().toString(36).slice(2, 9);

/* ---- persistence ---- */
function loadPersist() {
  try {
    const raw = localStorage.getItem(LS_KEY);
    if (!raw) return null;
    return JSON.parse(raw);
  } catch (e) { return null; }
}
function savePersist(state) {
  try {
    const payload = {
      watchlist: [...state.watchlist],
      purchases: state.purchases,
      shipTo: state.shipTo,
      notifReadAt: state.notifReadAt,
      customLots: state.customLots,
      lotEdits: state.lotEdits,
      staff: state.staff,
      claims: state.claims,
      dealers: state.dealers,
      consignors: state.consignors,
      settlements: state.settlements,
      wallet: state.wallet,
      savedSearches: state.savedSearches,
      buyer: state.buyer,
      config: state.config,
      outbox: state.outbox,
      freight: state.freight,
      lang: state.lang,
      soldComps: state.soldComps,
    };
    localStorage.setItem(LS_KEY, JSON.stringify(payload));
    FB.save(payload); // async mirror to Firestore (no-op if Firebase is unreachable)
    FB.pushSharedInventory(payload.customLots, payload.lotEdits, payload.consignors, payload.freight); // no-op unless signed in as admin
    if (typeof state.__syncOps === "function") state.__syncOps(); // push changed orders/offers/claims
  } catch (e) {}
}

/* ---- build runtime lot from static data ---- */
function dealerLabel(n) { return "Dealer #" + String(1000 + n).slice(-4); }

function seedLot(raw, now) {
  const inc = raw.increment;
  // Seed competitor maxes so the lot opens with a believable live price.
  const nC = raw.competitors;
  const competitors = [];
  // Top competitor's ceiling sits a bit above start, scaled by "heat".
  const topMax = Math.round((raw.startPrice * (1 + rnd(0.04, 0.06 + raw.heat * 0.16))) / inc) * inc;
  for (let i = 0; i < nC; i++) {
    const factor = 1 - i * rnd(0.02, 0.05);
    const max = Math.max(raw.startPrice, Math.round((topMax * factor) / inc) * inc);
    competitors.push({ id: "c" + i, label: dealerLabel(rndInt(1, 8999)), max, ts: now - rndInt(20, 900) * 1000 });
  }
  let marketValue;
  if (raw.marketValue != null && raw.marketValue !== "") marketValue = Number(raw.marketValue);
  else { const est = estimateFromComps(raw); marketValue = est ? est.value : Math.round(((raw.buyNow || raw.startPrice * 1.22) * 0.97) / 50) * 50; }
  const startsAt = raw.startsInS ? now + raw.startsInS * 1000 : now;
  const lot = {
    ...raw,
    marketValue,
    startsAt,
    endsAt: now + raw.endsInS * 1000,
    originalEndsAt: now + raw.endsInS * 1000,
    status: startsAt > now ? "scheduled" : "live",
    yourMax: null,
    yourTs: null,
    competitors,
    bidCount: rndInt(Math.max(2, nC), nC + rndInt(2, 8)),
    history: [],
    result: null,
    extensions: 0,
    lastBidTs: now - rndInt(10, 120) * 1000,
    highBid: raw.startPrice,
    highBidder: null,
    reserveMet: raw.reserve == null,
  };
  // NAAA structural-damage policy: announced structural damage caps the grade
  const structural = (lot.announcements || []).some(x => !x.ok && /frame|structur/i.test(x.t + " " + x.text));
  if (structural && lot.autograde > 2.0) { lot.autograde = 2.0; lot.structural = true; }
  else if (structural) lot.structural = true;
  resolve(lot, now, false);
  return lot;
}

const DAY_MS = 86400000;
const DEFAULT_CONFIG = {
  commissionTiers: [{ upTo: 15000, pct: 0.05 }, { upTo: 40000, pct: 0.04 }, { upTo: null, pct: 0.03 }],
  commissionMin: 300, commissionCap: 1500,
  freightMargin: 0.12, fxSpread: 0.012,
  paymentWindowH: 48, lateFeePerDay: 50, defaultDays: 5,
  penaltyPct: 0.10, penaltyMin: 800, pickupDays: 7, storagePerDay: 25,
  // marketplace settings
  platformName: "Carzello", defaultIncrement: 250, antiSnipeMin: 2,
  // NAAA online-sale standard: minimum 7 calendar days (sale day = day 1)
  arbitrationWindowH: 168,
  claimMinAmount: 800, claimPctOver50k: 0.02, asIsBelow: 3000, psiExtensionH: 168,
};
function genGatePassCode(id) {
  return "GP-" + String(id).replace(/\D/g, "").slice(-5) + "-" + Math.random().toString(36).slice(2, 6).toUpperCase();
}
function seedPurchase(p, now) {
  const wonTs = now - (p.wonDaysAgo || 0) * DAY_MS;
  const paidTs = p.paidDaysAgo != null ? now - p.paidDaysAgo * DAY_MS : null;
  const pickedUpTs = p.pickedDaysAgo != null ? now - p.pickedDaysAgo * DAY_MS : null;
  const { wonDaysAgo, paidDaysAgo, pickedDaysAgo, ...rest } = p;
  return { ...rest, wonTs, paidTs, pickedUpTs, gatePass: paidTs ? { code: genGatePassCode(p.id), issuedTs: paidTs } : null };
}

/* Core proxy resolution. Returns { changed, flippedAwayFromYou }. */
function resolve(lot, now, record = true) {
  const parts = lot.competitors.map(c => ({ id: c.id, label: c.label, max: c.max, ts: c.ts, you: false }));
  if (lot.yourMax) parts.push({ id: "you", label: "You", max: lot.yourMax, ts: lot.yourTs, you: true });

  const prevBidder = lot.highBidder;
  const prevBid = lot.highBid;

  if (parts.length === 0) {
    lot.highBid = lot.startPrice; lot.highBidder = null;
    lot.reserveMet = lot.reserve == null;
    return { changed: false, flippedAwayFromYou: false };
  }
  parts.sort((a, b) => (b.max - a.max) || (a.ts - b.ts));
  const winner = parts[0];
  let price;
  if (parts.length === 1) {
    price = lot.startPrice;
  } else {
    const second = parts[1];
    price = Math.min(winner.max, second.max + lot.increment);
    price = Math.max(price, lot.startPrice);
  }
  lot.highBid = price;
  lot.highBidder = winner.id;
  lot.reserveMet = lot.reserve == null ? true : price >= lot.reserve;

  const changed = price !== prevBid || winner.id !== prevBidder;
  if (changed && record) {
    lot.history.unshift({
      id: uid(), bidder: winner.label, you: winner.you,
      amount: price, type: winner.you ? "you" : "proxy", ts: now,
    });
    if (lot.history.length > 30) lot.history.length = 30;
    lot.bidCount++;
    lot.lastBidTs = now;
  }
  const flippedAwayFromYou = prevBidder === "you" && winner.id !== "you";
  return { changed, flippedAwayFromYou };
}

/* ============================================================
   The store (external, framework-agnostic) + React binding
   ============================================================ */
const Engine = (function () {
  const now0 = Date.now();
  const persisted = loadPersist();
  /* ids of retired demo records — scrubbed from any previously-persisted state */
  const LEGACY_DEMO_IDS = new Set([
    "SY-47812", "SY-47798", "SY-47755",                       // demo purchases
    "ARB-2031", "ARB-2038", "ARB-2041",                       // demo claims
    "D-1001", "D-1002", "D-1003",                             // demo dealers
    "CG-1001", "CG-1002", "CG-1003", "CG-1004", "CG-1005",    // demo consignors
    "u1", "u2", "u3", "u4", "u5", "u6",                       // demo staff
    "ss1", "ss2", "ss3",                                      // demo saved searches
  ]);
  /* shared inventory (pushed by admin accounts, cached by firebase.jsx) is
     authoritative for team-added lots & edits; local copy is the fallback */
  let sharedInv = null;
  try { sharedInv = JSON.parse(localStorage.getItem("carzello.sharedinv") || "null"); } catch (e) {}
  const lotEdits = { ...(persisted?.lotEdits || {}), ...((sharedInv && sharedInv.lotEdits) || {}) };
  const customLots = (sharedInv && sharedInv.customLots) ? sharedInv.customLots : (persisted?.customLots || []);

  // apply any persisted admin edit to a raw definition before seeding
  function withEdit(raw) { const e = lotEdits[raw.id]; return e ? { ...raw, ...e, grades: { ...raw.grades, ...(e.grades || {}) }, crNotes: { ...raw.crNotes, ...(e.crNotes || {}) } } : raw; }

  const lots = {};
  const rawIndex = {}; // for relisting a defaulted lot
  RAW_LISTINGS.forEach(raw => { lots[raw.id] = seedLot(withEdit(raw), now0); rawIndex[raw.id] = withEdit(raw); });
  customLots.forEach(raw => { lots[raw.id] = seedLot(withEdit(raw), now0); rawIndex[raw.id] = withEdit(raw); });

  let state = {
    lots,
    watchlist: new Set(persisted?.watchlist || []),
    purchases: (persisted?.purchases || [])
      .filter(p => !LEGACY_DEMO_IDS.has(p.id))
      .map(p => ({ wonTs: now0, paidTs: null, pickedUpTs: null, gatePass: null, ...p })),
    notifications: [],
    toasts: [],
    shipTo: persisted?.shipTo || "UAE",
    notifReadAt: persisted?.notifReadAt || 0,
    customLots,
    lotEdits,
    staff: (() => { const s = (persisted?.staff || []).filter(x => !LEGACY_DEMO_IDS.has(x.id)); return s.length ? s : SEED_STAFF.slice(); })(),
    claims: (persisted?.claims || []).filter(c => !LEGACY_DEMO_IDS.has(c.id)),
    dealers: (persisted?.dealers || []).filter(d => !LEGACY_DEMO_IDS.has(d.id)),
    consignors: (persisted?.consignors || []).filter(c => !LEGACY_DEMO_IDS.has(c.id)),
    settlements: persisted?.settlements || [],
    soldComps: persisted?.soldComps || [],       // real comps from completed shared auctions
    listingRequests: [],                          // consignor submissions awaiting team review (ops-synced)
    wallet: { deposit: 0, multiplier: 10 }, // real value comes from the auction server (admin-confirmed deposits)
    savedSearches: (persisted?.savedSearches || []).filter(s => !LEGACY_DEMO_IDS.has(s.id)),
    buyer: (() => {
      const d = { name: "Guest", email: "", phone: "", prefs: { closeAlert: true, outbidPush: true, digest: false }, currency: "USD" };
      const pb = persisted?.buyer || {};
      const merged = { ...d, ...pb, prefs: { ...d.prefs, ...(pb.prefs || {}) } };
      // scrub the pre-auth demo identity — real identity comes from the signed-in account
      if (merged.email === "buyer-demo@sayarah.co" || (merged.name === "Obaidullah Didar" && !merged.email)) {
        merged.name = "Guest"; merged.email = ""; merged.phone = "";
      }
      return merged;
    })(),
    // shallow-merge so an old blob missing the newer payment-term keys still gets defaults (no NaN)
    config: { ...DEFAULT_CONFIG, ...(persisted?.config || {}) },
    outbox: persisted?.outbox || [],
    // freight rates: the admin-published sheet (shared inventory doc) is authoritative
    // for every visitor — otherwise buyers would quote landed cost off stale defaults
    freight: (sharedInv && sharedInv.freight) || persisted?.freight || {
      month: "Default rates", uploadedAt: null, fileName: null,
      rates: SHIP_DEST.reduce((m, d) => { m[d.code] = { default: d.freight, bodies: {} }; return m; }, {}),
    },
    lang: persisted?.lang || "EN",
    offers: [],
    auditLog: [],
    buyingPower: 0, // real value comes from the auction server
    bootTs: now0,
  };

  const listeners = new Set();
  let saveTimer = null;
  function emit() {
    state = { ...state };
    listeners.forEach(l => l());
    if (saveTimer) clearTimeout(saveTimer);
    saveTimer = setTimeout(() => savePersist(state), 400); // trailing debounce
  }
  function emitNoSave() { state = { ...state }; listeners.forEach(l => l()); }
  function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }
  function getState() { return state; }

  /* ---- audit log ---- */
  function audit(actor, action, detail) {
    state.auditLog = [{ id: uid(), ts: Date.now(), actor, action, detail }, ...state.auditLog].slice(0, 120);
  }

  /* ---- buying power: total $ currently committed (fee-loaded), deduped per lot ----
     A single lot can only be bought once, so a live max AND an open offer on the
     same lot count as ONE obligation (the larger). Each active obligation also
     reserves the fixed buyer fees, matching what's owed at settlement. */
  function committed(excludeLotId) {
    if (state.walletCommitted != null && !excludeLotId) return state.walletCommitted; // authoritative server figure
    const perLot = {};
    Object.values(state.lots).forEach(L => {
      if (L.status === "live" && L.yourMax && L.id !== excludeLotId) perLot[L.id] = Math.max(perLot[L.id] || 0, L.yourMax);
    });
    state.offers.forEach(o => {
      if ((o.status === "pending" || o.status === "countered") && o.lotId !== excludeLotId) {
        const amt = o.status === "countered" ? o.counter : o.amount;
        perLot[o.lotId] = Math.max(perLot[o.lotId] || 0, amt);
      }
    });
    let sum = 0;
    Object.values(perLot).forEach(amt => { sum += amt + TOTAL_BUYER_FEES; }); // bid + fixed fees per lot
    state.purchases.forEach(p => { if (p.stage === "payment" && p.status !== "defaulted") sum += p.final + (p.fees || 0); });
    return sum;
  }
  function availablePower() { return Math.max(0, state.buyingPower - committed()); }
  function withinPower(lotId, amount) { return committed(lotId) + amount + TOTAL_BUYER_FEES <= state.buyingPower; }

  /* ---- language / direction ---- */
  const RTL = ["AR", "FA", "PS"];
  function applyDir(code) { try { document.documentElement.lang = code.toLowerCase(); document.documentElement.dir = RTL.includes(code) ? "rtl" : "ltr"; } catch (e) {} }
  function setLang(code) { state.lang = code; applyDir(code); emit(); }
  applyDir(state.lang);

  /* ---- notifications + toasts ---- */
  function notify(kind, lotId, title, body) {
    // high-intent events also fan out to the buyer's WhatsApp + email
    const highIntent = ["outbid", "win", "buynow", "snipe", "match", "offer", "lost"].includes(kind);
    const prefs = (state.buyer && state.buyer.prefs) || {};
    const extra = [];
    if (highIntent) { if (prefs.outbidPush !== false) extra.push("whatsapp"); if (prefs.closeAlert !== false) extra.push("email"); }
    const channels = ["app", ...extra];
    const n = { id: uid(), kind, lotId, title, body, ts: Date.now(), read: false, channels };
    state.notifications = [n, ...state.notifications].slice(0, 40);
    state.toasts = [...state.toasts, n].slice(-4);
  }
  function dismissToast(id) { state.toasts = state.toasts.filter(t => t.id !== id); emit(); }
  function markNotifsRead() { state.notifReadAt = Date.now(); emit(); }

  /* ---- watchlist ---- */
  function toggleWatch(id) {
    const w = new Set(state.watchlist);
    w.has(id) ? w.delete(id) : w.add(id);
    state.watchlist = w; emit();
  }

  function setShipTo(code) { state.shipTo = code; emit(); }

  /* ---- freight rates (uploaded monthly) ---- */
  function freightFor(lot, shipCode) {
    const code = shipCode || state.shipTo;
    const r = state.freight && state.freight.rates && state.freight.rates[code];
    if (r) {
      if (lot && lot.body && r.bodies && r.bodies[lot.body] != null) return r.bodies[lot.body];
      if (r.default != null) return r.default;
      if (typeof r === "number") return r;
    }
    const d = SHIP_DEST.find(s => s.code === code);
    return d ? d.freight : 0;
  }
  function setFreightRates({ month, rates, fileName }) {
    state.freight = { month: month || "Uploaded rates", uploadedAt: Date.now(), fileName: fileName || null, rates };
    audit("admin", "freight.upload", `${fileName || "rate sheet"} · ${Object.keys(rates).length} destinations`);
    notify("admin", null, "Freight rates updated", `${month || "New rates"} applied to all landed-cost calculations.`);
    emit();
    return { ok: true };
  }
  function resetFreight() {
    state.freight = { month: "Default rates", uploadedAt: null, fileName: null, rates: SHIP_DEST.reduce((m, d) => { m[d.code] = { default: d.freight, bodies: {} }; return m; }, {}) };
    emit();
  }

  /* ---- monetization: commission, freight margin, FX spread, revenue ---- */
  function commissionRate(price) {
    const tiers = (Array.isArray(state.config.commissionTiers) && state.config.commissionTiers.length) ? state.config.commissionTiers : DEFAULT_CONFIG.commissionTiers;
    for (const t of tiers) { if (t.upTo == null || price <= t.upTo) return t.pct; }
    return tiers[tiers.length - 1].pct;
  }
  function commissionFor(price) {
    const raw = Math.round(price * commissionRate(price));
    return Math.min(state.config.commissionCap || Infinity, Math.max(state.config.commissionMin || 0, raw));
  }
  function freightQuoted(lot, code) { return Math.round(freightFor(lot, code) * (1 + (state.config.freightMargin || 0))); }
  function fxSpreadRevenue(amount, code) { return code && code !== "USD" ? Math.round(amount * (state.config.fxSpread || 0)) : 0; }
  function revenueFor(lot, salePrice, code) {
    code = code || state.shipTo;
    const freightMargin = freightQuoted(lot, code) - freightFor(lot, code);
    const fx = fxSpreadRevenue(salePrice + TOTAL_BUYER_FEES, code);
    const commission = lot.owned ? 0 : commissionFor(salePrice);
    return { buyerFees: TOTAL_BUYER_FEES, commission, freightMargin, fx, total: TOTAL_BUYER_FEES + commission + freightMargin + fx };
  }
  function setConfig(patch) { state.config = { ...state.config, ...patch }; emit(); }

  /* ---- consignor settlements (payouts) ---- */
  const SETTLE_STAGES = ["pending", "funded", "paid"];
  function createSettlement(lot, salePrice) {
    if (!lot || lot.owned) return;                      // owned stock = no consignor payout
    if (state.settlements.some(s => s.lotId === lot.id)) return;
    const commission = commissionFor(salePrice);
    state.settlements = [{
      id: uid(), lotId: lot.id, consignorId: lot.consignorId || null, consignor: lot.seller,
      title: `${lot.year} ${lot.make} ${lot.model}`, salePrice, commission, net: salePrice - commission,
      status: "pending", soldAt: Date.now(), paidAt: null,
    }, ...state.settlements];
    audit("system", "settlement.create", `${lot.id} · net ${fmtUSD(salePrice - commission)} → ${lot.seller}`);
  }
  function advanceSettlement(id) {
    const stl0 = state.settlements.find(s => s.id === id);
    if (stl0 && stl0.status === "funded" && state.claims.some(c => c.lot === stl0.lotId && c.status !== "resolved")) {
      notify("admin", stl0.lotId, "Payout held", `Settlement for ${stl0.lotId} is held while an arbitration claim is open.`);
      return { ok: false, reason: "claim-open" };
    }
    state.settlements = state.settlements.map(s => {
      if (s.id !== id) return s;
      const i = Math.min(SETTLE_STAGES.length - 1, SETTLE_STAGES.indexOf(s.status) + 1);
      const ns = { ...s, status: SETTLE_STAGES[i] }; if (ns.status === "paid") ns.paidAt = Date.now();
      return ns;
    });
    audit("admin", "settlement.advance", id);
    emit();
  }

  /* ---- consignor CRUD ---- */
  function genConsignorId() { let n = 1006; while (state.consignors.some(c => c.id === "CG-" + n)) n++; return "CG-" + n; }
  function addConsignor(data) {
    const c = { id: genConsignorId(), rating: 0, status: "active", ...data, createdAt: Date.now() };
    state.consignors = [c, ...state.consignors];
    audit("admin", "consignor.add", c.business);
    emit(); return { ok: true, consignor: c };
  }
  function updateConsignor(id, patch) { state.consignors = state.consignors.map(c => c.id === id ? { ...c, ...patch } : c); emit(); }
  function removeConsignor(id) { state.consignors = state.consignors.filter(c => c.id !== id); emit(); }

  /* ---- saved searches + match alerts ---- */
  function matchSearch(query, lot) {
    if (!query) return false;
    if (query.make && lot.make !== query.make) return false;
    if (query.body && lot.body !== query.body) return false;
    if (query.title && lot.title !== query.title) return false;
    if (query.maxPrice && lot.highBid > query.maxPrice) return false;
    if (query.minGrade && lot.autograde < query.minGrade) return false;
    if (query.q) { const t = query.q.toLowerCase(); if (!`${lot.year} ${lot.make} ${lot.model} ${lot.trim}`.toLowerCase().includes(t)) return false; }
    return true;
  }
  function savedMatchCount(query) { return Object.values(state.lots).filter(L => !L.hidden && matchSearch(query, L)).length; }
  function saveSearch(name, query) {
    const ss = { id: uid(), name, query, alerts: true, createdAt: Date.now() };
    state.savedSearches = [ss, ...state.savedSearches].slice(0, 12); emit(); return ss;
  }
  function removeSearch(id) { state.savedSearches = state.savedSearches.filter(s => s.id !== id); emit(); }
  function toggleSearchAlerts(id) { state.savedSearches = state.savedSearches.map(s => s.id === id ? { ...s, alerts: !s.alerts } : s); emit(); }

  /* ---- bidding ---- */
  function minNextBid(lot) {
    if (lot.highBidder === "you") return lot.yourMax + lot.increment; // raising your own max
    return lot.highBid + lot.increment;
  }

  const recentBids = {}; // lotId -> { amount, ts } — dedupe accidental double-submits
  function placeBid(lotId, maxAmount, opts = {}) {
    const lot = state.lots[lotId];
    const now = Date.now();
    if (!lot) return { ok: false, reason: "Auction has ended." };
    // Pre-bid on a scheduled lot: store the max; it resolves when the lane opens.
    if (lot.status === "scheduled") {
      if (!Number.isFinite(maxAmount) || maxAmount < lot.startPrice) return { ok: false, reason: `Minimum is ${fmtUSD(lot.startPrice)}.` };
      if (!withinPower(lotId, maxAmount)) return { ok: false, reason: `Over your buying power. ${fmtUSD(availablePower())} available.` };
      if (!lot.yourTs) lot.yourTs = now;
      lot.yourMax = maxAmount;
      audit("you", "bid.prebid", `${fmtUSD(maxAmount)} pre-bid on ${lot.id}`);
      notify("winning", lotId, "Pre-bid placed", `${lot.year} ${lot.make} ${lot.model}: your max ${fmtUSD(maxAmount)} is armed for when the lane opens.`);
      emit();
      return { ok: true, status: "prebid" };
    }
    if (lot.status !== "live" || now >= lot.endsAt) {
      return { ok: false, reason: "Auction has ended." };
    }
    const rb = recentBids[lotId];
    if (rb && rb.amount === maxAmount && now - rb.ts < 1500) {
      return { ok: false, reason: "Duplicate bid ignored." };
    }
    recentBids[lotId] = { amount: maxAmount, ts: now };
    const minNext = minNextBid(lot);
    if (!Number.isFinite(maxAmount) || maxAmount < minNext) {
      return { ok: false, reason: `Minimum is ${fmtUSD(minNext)}.`, min: minNext };
    }
    if (!withinPower(lotId, maxAmount)) {
      return { ok: false, reason: `Over your ${fmtUSD(state.buyingPower)} buying power (incl. ${fmtUSD(TOTAL_BUYER_FEES)} fees). ${fmtUSD(availablePower())} available.` };
    }
    if (lot.buyNow && maxAmount >= lot.buyNow) {
      return buyNow(lotId);
    }
    const wasHigh = lot.highBidder === "you";
    if (!lot.yourTs) lot.yourTs = now;        // keep earliest ts → win ties
    lot.yourMax = maxAmount;
    if (!wasHigh) { lot.bidCount++; }
    const r = resolve(lot, now, true);
    maybeAntiSnipe(lot, now);
    lot.lastBidTs = now;

    audit("you", "bid.place", `${fmtUSD(maxAmount)} max on ${lot.id}`);
    if (lot.highBidder === "you") {
      notify("winning", lotId, "You're the high bidder",
        `${lot.year} ${lot.make} ${lot.model} · ${fmtUSD(lot.highBid)}${opts.proxy ? ` · max ${fmtUSD(maxAmount)}` : ""}`);
      emit();
      return { ok: true, status: "winning", highBid: lot.highBid };
    } else {
      notify("outbid", lotId, "Outbid instantly",
        `Another dealer's max is higher. Current ${fmtUSD(lot.highBid)}.`);
      emit();
      return { ok: true, status: "outbid", highBid: lot.highBid };
    }
  }

  function buyNow(lotId) {
    const lot = state.lots[lotId];
    if (!lot || lot.status !== "live" || !lot.buyNow) return { ok: false, reason: "Unavailable." };
    if (!withinPower(lotId, lot.buyNow)) {
      return { ok: false, reason: `Over your ${fmtUSD(state.buyingPower)} buying power (incl. ${fmtUSD(TOTAL_BUYER_FEES)} fees). ${fmtUSD(availablePower())} available.` };
    }
    lot.highBid = lot.buyNow; lot.highBidder = "you"; lot.yourMax = lot.buyNow;
    lot.reserveMet = true; lot.status = "ended"; lot.result = "won";
    lot.history.unshift({ id: uid(), bidder: "You", you: true, amount: lot.buyNow, type: "buynow", ts: Date.now() });
    addPurchase(lot);
    audit("you", "bid.buynow", `${fmtUSD(lot.buyNow)} on ${lot.id}`);
    notify("buynow", lotId, "Purchased with Buy Now", `${lot.year} ${lot.make} ${lot.model} · ${fmtUSD(lot.buyNow)}`);
    emit();
    return { ok: true, status: "won" };
  }

  /* ---- offers / reserve counteroffer ---- */
  function offerFloor(lot) { return lot.reserve != null ? lot.reserve : Math.round((lot.marketValue || lot.startPrice) * 0.9); }
  function addOfferPurchase(lot, amount) {
    if (state.purchases.some(p => p.id === lot.id)) return;
    lot.result = "won"; if (lot.status === "live") lot.status = "ended";
    state.purchases = [{
      id: lot.id, title: `${lot.year} ${lot.make} ${lot.model}`, vin: lot.vin,
      final: amount, fees: estFees(lot), wonTs: Date.now(), paidTs: null, pickedUpTs: null, gatePass: null,
      stage: "payment", dest: shipLabel(state.shipTo), region: state.shipTo,
    }, ...state.purchases];
    createSettlement(lot, amount);
  }
  function makeOffer(lotId, amount) {
    const lot = state.lots[lotId];
    if (!lot) return { ok: false, reason: "Unavailable." };
    if (!Number.isFinite(amount) || amount <= 0) return { ok: false, reason: "Enter an amount." };
    if (!withinPower(lotId, amount)) return { ok: false, reason: `Over your buying power (incl. ${fmtUSD(TOTAL_BUYER_FEES)} fees). ${fmtUSD(availablePower())} available.` };
    const offer = { id: uid(), lotId, amount, status: "pending", counter: null, ts: Date.now(), expiresAt: Date.now() + 24 * 3600000 };
    state.offers = [offer, ...state.offers.filter(o => !(o.lotId === lotId && (o.status === "pending" || o.status === "countered")))];
    audit("you", "offer.make", `${fmtUSD(amount)} on ${lotId}`);
    notify("offer", lotId, "Offer submitted", `${fmtUSD(amount)} on ${lot.year} ${lot.make} ${lot.model}. Awaiting seller…`);
    emit();
    // real flow: every offer awaits a real answer (Carzello desk for owned stock, the consignor for consigned)
    notify("admin", lotId, "Offer awaiting seller", `${fmtUSD(amount)} on ${lot.year} ${lot.make} ${lot.model} — respond in Offers.`);
    return { ok: true };
  }
  function respondToOffer(id) {
    const o = state.offers.find(x => x.id === id);
    if (!o || o.status !== "pending") return;
    const lot = state.lots[o.lotId]; if (!lot) return;
    // Reserve is a HARD floor: a reserved lot can never be accepted below reserve.
    // No-reserve lots use a softer market floor with a small tolerance.
    const hasReserve = lot.reserve != null;
    const floor = hasReserve ? lot.reserve : Math.round((lot.marketValue || lot.startPrice) * 0.9);
    const acceptAt = hasReserve ? floor : floor * 0.985;
    if (o.amount >= acceptAt) {
      o.status = "accepted"; addOfferPurchase(lot, o.amount);
      audit("seller", "offer.accept", `${fmtUSD(o.amount)} on ${lot.id}`);
      notify("win", o.lotId, "Offer accepted!", `Seller accepted ${fmtUSD(o.amount)} for ${lot.make} ${lot.model}. Proceed to checkout.`);
    } else if (o.amount >= floor * 0.9) {
      // counter never below the floor (≥ reserve for reserved lots)
      o.counter = Math.ceil(floor / lot.increment) * lot.increment; o.status = "countered";
      audit("seller", "offer.counter", `${fmtUSD(o.counter)} on ${lot.id}`);
      notify("offer", o.lotId, "Seller counter-offer", `Seller countered at ${fmtUSD(o.counter)} for ${lot.make} ${lot.model}.`);
    } else {
      o.status = "declined";
      audit("seller", "offer.decline", `${fmtUSD(o.amount)} on ${lot.id}`);
      notify("lost", o.lotId, "Offer declined", `Seller declined ${fmtUSD(o.amount)} for ${lot.make} ${lot.model}.`);
    }
    emit(); // persist — an accepted offer creates a real purchase/settlement that must survive reload
  }
  function acceptCounter(id) {
    const o = state.offers.find(x => x.id === id);
    if (!o || o.status !== "countered") return { ok: false };
    const lot = state.lots[o.lotId];
    o.status = "accepted"; o.amount = o.counter; addOfferPurchase(lot, o.counter);
    audit("you", "offer.acceptCounter", `${fmtUSD(o.counter)} on ${lot.id}`);
    notify("win", o.lotId, "Counter accepted", `You accepted ${fmtUSD(o.counter)} for ${lot.make} ${lot.model}. Proceed to checkout.`);
    emit();
    return { ok: true };
  }
  function declineOffer(id) {
    state.offers = state.offers.map(o => o.id === id ? { ...o, status: "declined" } : o);
    emit();
  }
  function offerFor(lotId) { return state.offers.find(o => o.lotId === lotId && o.status !== "declined"); }

  function maybeAntiSnipe(lot, now) {
    const win = (state.config.antiSnipeMin || 2) * 60000;
    const remaining = lot.endsAt - now;
    if (remaining > 0 && remaining <= win) {
      lot.endsAt = now + win;
      lot.extensions++;
      const mm = (state.config.antiSnipeMin || 2);
      notify("snipe", lot.id, `Anti-snipe · +${mm}:00`, `Late bid extended ${lot.make} ${lot.model} by ${mm} minutes.`);
    }
  }

  const STAGES = ["payment", "title", "shipping", "delivered"];
  function advancePurchase(id) {
    state.purchases = state.purchases.map(p => {
      if (p.id !== id) return p;
      const i = Math.min(STAGES.length - 1, STAGES.indexOf(p.stage) + 1);
      const np = { ...p, stage: STAGES[i] };
      if (np.stage === "delivered" && !np.deliveredTs) np.deliveredTs = Date.now();
      return np;
    });
    emit();
  }

  function addPurchase(lot) {
    if (state.purchases.some(p => p.id === lot.id)) return;
    state.purchases = [{
      id: lot.id, title: `${lot.year} ${lot.make} ${lot.model}`, vin: lot.vin,
      final: lot.highBid, fees: lot.buyFee || estFees(lot), wonTs: Date.now(),
      paidTs: null, pickedUpTs: null, gatePass: null,
      stage: "payment", dest: shipLabel(state.shipTo), region: state.shipTo,
    }, ...state.purchases];
    createSettlement(lot, lot.highBid);
  }

  /* ---- the world clock / competitor simulation ---- */
  function tick() {
    const now = Date.now();
    let dirty = false;
    Object.values(state.lots).forEach(lot => {
      if (lot.remote) return; // shared auctions are driven by the auction server, not the local simulation
      if (lot.status === "scheduled") {
        if (now >= lot.startsAt) {
          lot.status = "live";
          resolve(lot, now, false);
          notify("admin", lot.id, "Lane open", `${lot.year} ${lot.make} ${lot.model} is now live (Lane ${lot.lane} · Run ${lot.run}).`);
          dirty = true;
        }
        return;
      }
      if (lot.status !== "live") return;

      // End the auction?
      if (now >= lot.endsAt) {
        lot.status = "ended";
        if (lot.yourMax) {
          if (lot.highBidder === "you" && lot.reserveMet) {
            lot.result = "won"; addPurchase(lot);
            notify("win", lot.id, "You won!", `${lot.year} ${lot.make} ${lot.model} · ${fmtUSD(lot.highBid)}. Proceed to checkout.`);
          } else {
            lot.result = "lost";
            const why = !lot.reserveMet && lot.highBidder === "you" ? "reserve not met" : "outbid";
            // If-sale: your below-reserve high bid goes to the seller for approval.
            if (!lot.reserveMet && lot.highBidder === "you" && !state.offers.some(o => o.lotId === lot.id && o.status !== "declined")) {
              state.offers = [{ id: uid(), lotId: lot.id, amount: lot.highBid, status: "pending", counter: null, ts: now, ifSale: true, expiresAt: now + 24 * 3600000 }, ...state.offers];
              notify("offer", lot.id, "Sent to seller for approval", `Your high bid ${fmtUSD(lot.highBid)} was below reserve — the seller can accept, counter, or decline.`);
            }
            notify("lost", lot.id, "Auction ended", `${lot.year} ${lot.make} ${lot.model} — ${why}. Final ${fmtUSD(lot.highBid)}.`);
          }
        }
        dirty = true;
        return;
      }

      // Competitor activity — probability scales with heat + urgency.
      const remaining = lot.endsAt - now;
      const urgency = remaining < 60000 ? 2.4 : remaining < 300000 ? 1.5 : 1;
      const p = 0.10 * lot.heat * urgency;
      if (Math.random() < p) {
        const ceil = lot.buyNow ? lot.buyNow * 0.98 : lot.startPrice * 2;
        // raise an existing competitor or add a new one
        let comp;
        if (lot.competitors.length && Math.random() < 0.7) {
          comp = lot.competitors[rndInt(0, lot.competitors.length - 1)];
        } else {
          comp = { id: "c" + uid(), label: dealerLabel(rndInt(1, 8999)), max: lot.highBid, ts: now };
          lot.competitors.push(comp);
        }
        const bump = lot.increment * rndInt(1, 4);
        comp.max = Math.min(ceil, Math.max(comp.max, lot.highBid) + bump);
        comp.ts = now;
        const r = resolve(lot, now, true);
        if (r.changed) {
          dirty = true;
          maybeAntiSnipe(lot, now);
          if (r.flippedAwayFromYou) {
            notify("outbid", lot.id, "You've been outbid",
              `${lot.year} ${lot.make} ${lot.model} — new high ${fmtUSD(lot.highBid)}.`);
          }
        }
      }
    });
    // if-sale/offer expiry: firm for 24h, then released (frees buying power)
    state.offers.forEach(o => {
      if ((o.status === "pending" || o.status === "countered") && o.expiresAt && now >= o.expiresAt) {
        o.status = "expired"; dirty = true;
        notify("lost", o.lotId, "Offer expired", `Your ${fmtUSD(o.status === "countered" ? o.counter : o.amount)} offer lapsed after 24h without seller response.`);
      }
    });
    // payment defaults: unpaid orders past the default window → relist + penalty
    let pd = false;
    state.purchases = state.purchases.map(p => {
      if (!p.paidTs && p.status !== "defaulted" && now - p.wonTs >= state.config.defaultDays * DAY_MS) { pd = true; return processDefault(p, now); }
      return p;
    });
    if (pd) dirty = true;
    if (dirty) emit();
    // No-change ticks no longer notify: countdowns self-update via useTick,
    // so the whole tree doesn't re-render every second for nothing.
  }

  /* ============================================================
     ADMIN operations
     ============================================================ */
  function genLotId() {
    let n = 48250;
    while (state.lots["SY-" + n]) n++;
    return "SY-" + n;
  }
  // normalize a form-shaped raw lot (strings → numbers) before seeding
  function normalizeRaw(raw) {
    const num = (v, d = 0) => { const n = Number(String(v).replace(/[^\d.]/g, "")); return Number.isFinite(n) && String(v).trim() !== "" ? n : d; };
    const g = {}; Object.keys(raw.grades || {}).forEach(k => g[k] = num(raw.grades[k], 3.5));
    return {
      ...raw,
      id: raw.id || genLotId(),
      year: num(raw.year), mileage: num(raw.mileage), photos: num(raw.photos, 24),
      run: num(raw.run, 1), increment: num(raw.increment, 250),
      startPrice: num(raw.startPrice), reserve: raw.reserve === "" || raw.reserve == null ? null : num(raw.reserve),
      buyNow: raw.buyNow === "" || raw.buyNow == null ? null : num(raw.buyNow),
      autograde: num(raw.autograde, 3.5), grades: g,
      endsInS: num(raw.endsInS, 86400), heat: Number(raw.heat) || 0.5, competitors: num(raw.competitors, 3),
      keys: num(raw.keys, 2),
      marketValue: raw.marketValue === "" || raw.marketValue == null ? null : num(raw.marketValue),
      owned: raw.owned !== false,
      source: raw.owned !== false ? "SAYARAH" : "CONSIGNED",
      seller: raw.owned !== false ? "Sayarah Auto" : (raw.seller || "Consignment"),
    };
  }

  function addLot(formRaw) {
    const raw = normalizeRaw(formRaw);
    state.customLots = [...state.customLots, raw];
    rawIndex[raw.id] = raw;
    const lot = seedLot(raw, Date.now());
    state.lots = { ...state.lots, [raw.id]: lot };
    audit("admin", "inventory.add", `${raw.year} ${raw.make} ${raw.model} (${raw.id})`);
    notify("admin", raw.id, "Inventory added", `${raw.year} ${raw.make} ${raw.model} is now live for buyers.`);
    // fire saved-search match alerts for buyers
    state.savedSearches.forEach(ss => {
      if (ss.alerts && matchSearch(ss.query, lot)) notify("match", raw.id, "New match for your saved search", `${ss.name}: ${raw.year} ${raw.make} ${raw.model} · ${fmtUSD(lot.highBid)}`);
    });
    emit();
    return { ok: true, id: raw.id };
  }

  function updateLot(id, patch) {
    const lot = state.lots[id];
    if (!lot) return { ok: false };
    const np = { ...patch };
    if ("grades" in np) { const g = { ...lot.grades, ...np.grades }; Object.keys(g).forEach(k => g[k] = Number(g[k]) || 0); np.grades = g; }
    if ("crNotes" in np) np.crNotes = { ...lot.crNotes, ...np.crNotes };
    // coerce numeric pricing fields when present
    ["startPrice", "reserve", "buyNow", "increment", "autograde", "mileage", "year", "marketValue"].forEach(k => {
      if (k in np && np[k] !== null && np[k] !== "") np[k] = Number(String(np[k]).replace(/[^\d.]/g, "")) || lot[k];
    });
    state.lots = { ...state.lots, [id]: { ...lot, ...np } };
    if (np.reserve != null) state.lots[id].reserveMet = state.lots[id].reserve == null ? true : state.lots[id].highBid >= state.lots[id].reserve;
    // persist as an edit (so it survives reload) — store only the patch
    state.lotEdits = { ...state.lotEdits, [id]: { ...(state.lotEdits[id] || {}), ...patch } };
    // keep customLots copy in sync if this is a custom lot
    state.customLots = state.customLots.map(r => r.id === id ? { ...r, ...patch } : r);
    emit();
    return { ok: true };
  }

  function setLotHidden(id, hidden) { updateLot(id, { hidden }); }

  function removeLot(id) {
    try { if (typeof Auction !== "undefined" && FB.enabled && FB.isAdmin()) Auction.close(id); } catch (e) {}
    if (state.customLots.some(r => r.id === id)) {
      state.customLots = state.customLots.filter(r => r.id !== id);
      const lots = { ...state.lots }; delete lots[id]; state.lots = lots;
      const le = { ...state.lotEdits }; delete le[id]; state.lotEdits = le;
      emit();
    } else {
      setLotHidden(id, true); // seed lots are hidden, not deleted (reversible)
    }
    return { ok: true };
  }

  function closeAuction(id) {
    const lot = state.lots[id];
    if (!lot || lot.status !== "live") return { ok: false };
    try { if (lot.remote && typeof Auction !== "undefined" && FB.enabled && FB.isAdmin()) Auction.close(id); } catch (e) {}
    state.lots = { ...state.lots, [id]: { ...lot, endsAt: Date.now() } }; // tick() resolves win/lost
    emit();
    return { ok: true };
  }
  function extendAuction(id, mins) {
    const lot = state.lots[id];
    if (!lot) return { ok: false };
    const endsAt = Math.max(lot.endsAt, Date.now()) + mins * 60000;
    state.lots = { ...state.lots, [id]: { ...lot, endsAt, status: "live", result: null } };
    // shared auctions: the server owns the clock — republish with the new end time
    try {
      if (lot.remote && typeof Auction !== "undefined" && FB.enabled && FB.isAdmin()) {
        Auction.publish({ lotId: id, startPrice: lot.startPrice, increment: lot.increment, reserve: lot.reserve || null, buyNow: lot.buyNow || null, endsAt });
      }
    } catch (e) {}
    emit();
    return { ok: true };
  }

  /* ---- dealers: onboarding + temp-password invite ---- */
  function genPassword() {
    const c = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
    let s = ""; for (let i = 0; i < 8; i++) s += c[Math.floor(Math.random() * c.length)];
    return "SYR-" + s.slice(0, 4) + "-" + s.slice(4);
  }
  function genDealerId() { let n = 1004; while (state.dealers.some(d => d.id === "D-" + n)) n++; return "D-" + n; }
  function sendWelcome(dealer) {
    const email = {
      id: uid(), channel: "email", to: dealer.email, dealerId: dealer.id, ts: Date.now(), status: "sent",
      subject: "Your Carzello dealer account is ready",
      body:
`Hi ${dealer.contact || "there"},

Your dealer account for ${dealer.business} is now active on Carzello.

  Login email:        ${dealer.email}
  Temporary password: ${dealer.tempPassword}

Sign in at https://clear.sayarah.co/login and set a new password on first use.
Approved buying power: ${fmtUSD(dealer.creditLimit || 0)}.

Welcome aboard,
The Sayarah team`,
    };
    const msgs = [email];
    if (dealer.phone) msgs.push({
      id: uid(), channel: "sms", to: dealer.phone, dealerId: dealer.id, ts: Date.now(), status: "sent",
      subject: "SMS", body: `Carzello: your dealer account is ready. Temp password ${dealer.tempPassword}. Sign in: carzello.com/login`,
    });
    state.outbox = [...msgs, ...state.outbox].slice(0, 80);
    notify("admin", null, "Welcome message sent", `Email${dealer.phone ? " + SMS" : ""} sent to ${dealer.contact || dealer.email} with a temporary password.`);
  }
  function addDealer(data) {
    const tempPassword = genPassword();
    const edd = data.country === "Afghanistan" || data.risk === "High";
    const dealer = { id: genDealerId(), ...data, creditLimit: Number(String(data.creditLimit).replace(/[^\d]/g, "")) || 0, status: "screening", edd, tempPassword, createdAt: Date.now(), lastActive: "—" };
    state.dealers = [dealer, ...state.dealers];
    audit("admin", "dealer.add", `${dealer.business} (${dealer.email})${edd ? " · EDD required" : ""}`);
    if (edd) {
      notify("admin", null, "EDD required", `${dealer.business} (${dealer.country}) requires enhanced due diligence — manual clearance before invite.`);
    } else {
      // simulated sanctions/PEP screening clears, then the invite goes out
      setTimeout(() => {
        state.dealers = state.dealers.map(d => d.id === dealer.id ? { ...d, status: "invited", screenedAt: Date.now() } : d);
        const dd = state.dealers.find(d => d.id === dealer.id);
        audit("system", "dealer.screenClear", dealer.business);
        sendWelcome(dd);
        emit();
      }, 2500);
    }
    emit();
    return { ok: true, dealer };
  }
  function clearDealerEDD(id) {
    state.dealers = state.dealers.map(d => d.id === id ? { ...d, status: "invited", screenedAt: Date.now(), eddClearedAt: Date.now() } : d);
    const dd = state.dealers.find(d => d.id === id);
    if (dd) { audit("admin", "dealer.eddClear", dd.business); sendWelcome(dd); }
    emit();
  }
  function updateDealer(id, patch) {
    const np = { ...patch };
    if ("creditLimit" in np) np.creditLimit = Number(String(np.creditLimit).replace(/[^\d]/g, "")) || 0;
    state.dealers = state.dealers.map(d => d.id === id ? { ...d, ...np } : d);
    audit("admin", "dealer.update", id);
    emit();
  }
  function removeDealer(id) { state.dealers = state.dealers.filter(d => d.id !== id); audit("admin", "dealer.remove", id); emit(); }
  function resendDealerInvite(id) {
    const tempPassword = genPassword();
    let updated = null;
    state.dealers = state.dealers.map(d => { if (d.id !== id) return d; updated = { ...d, tempPassword, status: "invited" }; return updated; });
    if (updated) { sendWelcome(updated); audit("admin", "dealer.resendInvite", id); emit(); }
    return { ok: !!updated, dealer: updated };
  }

  /* ---- staff CRUD ---- */
  function addStaff(s) { state.staff = [...state.staff, { id: "u" + uid(), status: "invited", lastActive: "—", ...s }]; emit(); }
  function updateStaff(id, patch) { state.staff = state.staff.map(u => u.id === id ? { ...u, ...patch } : u); emit(); }
  function removeStaff(id) { state.staff = state.staff.filter(u => u.id !== id); emit(); }

  /* ---- arbitration ---- */
  function updateClaim(id, patch) { state.claims = state.claims.map(c => c.id === id ? { ...c, ...patch } : c); emit(); }
  const EXTENDED_CLAIM_TYPES = ["Title / VIN", "Odometer", "Structural", "Flood / fire"]; // extended or open windows per NAAA
  function claimEligibility(p, type, amount) {
    const c = state.config;
    if (type === "Condition-grade opinion") return { ok: false, reason: "Grade opinions aren't arbitratable — claims must cite a specific undisclosed defect." };
    const asIs = (p.final || 0) <= c.asIsBelow;
    if (asIs && !EXTENDED_CLAIM_TYPES.includes(type)) return { ok: false, reason: `Vehicles ≤ ${fmtUSD(c.asIsBelow)} sell as-is — only title, odometer or VIN claims are accepted.` };
    // dollar thresholds (waived for title/odo/VIN class issues)
    if (!EXTENDED_CLAIM_TYPES.includes(type)) {
      const min = (p.final || 0) >= 50000 ? Math.max(c.claimMinAmount, Math.round(p.final * c.claimPctOver50k)) : c.claimMinAmount;
      if ((amount || 0) < min) return { ok: false, reason: `Claimed repair cost must be at least ${fmtUSD(min)} for this vehicle.` };
      // window: from delivery; PSI extends it
      const winH = c.arbitrationWindowH + ((p.addons && p.addons.psi) ? c.psiExtensionH : 0);
      const start = p.deliveredTs || p.pickedUpTs || p.paidTs;
      if (start && Date.now() > start + winH * 3600000) return { ok: false, reason: `The ${Math.round(winH / 24)}-day arbitration window has closed for this defect type.` };
    }
    return { ok: true };
  }
  function addClaim(data) {
    const p = state.purchases.find(x => (x.lotId || x.id) === data.lot) || {};
    const gate = claimEligibility(p, data.type || "Undisclosed defect", data.amount);
    if (!gate.ok) return gate;
    let n = 2050; while (state.claims.some(c => c.id === "ARB-" + n)) n++;
    const c = { id: "ARB-" + n, status: "open", filed: "Just now", sla: "5 days left", buyer: state.buyer.name, ...data };
    state.claims = [c, ...state.claims];
    audit("you", "claim.file", `${c.id} · ${c.lot} · ${c.type || ""}`);
    notify("admin", data.lot, "Arbitration claim filed", `${data.title}: ${data.reason}`);
    emit(); return { ok: true, claim: c };
  }
  function reviewListing(id, status) { state.listingRequests = state.listingRequests.map(r => r.id === id ? { ...r, status } : r); emit(); }
  function setBuyerPref(key, val) { state.buyer = { ...state.buyer, prefs: { ...(state.buyer.prefs || {}), [key]: val } }; emit(); }
  function setBuyerIdentity(patch) { state.buyer = { ...state.buyer, ...patch }; emit(); }

  /* ---- order stage (admin can set directly) ---- */
  function setPurchaseStage(id, stage) { state.purchases = state.purchases.map(p => p.id === id ? { ...p, stage, deliveredTs: stage === "delivered" && !p.deliveredTs ? Date.now() : p.deliveredTs } : p); emit(); }

  /* ============================================================
     PAYMENT LIFECYCLE
     48h to pay → $50/day late → 5-day default (relist + 10%/$800)
     → gate pass on payment → 7-day pickup grace → $25/day storage
     ============================================================ */
  function paymentStatus(p, now) {
    now = now || Date.now();
    const c = state.config;
    const amountDue = (p.final || 0) + (p.fees || 0) + addonCosts(p).total;
    const dueAt = p.wonTs + c.paymentWindowH * 3600000;
    const paid = !!p.paidTs;
    const defaultAt = p.wonTs + c.defaultDays * DAY_MS;
    const defaulted = p.status === "defaulted" || (!paid && now >= defaultAt);
    // late fee accrues from the window to payment (or now), per started day
    const lateEnd = paid ? p.paidTs : (defaulted ? defaultAt : now); // freeze late fee at default
    const lateMs = Math.max(0, lateEnd - dueAt);
    const daysLate = lateMs > 0 ? Math.ceil(lateMs / DAY_MS) : 0;
    // once defaulted, fees freeze to the snapshot captured at default time
    const lateFee = (p.status === "defaulted" && p.lateFeeAtDefault != null) ? p.lateFeeAtDefault : daysLate * c.lateFeePerDay;
    const penalty = !defaulted ? 0 : (p.penalty != null ? p.penalty : Math.max(Math.round(amountDue * c.penaltyPct), c.penaltyMin));
    // storage: after pickup grace from payment
    let pickupDueAt = null, storageDaysLate = 0, storageFee = 0;
    if (paid) {
      pickupDueAt = p.paidTs + c.pickupDays * DAY_MS;
      const pickEnd = p.pickedUpTs || now;
      const sMs = Math.max(0, pickEnd - pickupDueAt);
      storageDaysLate = sMs > 0 ? Math.ceil(sMs / DAY_MS) : 0;
      storageFee = storageDaysLate * c.storagePerDay;
    }
    const owedNow = defaulted ? (lateFee + penalty) : (paid ? 0 : amountDue + lateFee);
    return {
      amountDue, dueAt, paid, defaulted, defaultAt, daysLate, lateFee, penalty,
      pickupDueAt, storageDaysLate, storageFee, owedNow, gatePass: p.gatePass,
      pickedUp: !!p.pickedUpTs, msToDue: dueAt - now, msToDefault: defaultAt - now,
    };
  }

  /* buyer reports the wire transfer; Carzello confirms receipt via payPurchase (admin Orders) */
  function claimPaymentSent(id) {
    let hit = false;
    state.purchases = state.purchases.map(p => {
      if (p.id !== id || p.paidTs || p.paymentClaimedTs) return p;
      hit = true;
      return { ...p, paymentClaimedTs: Date.now() };
    });
    if (!hit) return { ok: false };
    audit("you", "order.paymentClaimed", id);
    notify("admin", id, "Wire transfer reported", `Buyer reports payment sent for ${id}. Confirm receipt in Orders.`);
    emit();
    return { ok: true };
  }

  function payPurchase(id) {
    const now = Date.now();
    let done = null;
    state.purchases = state.purchases.map(p => {
      if (p.id !== id) return p;
      const ps = paymentStatus(p, now);
      if (ps.paid || ps.defaulted) return p;
      done = p;
      return { ...p, paidTs: now, lateFeePaid: ps.lateFee, gatePass: { code: genGatePassCode(p.id), issuedTs: now }, stage: p.stage === "payment" ? "title" : p.stage };
    });
    if (done) {
      // buyer funds cleared → advance the consignor settlement pending → funded
      state.settlements = state.settlements.map(stl => (stl.lotId === id && stl.status === "pending") ? { ...stl, status: "funded" } : stl);
      audit("you", "order.pay", `${id} paid in full`);
      notify("win", id, "Payment received · gate pass issued", `${done.title}: cleared for pickup. Gate pass available in My Purchases.`);
      emit();
      return { ok: true };
    }
    return { ok: false, reason: "Cannot pay — already paid or defaulted." };
  }

  /* ---- purchase add-ons (Sayarah Assurance buyback + post-sale inspection) ---- */
  function addonCosts(p) {
    const a = p.addons || {};
    const assurance = a.assurance ? Math.max(250, Math.round((p.final || 0) * 0.015)) : 0;
    const psi = a.psi ? 150 : 0;
    return { assurance, psi, total: assurance + psi };
  }
  function setPurchaseAddon(id, key, val) {
    state.purchases = state.purchases.map(p => (p.id === id && !p.paidTs) ? { ...p, addons: { ...(p.addons || {}), [key]: val } } : p);
    emit();
  }

  /* ---- deposit wallet: buying power = deposit × multiplier ---- */
  function topUpDeposit(amount) {
    state.wallet = { ...state.wallet, deposit: state.wallet.deposit + amount };
    state.buyingPower = state.wallet.deposit * state.wallet.multiplier;
    audit("you", "wallet.topup", fmtUSD(amount));
    notify("admin", null, "Deposit received", `${fmtUSD(amount)} added. Buying power is now ${fmtUSD(state.buyingPower)}.`);
    emit();
  }
  function setBuyerCurrency(cur) { state.buyer = { ...state.buyer, currency: cur }; emit(); }

  /* ---- seller-side offer handling (admin for owned, consignor for consigned) ---- */
  function respondOffer(id, action, counterAmt) {
    const o = state.offers.find(x => x.id === id);
    if (!o || (o.status !== "pending" && o.status !== "countered")) return { ok: false };
    const lot = state.lots[o.lotId]; if (!lot) return { ok: false };
    if (action === "accept") {
      o.status = "accepted"; addOfferPurchase(lot, o.amount);
      audit("seller", "offer.accept", `${fmtUSD(o.amount)} on ${lot.id}`);
      notify("win", o.lotId, "Offer accepted!", `Seller accepted ${fmtUSD(o.amount)} for ${lot.make} ${lot.model}. Proceed to checkout.`);
    } else if (action === "counter") {
      o.counter = Math.max(counterAmt || 0, lot.increment); o.status = "countered";
      audit("seller", "offer.counter", `${fmtUSD(o.counter)} on ${lot.id}`);
      notify("offer", o.lotId, "Seller counter-offer", `Seller countered at ${fmtUSD(o.counter)} for ${lot.make} ${lot.model}.`);
    } else {
      o.status = "declined";
      audit("seller", "offer.decline", `${fmtUSD(o.amount)} on ${lot.id}`);
      notify("lost", o.lotId, "Offer declined", `Seller declined ${fmtUSD(o.amount)} for ${lot.make} ${lot.model}.`);
    }
    emit(); return { ok: true };
  }

  /* ---- relist an unsold (ended, not won) lot ---- */
  function relistUnsold(id, patch = {}) {
    const lot = state.lots[id];
    const raw = rawIndex[id];
    if (!raw || !lot || lot.result === "won") return { ok: false, reason: "Not relistable." };
    const merged = { ...raw, ...patch, endsInS: patch.endsInS || 24 * 3600 };
    rawIndex[id] = merged;
    state.offers = state.offers.filter(o => o.lotId !== id);
    state.lots = { ...state.lots, [id]: seedLot(merged, Date.now()) };
    // shared auctions: publish the fresh run so all visitors bid on the relisted lot
    try {
      if (typeof Auction !== "undefined" && FB.enabled && FB.isAdmin()) {
        Auction.publish({ lotId: id, startPrice: merged.startPrice, increment: merged.increment || 250, reserve: merged.reserve || null, buyNow: merged.buyNow || null, endsAt: Date.now() + (merged.endsInS || 24 * 3600) * 1000 });
      }
    } catch (e) {}
    audit("admin", "inventory.relist", `${id}${patch.reserve != null ? " · new reserve " + fmtUSD(patch.reserve) : ""}`);
    notify("admin", id, "Relisted", `${lot.year} ${lot.make} ${lot.model} is running again.`);
    emit(); return { ok: true };
  }

  /* ---- schedule a lane: lots open together, close staggered ---- */
  function scheduleLane(lotIds, startTs, staggerMin, lane) {
    const now = Date.now();
    let i = 0;
    lotIds.forEach(id => {
      const lot = state.lots[id]; if (!lot) return;
      i++;
      state.lots[id] = {
        ...lot, lane: lane || lot.lane, run: i,
        startsAt: startTs, endsAt: startTs + i * staggerMin * 60000,
        status: startTs > now ? "scheduled" : "live", result: null,
        saleType: "live", extensions: 0,
      };
    });
    state.lots = { ...state.lots };
    audit("admin", "lane.schedule", `Lane ${lane} · ${i} lots · opens ${new Date(startTs).toLocaleString()}`);
    notify("admin", null, "Lane scheduled", `Lane ${lane}: ${i} lots, closes stagger ${staggerMin}m.`);
    emit(); return { ok: true, count: i };
  }

  function pickupPurchase(id) {
    state.purchases = state.purchases.map(p => p.id === id ? { ...p, pickedUpTs: Date.now(), stage: (p.stage === "title" || p.stage === "payment") ? "shipping" : p.stage } : p);
    audit("you", "order.pickup", id);
    emit();
  }

  function relistLot(id, now) {
    const raw = rawIndex[id]; if (!raw) return;
    state.lots = { ...state.lots, [id]: seedLot(raw, now) };
  }
  function processDefault(p, now) {
    const ps = paymentStatus(p, now);
    const penalty = ps.penalty;
    // void the consignor settlement (sale reversed) + any stale offers, then relist & free the lot id
    state.settlements = state.settlements.filter(x => x.lotId !== p.id);
    state.offers = state.offers.filter(o => o.lotId !== p.id);
    relistLot(p.id, now);
    audit("system", "order.default", `${p.id} · penalty ${fmtUSD(penalty + ps.lateFee)} · relisted`);
    notify("lost", p.id, "Order defaulted — vehicle relisted", `Payment not cleared within ${state.config.defaultDays} days. ${fmtUSD(penalty + ps.lateFee)} penalty charged; ${p.title} relisted.`);
    // rename the order id so the lot id is free for a fresh purchase on resale; keep lotId for display
    return { ...p, id: "DEF-" + p.id + "-" + uid().slice(0, 4), lotId: p.id, status: "defaulted", penalty, lateFeeAtDefault: ps.lateFee, defaultedTs: now };
  }

  /* ---- cross-tab sync: rehydrate persisted layer when another tab writes ---- */
  function hydrateFromStorage() {
    const p = loadPersist(); if (!p) return;
    state.watchlist = new Set(p.watchlist || []);
    state.purchases = p.purchases || state.purchases;
    state.shipTo = p.shipTo || state.shipTo;
    state.staff = p.staff || state.staff;
    state.claims = p.claims || state.claims;
    state.dealers = p.dealers || state.dealers;
    state.consignors = p.consignors || state.consignors;
    state.settlements = p.settlements || state.settlements;
    if (p.wallet) { state.wallet = p.wallet; state.buyingPower = p.wallet.deposit * p.wallet.multiplier; }
    state.savedSearches = p.savedSearches || state.savedSearches;
    state.config = { ...DEFAULT_CONFIG, ...(p.config || {}) };
    state.outbox = p.outbox || state.outbox;
    state.freight = p.freight || state.freight;
    state.notifReadAt = p.notifReadAt ?? state.notifReadAt;   // was a cross-tab parity gap
    state.buyer = p.buyer ? { ...state.buyer, ...p.buyer, prefs: { ...(state.buyer.prefs || {}), ...((p.buyer && p.buyer.prefs) || {}) } } : state.buyer;
    if (p.lang && p.lang !== state.lang) { state.lang = p.lang; applyDir(state.lang); }
    state.lotEdits = p.lotEdits || {};
    const newCustom = p.customLots || [];
    const newIds = new Set(newCustom.map(r => r.id));
    newCustom.forEach(raw => { if (!state.lots[raw.id]) { state.lots[raw.id] = seedLot(raw, Date.now()); rawIndex[raw.id] = raw; } });
    state.customLots.forEach(raw => { if (!newIds.has(raw.id) && state.lots[raw.id]) { const l = { ...state.lots }; delete l[raw.id]; state.lots = l; } });
    state.customLots = newCustom;
    Object.entries(state.lotEdits).forEach(([id, e]) => {
      if (state.lots[id]) state.lots[id] = { ...state.lots[id], ...e, grades: { ...state.lots[id].grades, ...(e.grades || {}) }, crNotes: { ...state.lots[id].crNotes, ...(e.crNotes || {}) } };
    });
    state.lots = { ...state.lots };
    emitNoSave();   // don't re-persist → no cross-tab ping-pong
  }
  try { window.addEventListener("storage", (e) => { if (e.key === LS_KEY && e.newValue) hydrateFromStorage(); }); } catch (e) {}

  // single global heartbeat
  setInterval(tick, 1000);

  /* ---- shared live auctions (real multi-user bidding via carzello-auction) ---- */
  const myUid = () => { try { const u = FB.currentUser && FB.currentUser(); return u ? u.uid : null; } catch (e) { return null; } };
  let lastRemoteJson = "";

  function applyRemoteAuctions(map, force) {
    if (!map) return;
    const j = JSON.stringify(map);
    if (!force && j === lastRemoteJson) return;
    lastRemoteJson = force ? "" : j;
    const uid = myUid();
    let dirty = false;
    Object.entries(map).forEach(([id, a]) => {
      const lot = state.lots[id];
      if (!lot) return;
      dirty = true;
      lot.remote = true;
      lot.increment = a.increment;
      lot.buyNow = a.buyNow;
      lot.endsAt = a.endsAt;
      lot.bidCount = a.bidCount;
      lot.highBid = a.highBid;
      lot.reserveMet = a.reserveMet;
      if (!a.hasReserve) lot.reserve = null;
      lot.highBidder = a.highBidderUid ? (uid && a.highBidderUid === uid ? "you" : (a.highBidderTag || "bidder")) : null;
      if (a.myMax) { lot.yourMax = a.myMax; lot.iBid = true; if (!lot.yourTs) lot.yourTs = Date.now(); }
      const wasLive = lot.status === "live";
      lot.status = a.status === "live" ? "live" : "ended";
      // record every real completed sale as a market comp (visible to everyone polling)
      if (lot.status === "ended" && a.saleMade && a.finalPrice && !state.soldComps.some(c => c.lotId === id)) {
        state.soldComps = [{ lotId: id, year: lot.year, make: lot.make, model: lot.model, grade: lot.autograde, final: a.finalPrice, ts: Date.now() }, ...state.soldComps].slice(0, 300);
      }
      if (lot.status === "ended" && !lot.result) {
        if (a.saleMade && uid && a.winnerUid === uid) {
          lot.result = "won";
          if (a.finalPrice) lot.highBid = a.finalPrice;
          addPurchase(lot);
          if (wasLive) notify("win", lot.id, "You won!", `${lot.year} ${lot.make} ${lot.model} · ${fmtUSD(lot.highBid)}. Proceed to checkout.`);
        } else if (lot.iBid) {
          lot.result = "lost";
        }
      }
    });
    if (dirty) { state.lots = { ...state.lots }; emit(); }
  }

  function applyWallet(me) {
    if (!me) return;
    if (state.wallet.deposit !== me.deposit || state.buyingPower !== me.power || state.walletCommitted !== me.committed) {
      state.wallet = { deposit: me.deposit, multiplier: 10 };
      state.buyingPower = me.power;
      state.walletCommitted = me.committed;
      emit();
    }
  }

  function pollRemote() {
    if (typeof Auction === "undefined" || (typeof document !== "undefined" && document.hidden)) return;
    Auction.list().then((res) => { if (res) { applyRemoteAuctions(res.auctions); applyWallet(res.me); } });
  }
  setInterval(pollRemote, 8000);
  setTimeout(pollRemote, 800);

  /* ---- shared operational records: orders / offers / claims ----
     Every signed-in user's records live in Firestore `ops`; admin (and
     consignor accounts, offers only) see everyone's. A diff-shadow stops
     echo loops between push and subscription. */
  const SEED_OPS_IDS = new Set([
    ...((typeof SEED_PURCHASES !== "undefined" ? SEED_PURCHASES : []).map((p) => p.id)),
    ...((typeof SEED_CLAIMS !== "undefined" ? SEED_CLAIMS : []).map((c) => c.id)),
  ]);
  let opsShadow = {}, opsUnsub = null;
  const opsKey = (t, r) => t + ":" + r.id + ":" + (r._doc || "");
  const opsJson = (r) => { const c = { ...r }; delete c._doc; delete c._owner; return JSON.stringify(c); };

  function syncOpsRecords() {
    if (typeof FB === "undefined" || !FB.enabled || !FB.currentUser()) return;
    [["order", state.purchases], ["offer", state.offers], ["claim", state.claims], ["listing", state.listingRequests]].forEach(([type, arr]) => {
      arr.forEach((rec) => {
        if (!rec || !rec.id || SEED_OPS_IDS.has(rec.id)) return;
        const j = opsJson(rec);
        if (opsShadow[opsKey(type, rec)] !== j) { opsShadow[opsKey(type, rec)] = j; FB.opsPush(type, rec); }
      });
    });
  }
  state.__syncOps = syncOpsRecords;

  function applyOpsDocs(docs) {
    let changed = false;
    const arrFor = (t) => (t === "order" ? "purchases" : t === "offer" ? "offers" : t === "claim" ? "claims" : t === "listing" ? "listingRequests" : null);
    docs.forEach(({ docId, type, owner, rec }) => {
      const name = arrFor(type);
      if (!name || !rec || !rec.id) return;
      const r = { ...rec, _doc: docId, _owner: owner };
      opsShadow[opsKey(type, r)] = opsJson(r);
      const arr = state[name];
      const i = arr.findIndex((x) => (x._doc && x._doc === docId) || (!x._doc && x.id === rec.id));
      if (i >= 0) {
        if (JSON.stringify(arr[i]) !== JSON.stringify(r)) { const cp = [...arr]; cp[i] = r; state[name] = cp; changed = true; }
      } else {
        state[name] = [r, ...arr]; changed = true;
      }
    });
    if (changed) emit();
  }

  function startOpsSync() {
    if (typeof FB === "undefined" || !FB.enabled) return;
    FB.onAuth((u) => {
      if (opsUnsub) { try { opsUnsub(); } catch (e) {} opsUnsub = null; }
      opsShadow = {};
      if (!u) return;
      const email = (u.email || "").toLowerCase();
      const mode = FB.isAdmin(u) ? "admin"
        : state.consignors.some((c) => (c.email || "").toLowerCase() === email) ? "consignor" : "own";
      opsUnsub = FB.opsWatch(applyOpsDocs, mode);
      syncOpsRecords(); // push anything created while signed out / before subscribe
    });
  }
  startOpsSync();

  function placeBidRemote(lotId, amount, { proxy } = {}) {
    const lot = state.lots[lotId];
    return Auction.bid(lotId, amount, proxy).then((r) => {
      if (lot && r && r.auction) { lot.iBid = true; if (r.ok) { lot.yourMax = Math.max(lot.yourMax || 0, amount); lot.yourTs = lot.yourTs || Date.now(); } applyRemoteAuctions({ [lotId]: r.auction }, true); }
      if (!r || !r.ok) return { ok: false, reason: (r && r.reason) || "Bid failed — try again." };
      return { ok: true, status: r.status, highBid: r.highBid };
    });
  }
  function buyNowRemote(lotId) {
    const lot = state.lots[lotId];
    return Auction.buyNow(lotId).then((r) => {
      if (lot && r && r.auction) { lot.iBid = true; applyRemoteAuctions({ [lotId]: r.auction }, true); }
      if (!r || !r.ok) return { ok: false, reason: (r && r.reason) || "Purchase failed — try again." };
      return { ok: true, status: "won" };
    });
  }

  return {
    subscribe, getState, toggleWatch, placeBid, buyNow, setShipTo, setLang,
    placeBidRemote, buyNowRemote,
    freightFor, setFreightRates, resetFreight,
    notify, dismissToast, markNotifsRead, minNextBid, advancePurchase,
    makeOffer, acceptCounter, declineOffer, offerFor, committed, availablePower,
    addLot, updateLot, setLotHidden, removeLot, closeAuction, extendAuction,
    addStaff, updateStaff, removeStaff, updateClaim, addClaim, setPurchaseStage, setBuyerPref, setBuyerIdentity, reviewListing,
    paymentStatus, payPurchase, claimPaymentSent, pickupPurchase,
    addonCosts, setPurchaseAddon, topUpDeposit, setBuyerCurrency,
    respondOffer, relistUnsold, scheduleLane,
    addDealer, updateDealer, removeDealer, resendDealerInvite, clearDealerEDD,
    commissionRate, commissionFor, freightQuoted, fxSpreadRevenue, revenueFor, setConfig,
    createSettlement, advanceSettlement, addConsignor, updateConsignor, removeConsignor,
    saveSearch, removeSearch, toggleSearchAlerts, matchSearch, savedMatchCount,
  };
})();

/* ---- helpers used above (defined here so engine is self-contained) ---- */
function estFees(lot) { return TOTAL_BUYER_FEES; } // flat $345 buyer + $170 title/inspection
function shipLabel(code) { const d = SHIP_DEST.find(s => s.code === code); return d ? d.label.replace(/^[^ ]+ /, "") : code; }

/* ---- React binding ---- */
function useStore() {
  return React.useSyncExternalStore(Engine.subscribe, Engine.getState);
}
function useLot(id) {
  const s = useStore();
  return s.lots[id];
}

Object.assign(window, { Engine, useStore, useLot, estFees, shipLabel });
