/* =============================================================
   CoreIQ Office — Promotions (LIVE · this pharmacy)

   Author here; the register executes. Publishing writes to the two tables
   the till already reads — a promo price per product (pricing_profiles) or
   one multibuy rule plus a pricing group (promotion_rules) — and PowerSync
   carries them to the registers within seconds. A draft changes nothing.

   Ported from the design's Promotions shell + builder + detail, restricted
   to the five mechanics the till can execute today. Threshold, second-unit,
   gift-with-purchase and member price arrive with the till release.

   Registers window.OFFICE_SCREENS.promotions / .promotionNew / .promotionDetail
   ============================================================= */
const { useState: pmState, useEffect: pmEffect, useMemo: pmMemo } = React;

const pmM = (c) => (c == null ? "—" : "$" + (Number(c) / 100).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
const pmSigned = (c) => (c == null ? "—" : (c > 0 ? "+" : c < 0 ? "−" : "") + pmM(Math.abs(c)));
const pmPct = (v) => (v == null ? "—" : Number(v).toFixed(1) + "%");
const pmDmy = (iso) => (iso ? iso.slice(8, 10) + "/" + iso.slice(5, 7) + "/" + iso.slice(0, 4) : "—");
const pmAddDays = (iso, d) => new Date(Date.parse(iso + "T00:00:00Z") + d * 86400000).toISOString().slice(0, 10);
const pmDays = (a, b) => Math.round((Date.parse(b) - Date.parse(a)) / 86400000);

const PM_STATE = {
  draft:     { l: "Draft",     bg: "var(--bg-subtle)", fg: "var(--text-subtle)", bd: "var(--line)" },
  scheduled: { l: "Scheduled", bg: "var(--navy-100)",  fg: "var(--navy-800)",    bd: "var(--navy-200)" },
  live:      { l: "Live",      bg: "var(--paid-bg)",   fg: "var(--paid-text)",   bd: "var(--paid)" },
  paused:    { l: "Paused",    bg: "var(--hold-bg)",   fg: "var(--hold-text)",   bd: "var(--hold)" },
  ended:     { l: "Ended",     bg: "var(--bg-subtle)", fg: "var(--text-subtle)", bd: "var(--line)" },
  cancelled: { l: "Cancelled", bg: "var(--bg-subtle)", fg: "var(--text-subtle)", bd: "var(--line)" },
};
const PM_ACTION = {
  publish: { l: "Publish", primary: true, confirm: "Publish this promotion? It reaches the registers within seconds." },
  pause: { l: "Pause", confirm: "Pause it? The registers stop applying it until you resume." },
  resume: { l: "Resume" },
  endEarly: { l: "End early", danger: true, confirm: "End it now? Prices revert on the registers and it cannot be resumed." },
  extend: { l: "Extend" },
  cancel: { l: "Cancel", danger: true, confirm: "Cancel it? It will not start." },
  startNow: { l: "Start now", confirm: "Start today instead of the scheduled date?" },
  rerun: { l: "Run it again" },
  edit: { l: "Edit" },
  discard: { l: "Discard", danger: true, confirm: "Discard this draft?" },
};
const PM_API_ACTION = { publish: "publish", pause: "pause", resume: "resume", endEarly: "end", cancel: "cancel", startNow: "start-now", rerun: "rerun" };

function PmStateTag({ state }) {
  const s = PM_STATE[state] || PM_STATE.draft;
  return <span style={{ display: "inline-block", fontSize: 10, fontWeight: 700, letterSpacing: ".04em", textTransform: "uppercase", padding: "2px 7px", borderRadius: 4, background: s.bg, color: s.fg, border: "1px solid " + s.bd, whiteSpace: "nowrap" }}>{s.l}</span>;
}

function PmErr({ text }) {
  return text ? <div className="off-card" style={{ borderColor: "var(--void-border)", background: "var(--void-bg)", color: "var(--void-text)", padding: "12px 16px", marginBottom: 14 }}><window.OIcon name="alert" /> {text}</div> : null;
}

// Runs a lifecycle action with a confirm where the design asks for one.
async function pmRunAction(id, key, extra, pushToast, onDone) {
  const a = PM_ACTION[key];
  if (a && a.confirm && !window.confirm(a.confirm)) return;
  try {
    const r = await window.OfficeAPI.promotionAction(id, PM_API_ACTION[key] || key, extra);
    pushToast && pushToast({ kind: "ok", text: (a ? a.l : key) + " — done." });
    onDone && onDone(r);
  } catch (e) {
    pushToast && pushToast({ kind: "error", text: e && e.message ? e.message : "That didn't work." });
  }
}

// =================================================================
// LIST + CALENDAR
// =================================================================
function PmCalendar({ promotions, today }) {
  const start = pmAddDays(today, -7);
  const DAYS = 42;
  const rows = promotions.filter((p) => p.state !== "draft" && p.state !== "cancelled" && pmDays(start, p.endsOn) >= 0 && pmDays(start, p.startsOn) < DAYS);
  const weeks = Array.from({ length: 6 }, (_, i) => pmAddDays(start, i * 7));
  return (
    <div className="off-card">
      <div className="off-card-header"><div><div className="off-card-title">Six weeks out</div><div className="off-card-meta">Where two bars overlap the register gives the customer whichever is better — that is the point of this view.</div></div></div>
      <div style={{ padding: "4px 16px 14px", overflowX: "auto" }}>
        <div style={{ display: "grid", gridTemplateColumns: "220px repeat(6, 1fr)", fontSize: "var(--fs-11)", color: "var(--text-subtle)", borderBottom: "1px solid var(--line)", paddingBottom: 6, minWidth: 640 }}>
          <div />{weeks.map((w) => <div key={w} style={{ fontFamily: "var(--font-mono)" }}>{pmDmy(w).slice(0, 5)}</div>)}
        </div>
        {rows.length === 0 && <div style={{ padding: "14px 0", fontSize: "var(--fs-12)", color: "var(--text-subtle)" }}>Nothing scheduled or live in this window.</div>}
        {rows.map((p) => {
          const s = Math.max(0, pmDays(start, p.startsOn)), e = Math.min(DAYS, pmDays(start, p.endsOn) + 1);
          const st = PM_STATE[p.state];
          return (
            <div key={p.id} style={{ display: "grid", gridTemplateColumns: "220px 1fr", alignItems: "center", minWidth: 640, borderBottom: "1px solid var(--line)" }}>
              <div style={{ padding: "8px 8px 8px 0", fontSize: "var(--fs-12)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}><b>{p.name}</b><div style={{ color: "var(--text-subtle)" }}>{p.say} · {p.scopeLabel}</div></div>
              <div style={{ position: "relative", height: 34 }}>
                <div style={{ position: "absolute", left: (pmDays(start, today) / DAYS * 100) + "%", top: 0, bottom: 0, width: 1, background: "var(--brand)", opacity: .6 }} title="today" />
                <div title={pmDmy(p.startsOn) + " → " + pmDmy(p.endsOn)} style={{ position: "absolute", left: (s / DAYS * 100) + "%", width: (Math.max(1, e - s) / DAYS * 100) + "%", top: 8, height: 18, borderRadius: 4, background: st.bg, border: "1px solid " + st.bd, color: st.fg, fontSize: 10, fontWeight: 700, padding: "1px 6px", overflow: "hidden", whiteSpace: "nowrap" }}>{p.where === "basket" ? "register rule" : "shelf price"}</div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function Promotions() {
  const { setScreen, setScreenState, pushToast } = window.useOffice();
  const Kpi = window.KpiCard;
  const [data, setData] = pmState(null);
  const [err, setErr] = pmState(null);
  const [loading, setLoading] = pmState(true);
  const [tab, setTab] = pmState("active");
  const [reloadKey, setReloadKey] = pmState(0);

  pmEffect(() => {
    let alive = true; setLoading(true); setErr(null);
    window.OfficeAPI.promotions().then((d) => { if (alive) setData(d); }).catch((e) => { if (alive) setErr(e && e.message ? e.message : "Couldn't load promotions."); }).finally(() => { if (alive) setLoading(false); });
    return () => { alive = false; };
  }, [reloadKey]);

  const all = (data && data.promotions) || [];
  const counts = (data && data.counts) || {};
  const TABS = [
    { k: "active", label: "Live & scheduled", n: (counts.live || 0) + (counts.scheduled || 0) + (counts.paused || 0) },
    { k: "draft", label: "Drafts", n: counts.draft || 0 },
    { k: "past", label: "Ended", n: (counts.ended || 0) + (counts.cancelled || 0) },
    { k: "all", label: "All", n: all.length },
  ];
  const rows = all.filter((p) => tab === "all" || (tab === "active" ? ["live", "scheduled", "paused"].includes(p.state) : tab === "draft" ? p.state === "draft" : ["ended", "cancelled"].includes(p.state)));
  const open = (p) => { setScreenState((st) => ({ ...st, promotionId: p.id })); setScreen("promotionDetail"); };
  const create = () => { setScreenState((st) => ({ ...st, promotionEditId: null, promotionRerunOf: null })); setScreen("promotionNew"); };
  const live = all.filter((p) => p.state === "live");
  const endingSoon = live.filter((p) => data && pmDays(data.today, p.endsOn) <= 7).length;

  return (
    <div className="off-screen">
      <header className="off-screen-header">
        <div className="off-screen-header-meta">
          <span className="off-screen-eyebrow">live · this pharmacy · writes to the registers</span>
          <h2 className="off-screen-title">Promotions</h2>
        </div>
        <button className="btn btn--primary btn--sm" onClick={create}><window.OIcon name="plus" /> New promotion</button>
      </header>
      <div className="off-screen-body">
        <PmErr text={err} />
        {data && Kpi && (
          <div className="off-grid-kpis" style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginBottom: 18 }}>
            <Kpi label="Live now" value={String(live.length)} delta={{ text: live.filter((p) => p.where === "basket").length + " are register rules, not shelf prices" }} />
            <Kpi label="Ending within 7 days" value={String(endingSoon)} delta={{ text: "price reverts to the shelf price then" }} />
            <Kpi label="Scheduled" value={String(counts.scheduled || 0)} delta={{ text: "already on the registers, waiting for the date" }} />
            <Kpi label="Supplier funded" value={String(all.filter((p) => p.funded && ["live", "scheduled", "paused"].includes(p.state)).length)} delta={{ text: "co-op claims to raise" }} />
          </div>
        )}
        {data && <div style={{ marginBottom: 16 }}><PmCalendar promotions={all} today={data.today} /></div>}
        <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 14, flexWrap: "wrap" }}>
          <div className="tabset">{TABS.map((t) => <button key={t.k} className="tabset-item" aria-selected={tab === t.k} onClick={() => setTab(t.k)}>{t.label}{t.n ? <span style={{ marginLeft: 6, opacity: .7, fontFamily: "var(--font-mono)", fontSize: "var(--fs-11)" }}>{t.n}</span> : null}</button>)}</div>
          <button className="btn btn--ghost btn--sm" style={{ marginLeft: "auto" }} onClick={() => setReloadKey((k) => k + 1)}><window.OIcon name="refresh" /> Refresh</button>
        </div>
        <div className="off-card">
          {!loading && rows.length === 0 && (
            <div style={{ padding: "28px 16px", textAlign: "center", color: "var(--text-subtle)", fontSize: "var(--fs-13)" }}>
              {all.length === 0 ? "No promotions yet. The first one you publish reaches the registers within seconds." : "Nothing in this tab."}
            </div>
          )}
          {rows.length > 0 && (
            <div className="off-grid" style={{ gridTemplateColumns: "minmax(220px, 2fr) 120px minmax(140px, 1fr) 150px 70px 96px" }}>
              <div className="off-grid-headcell">Promotion</div><div className="off-grid-headcell">Offer</div><div className="off-grid-headcell">Scope</div><div className="off-grid-headcell">Dates</div><div className="off-grid-headcell" style={{ justifyContent: "flex-end" }}>Lines</div><div className="off-grid-headcell">State</div>
              {rows.map((p) => (
                <div key={p.id} className="off-grid-row" style={{ cursor: "pointer" }} onClick={() => open(p)}>
                  <div className="off-grid-cell off-cell-strong" style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
                    <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</span>
                    <span style={{ fontSize: "var(--fs-11)", color: "var(--text-subtle)", fontWeight: 400 }}>{p.mechanicLabel}{p.where === "basket" ? " · register rule" : " · shelf price"}{p.funded ? " · funded by " + p.funded.supplier : ""}</span>
                  </div>
                  <div className="off-grid-cell" style={{ fontFamily: "var(--font-mono)", fontSize: "var(--fs-12)" }}>{p.say}</div>
                  <div className="off-grid-cell" style={{ fontSize: "var(--fs-12)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.scopeLabel}</div>
                  <div className="off-grid-cell" style={{ fontFamily: "var(--font-mono)", fontSize: "var(--fs-12)" }}>{pmDmy(p.startsOn)} → {pmDmy(p.endsOn)}</div>
                  <div className="off-grid-cell off-cell-num">{p.lineCount}</div>
                  <div className="off-grid-cell"><PmStateTag state={p.state} /></div>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// =================================================================
// BUILDER — the design's New promotion, on real data.
// Every section, its copy and its order come from the design prototype
// (office-screens-promotions-4.jsx). Numbers on the right come from
// /promotions/preview, which runs the design's planning maths server-side
// against the stock ledger, eight weeks of sales and observed competitor
// prices. Nothing reaches a register until you publish.
// =================================================================
const pmSay = (id, p) => {
  const M = (c) => pmM(c);
  switch (id) {
    case "pct": return (p.pct || 0) + "% off";
    case "amount": return M(p.offCents || 0) + " off";
    case "fixed": return M(p.priceCents || 0);
    case "member": return M(p.priceCents || 0) + " members";
    case "multibuy": return (p.qty || 0) + " for " + M(p.bundleCents || 0);
    case "mixmatch": return "any " + (p.qty || 0) + " for " + M(p.bundleCents || 0);
    case "threshold": return M(p.offCents || 0) + " off " + M(p.spendCents || 0);
    case "bundle": return "buy one, second line " + (p.bPct || 0) + "% off";
    case "secondhalf": return "2nd unit " + (p.pct || 50) + "% off";
    case "gwp": return ("free " + (p.gift || "")).trim();
    default: return "";
  }
};
const pmShort = (name) => String(name || "").split(" ").slice(0, 3).join(" ");

// One ticket — real artwork at real size. Ported from the design's shelf-ticket screen.
function PgTicket({ name, sku, beforeCents, priceCents, say, startsOn, endsOn, theme, themes, isPrice, member, clearance }) {
  const t = (themes && themes[theme]) || (themes && themes.red) || { flag: "#d81f2a", fg: "#fff", ink: "#b3141d", tint: "#fdecec" };
  const showPrice = isPrice && priceCents != null;
  const save = showPrice ? beforeCents - priceCents : null;
  const dollars = showPrice ? Math.floor(priceCents / 100) : "";
  const cents = showPrice ? String(priceCents % 100).padStart(2, "0") : "";
  return (
    <div className={"tk tk--shelf" + (!showPrice ? " tk--offer" : "")} style={{ width: 70 * 3.78 + "px", height: 50 * 3.78 + "px", "--tk-flag": t.flag, "--tk-fg": t.fg, "--tk-ink": t.ink, "--tk-tint": t.tint }}>
      <div className="tk-top">
        <span className="tk-flag">{clearance ? "CLEARANCE" : member ? "MEMBER PRICE" : !showPrice ? "OFFER" : "SPECIAL"}</span>
        {save != null && save > 0 && <span className="tk-save">SAVE ${(save / 100).toFixed(2)}</span>}
      </div>
      <div className="tk-name" style={{ fontSize: "8.5px" }}>{name}</div>
      {showPrice ? (
        <div className="tk-price"><span className="tk-cur" style={{ fontSize: "13px" }}>$</span><span className="tk-d" style={{ fontSize: "38px" }}>{dollars}</span><span className="tk-c" style={{ fontSize: "15px" }}>{cents}</span></div>
      ) : (
        <div className="tk-offer" style={{ fontSize: "14.5px" }}><span>{String(say || "").toUpperCase()}</span></div>
      )}
      <div className="tk-bot">
        <span className="tk-was">{showPrice ? "Usually " + pmM(beforeCents) : pmM(beforeCents) + " each"}</span>
        <span className="tk-dates">{pmDmy(startsOn)} – {pmDmy(endsOn)}</span>
      </div>
      <div className="tk-fine">{member ? "Loyalty card must be presented. " : ""}{clearance ? "Short-dated stock. " : ""}While stocks last.<span className="tk-sku">{sku}</span></div>
    </div>
  );
}

function PromotionNew() {
  const { screenState, setScreen, setScreenState, pushToast, stores: orgStores } = window.useOffice();
  const editId = screenState.promotionEditId || null;
  const rerunOf = screenState.promotionRerunOf || null;
  const OIcon = window.OIcon;
  const [meta, setMeta] = pmState(null);
  const [opts, setOpts] = pmState(null);
  const [today, setToday] = pmState(null);
  const [name, setName] = pmState("");
  const [mech, setMech] = pmState("pct");
  const [p, setP] = pmState({ pct: 20, offCents: 500, priceCents: 999, qty: 3, bundleCents: 3000, spendCents: 5000, bPct: 25, b: "", gift: "" });
  const [level, setLevel] = pmState("category");
  const [key, setKey] = pmState("");
  const [stores, setStores] = pmState([]);
  const [from, setFrom] = pmState("");
  const [to, setTo] = pmState("");
  const [channels, setChannels] = pmState(["esl", "ticket"]);
  const [funded, setFunded] = pmState(false);
  const [supplier, setSupplier] = pmState("");
  const [perCents, setPerCents] = pmState(150);
  const [prompt, setPrompt] = pmState("");
  const [reason, setReason] = pmState("");
  const [theme, setTheme] = pmState("red");
  const [excl, setExcl] = pmState([]);
  const [preview, setPreview] = pmState(null);
  const [previewErr, setPreviewErr] = pmState(null);
  const [busy, setBusy] = pmState(false);
  const [loaded, setLoaded] = pmState(!editId && !rerunOf);

  pmEffect(() => {
    let alive = true;
    window.OfficeAPI.promotionsMeta().then((m) => { if (alive) setMeta(m); }).catch(() => {});
    window.OfficeAPI.promotionScopeOptions().then((o) => { if (alive) setOpts(o); }).catch((e) => { if (alive) setPreviewErr(e && e.message ? e.message : "Couldn't load the scope options."); });
    window.OfficeAPI.promotions().then((d) => { if (!alive) return; setToday(d.today); if (!editId && !rerunOf) { setFrom(d.today); setTo(pmAddDays(d.today, 13)); } }).catch(() => {});
    const src = editId || rerunOf;
    if (src) window.OfficeAPI.promotion(src).then((d) => {
      if (!alive) return;
      setName(d.name); setMech(d.mechanic); setP((s) => ({ ...s, ...d.params })); setLevel(d.scopeKind === "all" ? "group" : d.scopeKind); setKey(d.scopeRef || "");
      setChannels((d.channels || []).filter((c) => c !== "pos")); setExcl(d.exclusions || []); setPrompt(d.prompt || ""); setReason(d.why || ""); setTheme(d.ticketTheme || "red");
      if (d.funded) { setFunded(true); setSupplier(d.funded.supplier); setPerCents(d.funded.perCents); }
      if (editId) { setFrom(d.startsOn); setTo(d.endsOn); }
      else { const days = pmDays(d.startsOn, d.endsOn); window.OfficeAPI.promotions().then((x) => { if (alive) { setFrom(x.today); setTo(pmAddDays(x.today, days)); } }).catch(() => {}); }
      setLoaded(true);
    }).catch((e) => setPreviewErr(e.message));
    return () => { alive = false; };
  }, [editId, rerunOf]);

  // one store today; the chip row is the design's, the list is real
  pmEffect(() => { if (orgStores && orgStores.length && stores.length === 0) setStores([orgStores[0].id]); }, [orgStores]);

  const M = (meta && meta.mechanics) || [];
  const m = M.find((x) => x.id === mech) || { id: mech, where: "price", label: mech, d: "" };
  const isPrice = m.where === "price";
  const params = mech === "pct" ? { pct: Number(p.pct) } : mech === "amount" ? { offCents: Number(p.offCents) } : (mech === "fixed" || mech === "member") ? { priceCents: Number(p.priceCents) }
    : (mech === "multibuy" || mech === "mixmatch") ? { qty: Number(p.qty), bundleCents: Number(p.bundleCents) } : mech === "threshold" ? { spendCents: Number(p.spendCents), offCents: Number(p.offCents) }
    : mech === "secondhalf" ? { pct: Number(p.pct) || 50 } : mech === "bundle" ? { bPct: Number(p.bPct), b: p.b } : { gift: p.gift, qty: Number(p.qty) };
  const scopeKind = level === "group" ? "all" : level;
  const input = { name: name || "Untitled promotion", mechanic: mech, params, scopeKind, scopeRef: scopeKind === "all" ? null : key || null, exclusions: excl, startsOn: from, endsOn: to,
    why: reason || null, prompt: channels.includes("prompt") ? prompt || null : null, channels: ["pos", ...channels], ticketTheme: theme,
    funded: funded && supplier ? { supplier, perCents: Number(perCents) || 0 } : null, siteId: stores[0] || null };
  const scoped = scopeKind === "all" || !!key;
  const ready0 = !!from && !!to && scoped && loaded;

  pmEffect(() => {
    if (!ready0) { setPreview(null); return; }
    let alive = true;
    const t = setTimeout(() => {
      window.OfficeAPI.previewPromotion(input).then((pv) => { if (alive) { setPreview(pv); setPreviewErr(null); } }).catch((e) => { if (alive) { setPreview(null); setPreviewErr(e && e.message ? e.message : "Couldn't preview."); } });
    }, 250);
    return () => { alive = false; clearTimeout(t); };
  }, [JSON.stringify(input), ready0]);

  const lines = (preview && preview.lines) || [];
  const affected = lines.filter((l) => !l.held);
  const held = lines.filter((l) => l.held);
  const sum = preview && preview.summary;
  const breaches = affected.filter((l) => l.under);
  const overlaps = affected.filter((l) => l.existing && l.existing.length);
  const stock = affected.filter((l) => l.stock);
  const shortL = stock.filter((l) => l.stock.short);
  const heavy = stock.filter((l) => l.stock.heavy);
  const comp = affected.filter((l) => l.landing);
  const overshot = comp.filter((l) => l.landing.overshootCents > 5).sort((a, b) => b.landing.overshootCents - a.landing.overshootCents);
  const stillLosing = comp.filter((l) => l.promoCents != null && l.landing.cheapest && l.promoCents > l.landing.cheapest.priceCents).sort((a, b) => (b.promoCents - b.landing.cheapest.priceCents) - (a.promoCents - a.landing.cheapest.priceCents));
  const roll = (sum && sum.roll) || { wins: 0, losses: 0, noData: 0, over: 0, total: 0 };
  const durDays = sum ? sum.days : 0;
  const storeName = (sum && sum.storeName) || (orgStores && orgStores[0] && orgStores[0].name) || "this store";
  const ready = name.trim().length > 2 && stores.length > 0 && affected.length > 0 && !(preview && preview.problem) && (!breaches.length || reason.trim().length > 3) && m.available !== false;

  const save = async (asDraft) => {
    setBusy(true);
    try {
      const body = { ...input, name: name.trim() };
      const d = editId ? await window.OfficeAPI.updatePromotion(editId, body) : await window.OfficeAPI.createPromotion(body);
      if (!asDraft) {
        if (!window.confirm("Publish \"" + name.trim() + "\"? " + (isPrice ? "It pushes " + affected.length + " price" + (affected.length === 1 ? "" : "s") + " to the registers within seconds." : "The rule reaches every till within seconds."))) { setBusy(false); setScreenState((st) => ({ ...st, promotionId: d.id, promotionEditId: null, promotionRerunOf: null })); setScreen("promotionDetail"); return; }
        await window.OfficeAPI.promotionAction(d.id, "publish");
      }
      pushToast({ kind: asDraft ? "hold" : "ok", text: asDraft ? "Saved as draft · " + name.trim() + " — not visible at any register until you publish it." : (editId ? "Changes saved · " : "Promotion created · ") + name.trim() + " · " + affected.length + " line" + (affected.length === 1 ? "" : "s") + " · " + (isPrice ? "prices reach the registers within seconds" : "rule cached on every till") });
      setScreenState((st) => ({ ...st, promotionId: d.id, promotionEditId: null, promotionRerunOf: null })); setScreen(asDraft ? "promotionDetail" : "promotions");
    } catch (e) { pushToast({ kind: "error", text: e && e.message ? e.message : "Couldn't save." }); }
    finally { setBusy(false); }
  };

  const Chip = ({ on, onClick, children, title }) => <button className={"pb-chip" + (on ? " is-on" : "")} onClick={onClick} title={title}>{children}</button>;
  const setKeyFor = (l) => { setLevel(l); setKey(""); };
  const cats = (opts && opts.categories) || [], brands = (opts && opts.brands) || [], prods = (opts && opts.products) || [];
  const setsAll = (meta && meta.exclusionSets) || [];
  const chans = ((meta && meta.channels) || []).filter((c) => !c.always);
  const themes = (meta && meta.ticketThemes) || {};
  const heldCountFor = (id) => held.filter((h) => h.held.set === id).length;
  const cents = (v, set, step) => <input className="input" type="number" step={step || "0.5"} min="0" value={(Number(v) / 100).toFixed(2)} onChange={(e) => set(Math.round(Number(e.target.value || 0) * 100))} />;
  const set = (k, v) => setP((s) => ({ ...s, [k]: v }));

  return (
    <div className="off-screen">
      <header className="off-screen-header">
        <div className="off-screen-header-meta">
          <button className="sup-back" onClick={() => setScreen("promotions")}><OIcon name="chevron-left" /> Promotions</button>
          <h2 className="off-screen-title">{editId ? "Edit promotion" : rerunOf ? "Run it again" : "New promotion"}</h2>
          <div className="off-muted" style={{ fontSize: 13, marginTop: 4 }}>
            {editId ? "Changes take effect at the next register sync. Shelf tickets will need reprinting if the price moves."
              : rerunOf ? "Copied from the earlier run. Give it new dates — everything else carried over."
              : "The panel on the right updates as you build. Nothing reaches a register until you publish."}
          </div>
        </div>
        <div className="off-screen-actions">
          <button className="btn btn--ghost btn--sm" onClick={() => setScreen("promotions")}>Cancel</button>
          <button className="btn btn--secondary btn--sm" disabled={busy || name.trim().length < 3} onClick={() => save(true)}>Save as draft</button>
          <button className="btn btn--primary btn--sm" disabled={busy || !ready} onClick={() => save(false)}><OIcon name="check" /> {editId ? "Save changes" : "Publish"}</button>
        </div>
      </header>

      <div className="off-screen-body">
        <PmErr text={previewErr} />
        <div className="pb-grid">
          {/* ---------------- form ---------------- */}
          <div className="pb-col">
            <div className="off-table pg-card">
              <div className="pg-card-h">Name it</div>
              <input className="input" style={{ marginTop: 9 }} placeholder="e.g. Vitamins — 20% off the range" value={name} onChange={(e) => setName(e.target.value)} />
              <div className="pg-card-s">This is what prints on the ticket and shows on the receipt, so write it the way a customer would read it.</div>
            </div>

            <div className="off-table pg-card">
              <div className="pg-card-h">Mechanic</div>
              <div className="pg-card-s">Price mechanics resolve to a number the register receives. Register rules are evaluated at the till against the whole basket.</div>
              <div className="pb-mechs">
                {M.map((x) => (
                  <button key={x.id} className={"pb-mech" + (mech === x.id ? " is-on" : "")} disabled={x.available === false} title={x.available === false ? x.unavailableReason : x.d} style={x.available === false ? { opacity: .45, cursor: "not-allowed" } : undefined} onClick={() => setMech(x.id)}>
                    <span className="pb-mech-l">{x.label}</span>
                    <span className={"pg-where pg-where--" + x.where}>{x.where === "price" ? "price" : "rule"}</span>
                  </button>
                ))}
              </div>
              <div className="pb-params">
                {mech === "pct" && <label><span>Percent off</span><input className="input" type="number" min="1" max="99" value={p.pct} onChange={(e) => set("pct", +e.target.value)} /></label>}
                {mech === "amount" && <label><span>Dollars off</span>{cents(p.offCents, (v) => set("offCents", v))}</label>}
                {(mech === "fixed" || mech === "member") && <label><span>{mech === "member" ? "Member price" : "Promo price"}</span>{cents(p.priceCents, (v) => set("priceCents", v))}</label>}
                {(mech === "multibuy" || mech === "mixmatch") && <><label><span>Quantity</span><input className="input" type="number" min="2" value={p.qty} onChange={(e) => set("qty", +e.target.value)} /></label><label><span>For</span>{cents(p.bundleCents, (v) => set("bundleCents", v))}</label></>}
                {mech === "threshold" && <><label><span>Spend at least</span>{cents(p.spendCents, (v) => set("spendCents", v), "1")}</label><label><span>Take off</span>{cents(p.offCents, (v) => set("offCents", v), "1")}</label></>}
                {mech === "secondhalf" && <label><span>Second unit off</span><input className="input" type="number" value={p.pct || 50} onChange={(e) => set("pct", +e.target.value)} /></label>}
                {mech === "bundle" && <><label><span>Second line</span><input className="input" value={p.b} placeholder="e.g. Nicabate gum 2mg 30s" onChange={(e) => set("b", e.target.value)} /></label><label><span>Discount %</span><input className="input" type="number" value={p.bPct} onChange={(e) => set("bPct", +e.target.value)} /></label></>}
                {mech === "gwp" && <><label><span>Gift</span><input className="input" value={p.gift} placeholder="e.g. Pigeon bottle brush" onChange={(e) => set("gift", e.target.value)} /></label><label><span>Buy quantity</span><input className="input" type="number" value={p.qty} onChange={(e) => set("qty", +e.target.value)} /></label></>}
              </div>
              <div className="pb-say">Reads as <strong>{pmSay(mech, params)}</strong></div>
            </div>

            <div className="off-table pg-card">
              <div className="pg-card-h">What it applies to</div>
              <div className="pg-card-s">Same hierarchy as pricing rules. A category promotion covers every line in it, now and any added later.</div>
              <div className="pb-levels">
                {[["group", "Everything"], ["category", "Category"], ["brand", "Brand"], ["sku", "Single line"]].filter(([l]) => mech !== "multibuy" || l === "sku").map(([l, lab]) => <Chip key={l} on={level === l} onClick={() => setKeyFor(l)}>{lab}</Chip>)}
              </div>
              {level === "category" && <select className="input" style={{ marginTop: 10 }} value={key} onChange={(e) => setKey(e.target.value)}><option value="">Choose a category…</option>{cats.map((c) => <option key={c.id} value={c.id}>{c.path}</option>)}</select>}
              {level === "brand" && <select className="input" style={{ marginTop: 10 }} value={key} onChange={(e) => setKey(e.target.value)}><option value="">Choose a brand…</option>{brands.map((b) => <option key={b} value={b}>{b}</option>)}</select>}
              {level === "sku" && <select className="input" style={{ marginTop: 10 }} value={key} onChange={(e) => setKey(e.target.value)}><option value="">Choose a line…</option>{prods.map((x) => <option key={x.id} value={x.id}>{x.name}{x.sku ? " · " + x.sku : ""} · {pmM(x.sellPriceCents)}</option>)}</select>}
              {level === "group" && <div className="xp-note" style={{ marginTop: 10 }}>Every front-of-shop line. Prescriptions, S8 and consignment stock are excluded automatically.</div>}
            </div>

            <div className="off-table pg-card">
              <div className="pg-card-h">Exclusions</div>
              <div className="pg-card-s">Applied before anything else. The locked sets are not ours to discount, so they are held out of every promotion whatever its scope.</div>
              <div className="pb-excl">
                {setsAll.map((s) => {
                  const on = s.mandatory || excl.includes(s.id);
                  const caught = heldCountFor(s.id);
                  return (
                    <div key={s.id} className={"pb-ex" + (on ? " is-on" : "") + (s.mandatory ? " is-locked" : "")} onClick={() => { if (!s.mandatory) setExcl((v) => v.includes(s.id) ? v.filter((x) => x !== s.id) : [...v, s.id]); }}>
                      <span className={"xp-yn " + (on ? "is-y" : "is-n")}><OIcon name={s.mandatory ? "lock" : on ? "check" : "x"} /></span>
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div className="pb-ex-l">{s.label}{s.mandatory && <span className="pb-ex-lock">always</span>}</div>
                        <div className="pb-ex-w">{s.why}</div>
                      </div>
                      {caught > 0 && <span className="pb-ex-n">{caught} in scope</span>}
                    </div>
                  );
                })}
              </div>
              {held.length > 0 && <div className="xp-note" style={{ marginTop: 10 }}>Holding back {held.length} line{held.length === 1 ? "" : "s"}: {held.slice(0, 6).map((h) => pmShort(h.name)).join(", ")}{held.length > 6 ? ", …" : ""}.</div>}
            </div>

            <div className="off-table pg-card">
              <div className="pg-card-h">Where and when</div>
              <div className="pb-levels" style={{ marginTop: 9 }}>
                {(orgStores || []).map((s) => <Chip key={s.id} on={stores.includes(s.id)} onClick={() => setStores([s.id])}>{s.name}</Chip>)}
              </div>
              <div className="pb-dates">
                <label><span>Starts</span><input className="input" type="date" value={from} min={today || undefined} onChange={(e) => setFrom(e.target.value)} /></label>
                <label><span>Ends</span><input className="input" type="date" value={to} min={from || undefined} onChange={(e) => setTo(e.target.value)} /></label>
              </div>
              <div className="xp-note" style={{ marginTop: 10 }}>Ends at close of trade on the last day. The price then returns to whatever the pricing engine says at that point, not to today's price.</div>
            </div>

            <div className="off-table pg-card">
              <div className="pg-card-h">Where it shows</div>
              <div className="pb-levels" style={{ marginTop: 9 }}>
                <Chip on={true} onClick={() => {}}>Register &amp; receipt</Chip>
                {chans.map((c) => <Chip key={c.id} on={channels.includes(c.id)} onClick={() => setChannels((s) => s.includes(c.id) ? s.filter((x) => x !== c.id) : [...s, c.id])}>{c.label}</Chip>)}
              </div>
              {channels.includes("prompt") && <><label className="xp-lab">What the till says to staff</label><input className="input" value={prompt} placeholder="e.g. Second Blackmores item is half price" onChange={(e) => setPrompt(e.target.value)} /></>}
            </div>

            <div className="off-table pg-card">
              <div className="pg-card-h">Supplier funding</div>
              <button className={"iaq-toggle" + (funded ? " is-on" : "")} style={{ marginTop: 9 }} onClick={() => setFunded(!funded)}>
                <span className="iaq-toggle-sw"><span /></span>
                <span><span className="iaq-toggle-l">A supplier funds part of this</span><span className="iaq-toggle-h">Changes the true margin and creates a claim against their statement.</span></span>
              </button>
              {funded && <div className="pb-dates"><label><span>Supplier</span><input className="input" value={supplier} placeholder="e.g. Sanofi CHC" onChange={(e) => setSupplier(e.target.value)} /></label><label><span>They fund per unit</span>{cents(perCents, setPerCents)}</label></div>}
            </div>
          </div>

          {/* ---------------- consequences ---------------- */}
          <div className="pb-col">
            <div className="off-table pg-card pb-sticky">
              <div className="pg-card-h">What this will do</div>
              <div className="pb-sum">
                <div><span>Lines affected</span><strong>{affected.length}</strong></div>
                <div><span>Stores</span><strong>{stores.length || "—"}</strong></div>
                <div><span>Avg margin after</span><strong className={sum && sum.avgMarginAfterPct != null && sum.avgMarginAfterPct < 15 ? "pc-up" : ""}>{sum && sum.avgMarginAfterPct != null ? Math.round(sum.avgMarginAfterPct) + "%" : isPrice ? "—" : "at till"}</strong></div>
                <div><span>Given up per unit</span><strong>{isPrice ? (sum && sum.givenUpPerUnitCents != null ? pmM(sum.givenUpPerUnitCents) : "—") : "varies"}</strong></div>
              </div>

              {ready0 && preview && affected.length === 0 && (
                <div className="xp-empty">
                  {held.length > 0
                    ? "Every line in this scope is held back by an exclusion. " + (held.some((h) => setsAll.find((s) => s.id === h.held.set && s.mandatory)) ? "At least one is a mandatory set, so this promotion cannot run as scoped." : "Turn one off above, or change the scope.")
                    : (preview.problem || "Nothing matches that scope yet.")}
                </div>
              )}
              {!ready0 && <div className="xp-empty">Choose the scope and dates to see what this will do.</div>}

              {stock.length > 0 && (
                <div className="pb-stock">
                  <div className="pg-card-h" style={{ marginBottom: 6 }}>Will the stock last {durDays} days?</div>
                  {shortL.length === 0 && heavy.length === 0 && <div className="pb-st-ok"><span className="xp-yn is-y"><OIcon name="check" /></span><span>Every line covers the run at the expected lift, with something left over. Nothing to do.</span></div>}
                  {shortL.length > 0 && sum.stock.worst && (
                    <div className="pc-flagbox"><OIcon name="alert" /><span>
                      <strong>{shortL.length === 1 ? "1 line runs out" : shortL.length + " lines run out"} before the end.</strong>{" "}
                      {pmShort(sum.stock.worst.name)} has {sum.stock.worst.onHand} on hand at {storeName} and would go on day {sum.stock.worst.runsOut} of {durDays}.
                      {sum.stock.worst.canReorder ? " Lead time is " + sum.stock.worst.leadDays + " days, so a reorder placed now still lands in time." : " Lead time is " + sum.stock.worst.leadDays + " days, which is too long to restock mid-promotion — either shorten the run or order before it starts."}
                    </span></div>
                  )}
                  {heavy.length > 0 && <div className="pb-st-warn"><OIcon name="info" /><span><strong>{heavy.length} line{heavy.length === 1 ? " is" : "s are"} over-covered.</strong> You will finish with promotion-priced stock on the shelf that then reverts to full price. Consider a shorter run or a deeper discount.</span></div>}
                  <div className="pb-strows">
                    {stock.slice(0, 6).map((l) => (
                      <div key={l.productId} className={"pb-strow" + (l.stock.short ? " is-short" : "")}>
                        <div style={{ minWidth: 0, flex: 1 }}><div className="pb-line-n">{l.name}</div><div className="pb-line-s">{l.stock.onHand} on hand · {Math.round(l.stock.weeklyUnits * 10) / 10}/wk normally · +{Math.round(l.stock.lift * 100)}% expected</div></div>
                        <div className="pb-st-v"><strong>{l.stock.short ? "day " + l.stock.runsOut : l.stock.leftOver + " left"}</strong><span>{l.stock.short ? "runs out" : "at the end"}</span></div>
                      </div>
                    ))}
                    {stock.length > 6 && <div className="pb-more">and {stock.length - 6} more</div>}
                  </div>
                  <div className="pb-st-note">Lift is a planning estimate from discount depth, whether the line is a KVI, and whether a ticket goes up. It is not a forecast — treat it as the order of magnitude, not the number.</div>
                </div>
              )}

              {comp.length > 0 && (
                <div className="pb-stock">
                  <div className="pg-card-h" style={{ marginBottom: 6 }}>Where this lands you</div>
                  {!isPrice ? (
                    <div className="pb-st-warn"><OIcon name="info" /><span>A {m.label.toLowerCase()} has no single unit price, so there is nothing to compare against a shelf price. The observed set below is context for the lines it covers, not a verdict.</span></div>
                  ) : (
                    <>
                      <div className="pb-cwrap">
                        <div className={"pb-cscore" + (roll.losses === 0 && roll.wins > 0 ? " is-all" : roll.wins === 0 ? " is-none" : "")}><strong>{roll.wins}</strong><span>of {roll.total - roll.noData} cheapest</span></div>
                        <div className="pb-cnote">
                          {roll.losses === 0 && roll.wins > 0 && "This depth makes you the cheapest observed price on every line it covers."}
                          {roll.losses > 0 && roll.wins > 0 && roll.losses + " line" + (roll.losses === 1 ? " is" : "s are") + " still beaten by someone even at this depth. Worth knowing before you print tickets calling it a special."}
                          {roll.wins === 0 && roll.losses > 0 && "This depth does not make you cheapest on any line. The promotion costs margin without winning the comparison."}
                          {roll.noData > 0 && " " + roll.noData + " line" + (roll.noData === 1 ? " has" : "s have") + " no usable observation."}
                        </div>
                      </div>
                      {overshot.length > 0 && <div className="pb-st-warn"><OIcon name="info" /><span><strong>Deeper than you need on {overshot.length === 1 ? "1 line" : overshot.length + " lines"}.</strong>{" "}{pmShort(overshot[0].name)} goes to {pmM(overshot[0].promoCents)}, but {pmM(overshot[0].landing.enoughCents)} would still have undercut {overshot[0].landing.cheapest.retailer} at {pmM(overshot[0].landing.cheapest.priceCents)} — {pmM(overshot[0].landing.overshootCents)} a unit given away for nothing.</span></div>}
                      {stillLosing.length > 0 && <div className="pc-flagbox"><OIcon name="alert" /><span><strong>Still beaten on {stillLosing.length === 1 ? "1 line" : stillLosing.length + " lines"}.</strong>{" "}{pmShort(stillLosing[0].name)} lands at {pmM(stillLosing[0].promoCents)} against {stillLosing[0].landing.cheapest.retailer} at {pmM(stillLosing[0].landing.cheapest.priceCents)}.</span></div>}
                    </>
                  )}
                  <div className="pb-crows">
                    {comp.slice(0, 5).map((l) => (
                      <div key={l.productId} className="pb-crow">
                        <div style={{ minWidth: 0, flex: 1 }}>
                          <div className="pb-line-n">{l.name}</div>
                          <div className="pb-cbar">
                            {l.landing.chips.slice(0, 4).map((c) => <span key={c.retailer} className={"pb-cchip" + (c.stale ? " is-stale" : "") + (l.promoCents == null ? " is-neutral" : l.promoCents <= c.priceCents ? " is-beat" : " is-lose")} title={c.retailer + " " + pmM(c.priceCents) + " · seen " + (c.ageDays === 0 ? "today" : c.ageDays === 1 ? "yesterday" : c.ageDays + " days ago") + (c.onSale ? " · looks like their promotional price" : "")}>{c.retailer} {pmM(c.priceCents)}{c.onSale && <em>sale</em>}</span>)}
                            {l.landing.chips.length === 0 && <span className="pb-cchip is-stale">not observed</span>}
                          </div>
                        </div>
                        <div className="pb-st-v"><strong>{l.promoCents != null ? pmM(l.promoCents) : "at till"}</strong><span>{l.promoCents == null ? "no verdict" : l.landing.position || "—"}</span></div>
                      </div>
                    ))}
                    {comp.length > 5 && <div className="pb-more">and {comp.length - 5} more</div>}
                  </div>
                  <div className="pb-st-note">
                    {comp.some((l) => l.landing.anyStale) ? "Greyed prices are older than three days and should not be trusted. " : ""}
                    {comp.some((l) => l.landing.chips.some((c) => c.onSale)) ? <>Prices marked <em className="pb-cat">sale</em> look like the competitor's own promotional price, not their shelf price — matching a catalogue special with a permanent promotion is how margin leaks. </> : null}
                    Nothing here sets your depth; that is your call.
                  </div>
                </div>
              )}

              {breaches.length > 0 && <div className="pc-flagbox"><OIcon name="alert" /><span><strong>{breaches.length} line{breaches.length === 1 ? " goes" : "s go"} below cost.</strong> Permitted with a reason, which is recorded against the promotion.</span></div>}
              {breaches.length > 0 && <textarea className="input" rows={2} style={{ marginTop: 8 }} placeholder="Why is below cost the right call here?" value={reason} onChange={(e) => setReason(e.target.value)} />}

              {overlaps.length > 0 && (
                <div className="pb-overlap">
                  <div className="pg-card-h" style={{ marginBottom: 5 }}>Already promoted</div>
                  {overlaps.slice(0, 3).map((l) => <div key={l.productId} className="pb-ov"><div style={{ minWidth: 0 }}><div className="pb-ov-n">{l.name}</div><div className="pb-ov-s">{l.existing.map((x) => x.name).join(" · ")}</div></div><span className="pg-cand-b">{isPrice && l.existing.some((x) => x.where === "price") ? "will compete" : "will stack"}</span></div>)}
                  <div className="pb-ov-note">Two shelf prices never stack — the customer gets the better one. A shelf price and a register rule both apply.</div>
                </div>
              )}

              {isPrice && affected.length > 0 && (
                <div className="pb-lines">
                  <div className="pg-card-h" style={{ marginBottom: 6 }}>New prices</div>
                  {affected.slice(0, 8).map((l) => (
                    <div key={l.productId} className={"pb-line" + (l.under ? " is-under" : "")}>
                      <div style={{ minWidth: 0, flex: 1 }}><div className="pb-line-n">{l.name}</div><div className="pb-line-s">{l.under ? "under the " + pmM(l.floorCents) + " floor" : l.trueGpCents != null ? "GP " + pmM(l.trueGpCents) + (funded ? " with funding" : "") : "no cost on file"}</div></div>
                      <div className="pb-line-v"><span className="pg-was">{pmM(l.beforeCents)}</span><strong>{pmM(l.promoCents)}</strong></div>
                    </div>
                  ))}
                  {affected.length > 8 && <div className="pb-more">and {affected.length - 8} more</div>}
                </div>
              )}

              {!isPrice && (
                <div className="pb-rulebox"><OIcon name="box" /><div><div className="pb-ov-n">Runs at the till</div><div className="pb-ov-s">{m.d} There is no single new shelf price — the discount depends on what else is in the basket, so the ticket advertises the offer rather than a price.</div></div></div>
              )}

              <div className="pc-submit">
                <div className="pc-submit-s">
                  {!ready
                    ? (m.available === false ? m.unavailableReason : name.trim().length < 3 ? "Give it a name." : !stores.length ? "Pick at least one store." : affected.length === 0 ? "Nothing in scope." : preview && preview.problem ? preview.problem : "A reason is required for the below-cost lines.")
                    : isPrice ? "Publishing pushes " + affected.length + " price" + (affected.length === 1 ? "" : "s") + " to the registers within seconds."
                    : "Publishing caches the rule on every till at " + stores.length + " store" + (stores.length === 1 ? "" : "s") + "."}
                </div>
                <button className="btn btn--primary btn--sm" disabled={busy || !ready} onClick={() => save(false)}>Publish</button>
              </div>
            </div>

            {channels.includes("ticket") && affected.length > 0 && (
              <div className="off-table pg-card">
                <div className="pg-card-h">Ticket preview</div>
                <div className="pg-card-s">One per line, printed at 70 × 50 mm.</div>
                <div style={{ display: "flex", justifyContent: "center", marginTop: 12 }}>
                  <PgTicket name={affected[0].name} sku={affected[0].sku} beforeCents={affected[0].beforeCents} priceCents={affected[0].promoCents} say={pmSay(mech, params)} startsOn={from} endsOn={to} theme={theme} themes={themes} isPrice={isPrice} member={mech === "member"} clearance={false} />
                </div>
                <div className="tk-swatches">
                  {Object.entries(themes).map(([k, t]) => <button key={k} className={"tk-sw" + (theme === k ? " is-on" : "")} title={t.l} onClick={() => setTheme(k)}><span style={{ background: t.flag }} /><em>{t.l}</em></button>)}
                </div>
                <button className="btn btn--secondary btn--sm" style={{ width: "100%", marginTop: 11 }} disabled title="The print sheet arrives with promo-priced tickets in a later release."><OIcon name="printer" /> See the full sheet</button>
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// =================================================================
// DETAIL — what the register holds, and the lifecycle
// =================================================================
function PromotionDetail() {
  const { screenState, setScreen, setScreenState, pushToast } = window.useOffice();
  const id = screenState.promotionId;
  const [d, setD] = pmState(null);
  const [err, setErr] = pmState(null);
  const [extendTo, setExtendTo] = pmState("");
  const [reloadKey, setReloadKey] = pmState(0);

  pmEffect(() => {
    if (!id) { setErr("No promotion selected."); return; }
    let alive = true;
    window.OfficeAPI.promotion(id).then((x) => { if (alive) { setD(x); setExtendTo(x.endsOn); } }).catch((e) => { if (alive) setErr(e && e.message ? e.message : "Couldn't load it."); });
    return () => { alive = false; };
  }, [id, reloadKey]);

  const act = (key) => {
    if (key === "edit") { setScreenState((st) => ({ ...st, promotionEditId: id, promotionRerunOf: null })); setScreen("promotionNew"); return; }
    if (key === "discard") { if (!window.confirm(PM_ACTION.discard.confirm)) return; window.OfficeAPI.discardPromotion(id).then(() => { pushToast({ kind: "ok", text: "Draft discarded." }); setScreen("promotions"); }).catch((e) => pushToast({ kind: "error", text: e.message })); return; }
    if (key === "extend") { if (!extendTo || extendTo <= d.endsOn) { pushToast({ kind: "error", text: "Pick a later end date first." }); return; } pmRunAction(id, "extend", { endsOn: extendTo }, pushToast, () => setReloadKey((k) => k + 1)); return; }
    if (key === "rerun") { setScreenState((st) => ({ ...st, promotionEditId: null, promotionRerunOf: id })); setScreen("promotionNew"); return; }
    pmRunAction(id, key, undefined, pushToast, () => setReloadKey((k) => k + 1));
  };

  return (
    <div className="off-screen">
      <header className="off-screen-header">
        <div className="off-screen-header-meta">
          <button className="btn btn--ghost btn--sm" style={{ marginBottom: 6, paddingLeft: 0 }} onClick={() => setScreen("promotions")}><window.OIcon name="chevron-left" /> Promotions</button>
          <span className="off-screen-eyebrow">live · this pharmacy</span>
          <h2 className="off-screen-title">{d ? d.name : "Promotion"}</h2>
          {d && <div style={{ display: "flex", gap: 10, alignItems: "center", fontSize: "var(--fs-12)", color: "var(--text-subtle)" }}><PmStateTag state={d.state} /><span>{d.say} · {d.scopeLabel} · {pmDmy(d.startsOn)} → {pmDmy(d.endsOn)}</span></div>}
        </div>
        {d && <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", justifyContent: "flex-end" }}>
          {d.actions.includes("extend") && <input className="off-input" type="date" value={extendTo} min={d.endsOn} onChange={(e) => setExtendTo(e.target.value)} style={{ width: 150 }} />}
          {d.actions.map((k) => <button key={k} className={"btn btn--sm " + (PM_ACTION[k] && PM_ACTION[k].primary ? "btn--primary" : PM_ACTION[k] && PM_ACTION[k].danger ? "btn--ghost" : "btn--secondary")} style={PM_ACTION[k] && PM_ACTION[k].danger ? { color: "var(--void-text)" } : undefined} onClick={() => act(k)}>{PM_ACTION[k] ? PM_ACTION[k].l : k}</button>)}
        </div>}
      </header>
      <div className="off-screen-body">
        <PmErr text={err} />
        {d && (
          <div style={{ display: "grid", gridTemplateColumns: "minmax(0, 3fr) minmax(260px, 2fr)", gap: 16, alignItems: "start" }}>
            <div className="off-card">
              <div className="off-card-header"><div><div className="off-card-title">{d.lines.length} line{d.lines.length === 1 ? "" : "s"}</div><div className="off-card-meta">{d.state === "draft" ? "Publishing writes these to the registers." : "What was written when it was published."}</div></div></div>
              {d.lines.length === 0 && <div style={{ padding: "16px", fontSize: "var(--fs-12)", color: "var(--text-subtle)" }}>Nothing written yet — a draft changes nothing until it is published.</div>}
              {d.lines.length > 0 && <div className="off-grid" style={{ gridTemplateColumns: "minmax(200px, 2fr) 100px 100px 90px" }}>
                <div className="off-grid-headcell">Line</div><div className="off-grid-headcell" style={{ justifyContent: "flex-end" }}>Shelf</div><div className="off-grid-headcell" style={{ justifyContent: "flex-end" }}>Promo</div><div className="off-grid-headcell" style={{ justifyContent: "flex-end" }}>Move</div>
                {d.lines.map((l) => <div key={l.productId} className="off-grid-row"><div className="off-grid-cell off-cell-strong" style={{ minWidth: 0 }}><span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", display: "block" }}>{l.name}</span><span style={{ fontSize: "var(--fs-11)", color: "var(--text-subtle)", fontFamily: "var(--font-mono)", fontWeight: 400 }}>{l.sku || ""}</span></div><div className="off-grid-cell off-cell-num off-muted">{pmM(l.beforeCents)}</div><div className="off-grid-cell off-cell-num off-cell-strong">{l.promoCents == null ? (d.where === "basket" ? "rule" : "—") : pmM(l.promoCents)}</div><div className="off-grid-cell off-cell-num" style={{ color: "var(--paid-text)" }}>{l.promoCents == null ? "" : pmSigned(l.promoCents - l.beforeCents)}</div></div>)}
              </div>}
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
              <div className="off-card"><div className="off-card-header"><div><div className="off-card-title">What the register holds</div><div className="off-card-meta">Read back from the tables the tills sync</div></div></div>
                <div style={{ padding: "0 16px 12px", fontSize: "var(--fs-12)" }}>
                  {[
                    d.where === "price" ? ["Lines carrying a promo price", String(d.tillHolds.profilesWithPromoPrice), "pricing_profiles.promo_price_cents"] : ["Lines in the pricing group", String(d.tillHolds.profilesInGroup), d.tillHolds.pricingGroup != null ? "group " + d.tillHolds.pricingGroup : "no group yet"],
                    d.where === "basket" ? ["Register rule", d.tillHolds.rule ? (d.tillHolds.rule.isActive ? "active" : "inactive") : "not written", d.tillHolds.rule ? d.tillHolds.rule.quantityA + " for " + pmM(d.tillHolds.rule.bundlePriceCents) : ""] : null,
                    d.tillHolds.rule ? ["Rule window", (d.tillHolds.rule.startDate || "").slice(0, 16), "→ " + (d.tillHolds.rule.endDate || "").slice(0, 16)] : null,
                    ["Published", d.publishedAt ? d.publishedAt.slice(0, 16).replace("T", " ") : "not yet", ""],
                  ].filter(Boolean).map(([k, v, s]) => <div key={k} style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 10, padding: "8px 0", borderTop: "1px solid var(--line)" }}><div><div style={{ fontWeight: 600 }}>{k}</div>{s && <div style={{ color: "var(--text-subtle)", fontFamily: "var(--font-mono)", fontSize: "var(--fs-11)" }}>{s}</div>}</div><div style={{ fontFamily: "var(--font-mono)", textAlign: "right" }}>{v}</div></div>)}
                </div>
              </div>
              {(d.why || d.funded) && <div className="off-card"><div className="off-card-header"><div><div className="off-card-title">Why</div></div></div><div style={{ padding: "0 16px 14px", fontSize: "var(--fs-12)" }}>{d.why && <p style={{ margin: "0 0 8px" }}>{d.why}</p>}{d.funded && <div style={{ color: "var(--text-subtle)" }}>Funded by <b>{d.funded.supplier}</b> at {pmM(d.funded.perCents)} per unit{d.funded.ref ? " · ref " + d.funded.ref : ""}.</div>}</div></div>}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, { promotions: Promotions, promotionNew: PromotionNew, promotionDetail: PromotionDetail });
