/* =================== BROWSING REPORT (live activity) =================== */
const { useState: useStateB, useEffect: useEffectB, useMemo: useMemoB } = React;

// Full event stream for one visitor (anonymous clientId or identified email).
function VisitorEvents({ group, onClose }) {
  const PAGE = 20;
  const [evs, setEvs] = useStateB([]);
  const [more, setMore] = useStateB(false);
  const [loading, setLoading] = useStateB(true);
  const moreRef = React.useRef(null);
  const base = group.email
    ? "/api/events/customer/" + encodeURIComponent(group.email)
    : "/api/events/client/" + encodeURIComponent(group.clientId);
  useEffectB(() => {
    let alive = true; setEvs([]); setMore(false); setLoading(true);
    fetch(base + "?limit=" + PAGE).then(r => r.json()).then(j => {
      if (!alive) return; const ev = (j && j.events) || []; setEvs(ev); setMore(ev.length >= PAGE); setLoading(false);
    }).catch(() => { if (alive) { setEvs([]); setLoading(false); } });
    return () => { alive = false; };
  }, [group.clientId, group.email]);
  async function loadMore() {
    if (loading || !more || !evs.length) return;
    setLoading(true);
    const before = evs[evs.length - 1].ts;
    try {
      const j = await fetch(base + "?limit=" + PAGE + "&before=" + before).then(r => r.json());
      const ev = (j && j.events) || []; setEvs(prev => [...prev, ...ev]); setMore(ev.length >= PAGE);
    } catch (e) {} finally { setLoading(false); }
  }
  useEffectB(() => {
    const el = moreRef.current; if (!el || !more) return;
    const io = new IntersectionObserver(es => { if (es[0].isIntersecting) loadMore(); }, { root: el.closest(".modal__body"), rootMargin: "150px" });
    io.observe(el); return () => io.disconnect();
  }, [more, loading, evs.length]);
  const fmt = window.fmtEventTime, label = window.eventLabel;
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal__head">
          <h3>{group.email ? group.email : "Anonymous visitor"}</h3>
          <button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
        </div>
        <div className="modal__body" style={{ maxHeight: "70vh" }}>
          <div className="mono dim" style={{ fontSize: 11, marginBottom: 10 }}>client {group.clientId} · {evs.length}{more ? "+" : ""} events</div>
          {evs.length === 0 && loading ? <div className="dim" style={{ display: "flex", gap: 8, alignItems: "center" }}><span className="spin spin--dark" /> Loading…</div>
            : evs.length === 0 ? <div className="dim">No events.</div>
            : <div className="browse-feed">
                {evs.map(e => (
                  <div className="browse-row" key={e.id}>
                    <span className="browse-row__t mono" style={{ width: 110, flex: "none", color: "var(--txt-3)" }}>{fmt(e.ts)}</span>
                    <span className="browse-row__e">{label(e)}{e.url && <> · <a href={e.url} target="_blank" rel="noopener noreferrer" style={{ color: "var(--mint-ink)", textDecoration: "underline" }}>open ↗</a></>}</span>
                  </div>
                ))}
              </div>}
          {more && <div ref={moreRef} className="browse-sentinel">{loading ? <span className="spin spin--dark" /> : <span className="kick">scroll for more</span>}</div>}
        </div>
      </div>
    </div>
  );
}

// Per-product keyword overrides — when a product's type/title gives a poor or
// misleading noun, map (by URL substring / handle) to the word we'd actually use
// in an email. Matched case-insensitively against the product URL.
const KW_OVERRIDES = [
  { match: "create-your-uv-dtf-gang-sheet-online-builder", kw: "uv stickers" },
  { match: "collections/dtf-transfers", kw: "DTF transfers" },
  { match: "build-a-dtf-gang-sheet-online", kw: "DTF gang sheet builder" },
];
function kwOverride(url) {
  const u = (url || "").toLowerCase();
  if (!u) return "";
  for (const o of KW_OVERRIDES) if (u.includes(o.match)) return o.kw;
  return "";
}

// Body uses lightweight markdown links — [words](https://…) — so links attach to
// words, not bare URLs. These convert to HTML (for the email + preview) and to a
// readable plain-text fallback.
const MD_LINK_RE = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
function _escHtml(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
function mdLinksToHtml(s) {
  let out = "", last = 0, m; MD_LINK_RE.lastIndex = 0;
  while ((m = MD_LINK_RE.exec(s))) {
    out += _escHtml(s.slice(last, m.index));
    out += `<a href="${m[2]}" style="color:#057a54;">${_escHtml(m[1])}</a>`;
    last = m.index + m[0].length;
  }
  out += _escHtml(s.slice(last));
  return out.replace(/\n/g, "<br>");
}
function mdLinksToText(s) { return s.replace(MD_LINK_RE, "$1 ($2)"); }
function hasMdLinks(s) { MD_LINK_RE.lastIndex = 0; return MD_LINK_RE.test(s); }

// Personal outreach email to an identified visitor who's been browsing.
// Sent from the logged-in user, short + friendly, with an AI-write option.
function BrowseEmailModal({ group, events, ordered10d, onClose, initialNotes, defaultTpl }) {
  const cust = group.email && (DTF.customers || []).find(c => (c.email || "").toLowerCase() === group.email.toLowerCase());
  const fullName = (cust && cust.name) || "";
  const eml = (group.email || "").toLowerCase();
  const first = fullName.trim().split(/\s+/)[0] || "";
  const me = (DTF.userName || "").trim() || "the DTF.UK team";
  // Distinct products/collections this visitor viewed, newest first, with their
  // storefront links — fed to the AI so the note can offer product-related help.
  const products = (() => {
    const out = [], seen = new Set();
    for (const e of (events || [])) {
      if (e.type !== "product_viewed" && e.type !== "collection_viewed") continue;
      const d = e.data || {}, isCol = e.type === "collection_viewed";
      const title = isCol ? (d.collectionTitle || d.collectionId) : (d.productTitle || d.productId);
      if (!title) continue;
      const key = (isCol ? "c:" : "p:") + String(title).toLowerCase();
      if (seen.has(key)) continue; seen.add(key);
      out.push({ title: String(title), url: e.url || null, kind: isCol ? "collection" : "product", type: (d.productType || "").trim(), kw: kwOverride(e.url) });
      if (out.length >= 8) break;
    }
    return out;
  })();
  const lastLabel = group.last && window.eventLabel ? window.eventLabel(group.last) : "";
  const greet = first ? `Hi ${first},` : "Hi there,";
  const isExisting = !!(cust && cust.orders > 0);
  const isVip = !!(cust && DTF.isVip(cust));
  // A short keyword for what they were looking at (first selected/seen product),
  // pluralised so it reads naturally ("a new run of gang sheets"). Auto-link will
  // wrap it in the product link.
  const welcomeSub = first ? `${first}, good to see you again!` : "Good to see you again!";
  const kwBase = (products[0] && (products[0].kw || products[0].type || products[0].title) || "").toLowerCase();
  const kw = kwBase ? (/s$/.test(kwBase) ? kwBase : kwBase + "s") : "";
  // Templates: short hello to a likely-new customer, a warm "been a while" nudge
  // to an existing one, and a VIP variant that asks about their recent order.
  const TEMPLATES = {
    reactivation: {
      label: "Re-activation",
      subject: first ? `${first}, how's it all going?` : "How's it all going?",
      body:
`${greet}

It's been a while since your last order with us — I hope you and the business are keeping well.

I just wanted to check in and see how things are going your end. If you've got a run of transfers coming up, or there's anything we can help with, we'd be glad to sort it for you.

And if there's anything we could do to improve how we work together, I'd genuinely welcome the feedback — always keen to make things easier for you.

No pressure at all — just saying hello.

${me}
DTF.UK`,
    },
    new: {
      label: "New customer",
      subject: "Anything I can help with?",
      body:
`${greet}

Saw you having a look round the site and thought I'd say hello.

If DTF transfers are new to you and you've got any questions, I'm happy to help — just reply and let me know what you're after.

No rush either way.

${me}
DTF.UK`,
    },
    existing: {
      label: "Existing customer",
      subject: welcomeSub,
      body:
`${greet}

Been a while! Saw you back on the site having a look.

If there's anything I can help with, just give me a shout — happy to talk through whatever you need.

${me}
DTF.UK`,
    },
    vip: {
      label: "VIP customer",
      subject: welcomeSub,
      body:
`${greet}

Been a while — lovely to see you back!

How did your last order turn out? Hope it all went well. Are you planning another run of ${kw || "prints"}, or is there anything else I can help with?

Always happy to help — just give me a shout.

${me}
DTF.UK`,
    },
  };
  const initTpl = (defaultTpl && TEMPLATES[defaultTpl]) ? defaultTpl : (isVip ? "vip" : isExisting ? "existing" : "new");
  const [tpl, setTpl] = useStateB(initTpl);
  const [selected, setSelected] = useStateB(() => new Set(products.map((_, i) => i)));
  // Ticking a product auto-links its keyword in the body (if present) to its URL.
  const toggleSel = (i) => {
    const adding = !selected.has(i);
    setSelected(s => { const n = new Set(s); n.has(i) ? n.delete(i) : n.add(i); return n; });
    if (adding && products[i] && products[i].url) setBody(b => autoLinkBody(b, [products[i]]));
  };
  const [to, setTo] = useStateB(group.email || "");
  const [subject, setSubject] = useStateB(TEMPLATES[initTpl].subject);
  const [body, setBody] = useStateB(TEMPLATES[initTpl].body);
  const pickTpl = (k) => { setTpl(k); setSubject(TEMPLATES[k].subject); setBody(autoLinkBody(TEMPLATES[k].body)); };
  const [aiNotes, setAiNotes] = useStateB(initialNotes || "");
  const [sending, setSending] = useStateB(false);
  const [genning, setGenning] = useStateB(false);
  const [err, setErr] = useStateB("");
  const [done, setDone] = useStateB("");
  const kbd = (navigator.platform || "").toLowerCase().includes("mac") ? "⌘" : "Ctrl";
  useEffectB(() => {
    const onKey = e => { if (e.key === "Escape") onClose(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  // Link the initial template's product keyword on open.
  useEffectB(() => { setBody(b => autoLinkBody(b)); }, []);
  // Auto-link the selected products' keywords (their type, e.g. "gang sheets",
  // else the title) to their store URLs — first unlinked match per product,
  // never touching text already inside a [text](url) link.
  const autoLinkBody = (text, list) => {
    const chosen = (list || products.filter((_, i) => selected.has(i))).filter(p => p.url);
    if (!chosen.length) return text;
    const usedUrl = new Set(); MD_LINK_RE.lastIndex = 0; let mm;
    while ((mm = MD_LINK_RE.exec(text))) usedUrl.add(mm[2]);
    const segs = []; let last = 0, m; MD_LINK_RE.lastIndex = 0;
    while ((m = MD_LINK_RE.exec(text))) { segs.push(["p", text.slice(last, m.index)]); segs.push(["l", m[0]]); last = m.index + m[0].length; }
    segs.push(["p", text.slice(last)]);
    for (const p of chosen) {
      if (usedUrl.has(p.url)) continue;
      // Candidate keywords: the product TYPE first (cleanest, e.g. "Gang Sheet"),
      // then the title and its non-numeric word tail as fallbacks.
      const cands = [];
      const add = s => { s = (s || "").trim(); if (s && !cands.includes(s)) cands.push(s); };
      add(p.kw); // explicit override always wins, tried first
      add(p.type);
      if (!p.type && !p.kw) {
        add(p.title);
        const w = (p.title || "").split(/\s+/).filter(x => !/\d/.test(x));
        if (w.length >= 2) add(w.slice(-2).join(" "));
      }
      let done = false;
      for (let si = 0; si < segs.length && !done; si++) {
        if (segs[si][0] !== "p") continue;
        for (const kw of cands) {
          // match the keyword with a flexible trailing 's' (singular/plural) and
          // flexible whitespace, e.g. "Gang Sheet" matches "gang sheets".
          const esc = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+").replace(/s$/i, "");
          const found = segs[si][1].match(new RegExp("\\b" + esc + "s?\\b", "i"));
          if (found) {
            const seg = segs[si][1], idx = found.index;
            segs[si][1] = seg.slice(0, idx) + "[" + found[0] + "](" + p.url + ")" + seg.slice(idx + found[0].length);
            usedUrl.add(p.url); done = true; break;
          }
        }
      }
    }
    return segs.map(s => s[1]).join("");
  };
  const genAI = () => {
    if (genning) return;
    setGenning(true); setErr("");
    const chosen = products.filter((_, i) => selected.has(i));
    const browsing = chosen.length
      ? chosen.map(p => `- ${p.title}${p.url ? " — " + p.url : ""}${p.kw ? ` (refer to this as "${p.kw}")` : ""}`).join("\n")
      : lastLabel;
    fetch("/api/ai/personal-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ context: aiNotes, customerName: fullName, userName: DTF.userName, browsing, kind: tpl }) })
      .then(async r => { const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || "AI failed"); return j; })
      .then(j => { if (j.subject) setSubject(j.subject); if (j.body) setBody(autoLinkBody(j.body || "")); })
      .catch(e => setErr(e.message)).finally(() => setGenning(false));
  };
  const send = () => {
    if (!to.trim()) { setErr("Add a recipient email"); return; }
    setSending(true); setErr(""); setDone("");
    const payload = { to: to.trim(), subject };
    if (hasMdLinks(body)) {
      payload.html = `<div style="font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:1.5;">${mdLinksToHtml(body)}</div>`;
      payload.text = mdLinksToText(body);
    } else { payload.text = body; }
    fetch("/api/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) })
      .then(async r => { const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || "Send failed"); return j; })
      .then(() => { setDone("Sent ✓"); setTimeout(() => onClose(true), 700); })
      .catch(e => setErr(e.message)).finally(() => setSending(false));
  };
  return (
    <div className="modal-overlay">
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal__head">
          <h3>Personal email</h3>
          <button className="icon-btn" onClick={() => onClose(false)} title="Close"><Icon name="x" /></button>
        </div>
        <div className="modal__body" style={{ display: "flex", flexDirection: "column", gap: 10, maxHeight: "72vh" }}>
          <div>
            <div style={{ fontWeight: 600, fontSize: 14 }}>{fullName || group.email || "Customer"}</div>
            <div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", marginTop: 4 }}>
              {group.email && <span className="mono dim" style={{ fontSize: 11 }}>{group.email}</span>}
              {cust && DTF.isVip(cust) && <span className="badge badge--violet" title="VIP customer">VIP</span>}
              {cust && cust.orders > 1 && <span className="badge badge--teal" title={`Repeat customer · ${cust.orders} orders`}>Repeat</span>}
              {eml && ordered10d && ordered10d.has(eml) && <span className="badge badge--green" title="Placed an order in the last 10 days">Ordered (10d)</span>}
              {eml && ordered10d && !ordered10d.has(eml) && <span className="badge badge--amber" title="No order in the last 10 days — may need outreach">No orders (10d)</span>}
            </div>
          </div>
          <label className="out-field"><span>To</span><input className="rm-in" value={to} onChange={e => setTo(e.target.value)} placeholder="customer@email.com" /></label>
          {products.length > 0 ? (
            <div className="out-field"><span>Was looking at <span className="dim" style={{ textTransform: "none", letterSpacing: 0 }}>· tick what AI should use</span></span>
              <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
                {products.map((p, i) => (
                  <label key={i} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, cursor: "pointer" }}>
                    <input type="checkbox" checked={selected.has(i)} onChange={() => toggleSel(i)} />
                    {p.url
                      ? <a href={p.url} target="_blank" rel="noopener noreferrer" onClick={e => e.stopPropagation()} style={{ color: "var(--mint-ink)", textDecoration: "underline" }}>{p.title}</a>
                      : <span>{p.title}</span>}
                    {p.kind === "collection" ? <span className="dim"> (collection)</span> : null}
                  </label>
                ))}
              </div>
            </div>
          ) : lastLabel ? <div className="kick">Was looking at: {lastLabel}</div> : null}
          <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
            <span className="kick">Template</span>
            <button className={"btn btn--sm " + (tpl === "new" ? "btn--primary" : "btn--ghost")} onClick={() => pickTpl("new")}>New customer</button>
            <button className={"btn btn--sm " + (tpl === "existing" ? "btn--primary" : "btn--ghost")} onClick={() => pickTpl("existing")}>Existing customer</button>
            {isVip && <button className={"btn btn--sm " + (tpl === "vip" ? "btn--primary" : "btn--ghost")} onClick={() => pickTpl("vip")}>VIP customer</button>}
            {isExisting && <span className="kick" title={`${cust.orders} previous order${cust.orders > 1 ? "s" : ""}`}>· {cust.orders} order{cust.orders > 1 ? "s" : ""} on file</span>}
          </div>
          <label className="out-field"><span>Notes for AI (optional)</span>
            <textarea className="out-html" style={{ height: 70 }} value={aiNotes} onChange={e => setAiNotes(e.target.value)}
              onKeyDown={e => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); genAI(); } }}
              placeholder={`Anything to steer the note? e.g. mention our 24h turnaround… (${kbd}+Enter to write)`} /></label>
          <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
            <button className="btn btn--primary btn--sm" disabled={genning} onClick={genAI} title="AI-write a short personal note from you"><Icon name="spark" /> {genning ? "Writing…" : "AI write"}</button>
            <span className="kbd-hint">{kbd}+↵</span>
          </div>
          <label className="out-field"><span>Subject</span><input className="rm-in" value={subject} onChange={e => setSubject(e.target.value)} /></label>
          <label className="out-field"><span>Body <span className="dim" style={{ textTransform: "none", letterSpacing: 0 }}>· tick a product above to link its word</span></span>
            <textarea className="out-html" style={{ height: 200 }} value={body} onChange={e => setBody(e.target.value)}
              onBlur={() => setBody(b => autoLinkBody(b))}
              onKeyDown={e => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); send(); } }} /></label>
          <div className="out-field"><span>Preview <span className="dim" style={{ textTransform: "none", letterSpacing: 0 }}>· as the customer sees it</span></span>
            <div style={{ border: "1px solid var(--rule)", borderRadius: 8, padding: "10px 12px", fontSize: 13.5, lineHeight: 1.55, background: "#fff", whiteSpace: "normal", minHeight: 40 }}
              dangerouslySetInnerHTML={{ __html: mdLinksToHtml(body) }} />
          </div>
          {err && <div style={{ color: "var(--c-red)", fontSize: 12 }}>{err}</div>}
        </div>
        <div className="modal__foot">
          <span className="kick" style={{ marginRight: "auto" }}>Personal note · signed {me}</span>
          {done && <span className="kick" style={{ color: "var(--mint-ink)" }}>{done}</span>}
          <button className="btn btn--ghost btn--sm" onClick={() => onClose(false)}>Cancel</button>
          <button className="btn btn--primary btn--sm" disabled={sending} onClick={send}><Icon name="send" /> {sending ? "Sending…" : "Send email"}</button>
          <span className="kbd-hint">{kbd}+↵</span>
        </div>
      </div>
    </div>
  );
}

// Merge two event lists, de-dupe by id, newest first.
function _mergeEvents(a, b) {
  const seen = new Set(); const out = [];
  for (const e of a) { if (!seen.has(e.id)) { seen.add(e.id); out.push(e); } }
  for (const e of b) { if (!seen.has(e.id)) { seen.add(e.id); out.push(e); } }
  out.sort((x, y) => y.ts - x.ts);
  return out;
}
const BROWSE_PAGE = 200;

function BrowsingReport({ go, route }) {
  const [identifiedOnly, setIdentifiedOnly] = useStateB(true);
  const [range, setRange] = useStateB((route && route.period) || "today"); // today | week | all
  // Selecting a period updates the URL (#/browsing/<period>) so it's linkable
  // and survives reload; back/forward re-syncs the buttons.
  const pickRange = (r) => { setRange(r); go({ view: "browsing", period: r }); };
  useEffectB(() => { const p = route && route.period; if (p && p !== range) setRange(p); }, [route && route.period]);
  const [noOrdersOnly, setNoOrdersOnly] = useStateB(false); // only identified customers with no order in 10d
  const [events, setEvents] = useStateB([]);
  const [total, setTotal] = useStateB(null);
  const [err, setErr] = useStateB(null);
  const [loaded, setLoaded] = useStateB(false);
  const [auto, setAuto] = useStateB(true);
  const [detail, setDetail] = useStateB(null);
  const [emailFor, setEmailFor] = useStateB(null); // personal-email modal target
  const [emailedSet, setEmailedSet] = useStateB(() => new Set());   // sent this session
  const [emailedLog, setEmailedLog] = useStateB(() => new Set());   // sent within the window (from the server log)
  const [refreshing, setRefreshing] = useStateB(false);
  const [loadingMore, setLoadingMore] = useStateB(false);
  const [noMore, setNoMore] = useStateB(false); // hit the end of the older-events list
  const [query, setQuery] = useStateB("");
  // Server-side accurate identified-visitor roll-up for the window (the client
  // event cache only holds the most recent few thousand events — a few minutes
  // on a busy day — so "this week identified" was undercounting badly).
  const [idGroups, setIdGroups] = useStateB(null);
  const [idLoading, setIdLoading] = useStateB(false);
  const [idCapped, setIdCapped] = useStateB(false);
  const moreRef = React.useRef(null);

  // Refresh = pull the newest page and merge (keeps already-loaded older events).
  function load() {
    setRefreshing(true);
    return fetch("/api/events/recent?limit=" + BROWSE_PAGE).then(r => r.json()).then(j => {
      if (j && j.error) { setErr(j.error); return; }
      setErr(null);
      setEvents(prev => _mergeEvents((j && j.events) || [], prev));
      if (j && typeof j.total === "number") setTotal(j.total);
      setLoaded(true);
    }).catch(e => setErr(e.message)).finally(() => setRefreshing(false));
  }
  // Load older events (infinite scroll).
  async function loadOlder() {
    if (loadingMore || !events.length) return;
    setLoadingMore(true);
    try {
      const before = events[events.length - 1].ts;
      const j = await fetch("/api/events/recent?limit=" + BROWSE_PAGE + "&before=" + before).then(r => r.json());
      if (j && typeof j.total === "number") setTotal(j.total);
      const fetched = (j && j.events) || [];
      // A short page (fewer than a full page) means we've reached the oldest event.
      if (fetched.length < BROWSE_PAGE) setNoMore(true);
      setEvents(prev => _mergeEvents(prev, fetched));
    } catch (e) { setNoMore(true); } finally { setLoadingMore(false); }
  }
  useEffectB(() => { load(); }, []);
  useEffectB(() => {
    if (!auto) return;
    const t = setInterval(load, 15000); // refresh "watching now" every 15s
    return () => clearInterval(t);
  }, [auto]);

  // When a wider window is selected (today→week, etc.), the in-range events may
  // not be loaded yet — proactively pull older pages until the window is covered
  // so the list updates immediately rather than waiting for the user to scroll.
  useEffectB(() => {
    if (!loaded || range === "all" || loadingMore || noMore || !events.length) return;
    if (events.length >= 4000) return; // safety cap against runaway fills
    const oldest = events[events.length - 1].ts;
    if (oldest >= cutoff && (total == null || events.length < total)) loadOlder();
  }, [loaded, range, cutoff, events, loadingMore, noMore, total]);

  // Once the oldest loaded event predates the window, there's nothing more in
  // range to fetch (only matters for today/week).
  const oldestLoaded = events.length ? events[events.length - 1].ts : 0;
  const rangeNeedsMore = range === "all" || oldestLoaded >= cutoff;
  const hasMore = loaded && !noMore && events.length > 0 && (total == null || events.length < total) && rangeNeedsMore;
  // Infinite scroll: observe a sentinel at the bottom of the list.
  useEffectB(() => {
    const el = moreRef.current; if (!el || !hasMore) return;
    const io = new IntersectionObserver(es => { if (es[0].isIntersecting) loadOlder(); },
      { root: el.closest(".content"), rootMargin: "300px" });
    io.observe(el); return () => io.disconnect();
  }, [hasMore, loadingMore, events.length]);

  // Time window for the report. "today" = since local midnight, "week" = since
  // Monday 00:00, "all" = everything loaded.
  const cutoff = useMemoB(() => {
    if (range === "all") return 0;
    const d = new Date();
    d.setHours(0, 0, 0, 0);
    if (range === "week") { const mon = (d.getDay() + 6) % 7; d.setDate(d.getDate() - mon); }
    return d.getTime();
  }, [range]);
  const inRange = useMemoB(() => range === "all" ? events : events.filter(e => e.ts >= cutoff), [events, cutoff, range]);

  // Accurate identified-visitor list for the window, straight from the server DB
  // (not the capped client cache). Refetches when the window/filter changes and
  // every 30s while auto-refresh is on. "all" uses since=0 (whole history).
  useEffectB(() => {
    if (!identifiedOnly) { setIdGroups(null); setIdCapped(false); return; }
    let live = true;
    const run = () => {
      setIdLoading(true);
      const since = range === "all" ? 0 : cutoff;
      fetch("/api/events/identified?since=" + since)
        .then(r => r.json())
        .then(j => { if (!live) return; setIdGroups((j && j.groups) || []); setIdCapped(!!(j && j.capped)); })
        .catch(() => { if (live) setIdGroups(null); })
        .finally(() => { if (live) setIdLoading(false); });
    };
    run();
    const t = auto ? setInterval(run, 30000) : null;
    return () => { live = false; if (t) clearInterval(t); };
  }, [identifiedOnly, cutoff, range, auto]);

  // Persisted "Emailed ✓": load the recipients emailed within the current window
  // from the server send-log, so someone emailed earlier today still shows as
  // contacted after a refresh/reload (the session set only covers this tab).
  useEffectB(() => {
    const since = range === "all" ? (Date.now() - 30 * 24 * 3600 * 1000) : cutoff;
    fetch("/api/sent/recent?since=" + since).then(r => r.json()).then(j => {
      setEmailedLog(new Set(((j && j.sent) || []).map(e => (e.to || "").toLowerCase()).filter(Boolean)));
    }).catch(() => {});
  }, [cutoff, range]);
  const isEmailed = (eml) => { const x = (eml || "").toLowerCase(); return !!x && (emailedSet.has(x) || emailedLog.has(x)); };

  // Identified emails that HAVE ordered in the last 10 days — used to flag the
  // ones who haven't (potential outreach leads). Sourced from loaded orders AND
  // from browsing 'checkout_completed' events (the latter catches very recent
  // orders not yet in the loaded orders page).
  const ordered10d = useMemoB(() => {
    const since = Date.now() - 10 * 24 * 3600 * 1000;
    const set = new Set();
    for (const o of (DTF.orders || [])) {
      const t = Date.parse(String(o.date || "").replace(" ", "T"));
      if (!t || t < since) continue;
      const c = DTF.getCustomer(o.customerId);
      const e = c && c.email && c.email.toLowerCase();
      if (e) set.add(e);
    }
    for (const ev of (events || [])) {
      if (ev.type !== "checkout_completed" || !ev.email) continue;
      if (ev.ts < since) continue;
      set.add(ev.email.toLowerCase());
    }
    return set;
  }, [events]);

  const groups = useMemoB(() => {
    const raw = query.trim();
    // Optional event-count filter, e.g. "events>2", ">2", "events>=3", "events:5".
    // Combinable with a text term ("polo events>1"). "=" / ":" mean exactly N.
    let countPred = null, text = raw;
    const mm = raw.match(/events?\s*(>=|<=|>|<|=|:)\s*(\d+)/i) || raw.match(/(?:^|\s)(>=|<=|>|<)\s*(\d+)\s*$/);
    if (mm) {
      const op = mm[1], n = +mm[2];
      countPred = c => op === ">" ? c > n : op === ">=" ? c >= n : op === "<" ? c < n : op === "<=" ? c <= n : c === n;
      text = raw.replace(mm[0], " ");
    }
    // strip stray separators (e.g. "uv, events>2" → text "uv")
    const term = text.replace(/[,;]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();

    // Identified filter → use the server-side window roll-up (accurate over the
    // whole window, not just cached events). Text search here matches the email
    // and the visitor's most-recent event (we don't hold their full history
    // client-side); the event-count filter uses the true window count.
    if (identifiedOnly && idGroups) {
      let arr = idGroups.slice();
      if (term) arr = arr.filter(g => {
        const d = (g.last && g.last.data) || {};
        const hay = [g.email, g.last && g.last.title, d.productTitle, d.collectionTitle, d.query, g.last && g.last.pageType, g.last && g.last.path].filter(Boolean).join("  ").toLowerCase();
        return hay.includes(term);
      });
      if (noOrdersOnly) arr = arr.filter(g => g.email && !ordered10d.has(g.email.toLowerCase()));
      if (countPred) arr = arr.filter(g => countPred(g.count));
      return arr.sort((a, b) => b.lastTs - a.lastTs);
    }

    // Search by browsed item title (product / collection / page / search query).
    const evs = term ? inRange.filter(e => {
      const d = e.data || {};
      const hay = [e.title, d.productTitle, d.collectionTitle, d.query, e.pageType, e.path].filter(Boolean).join("  ").toLowerCase();
      return hay.includes(term);
    }) : inRange;
    const m = new Map();
    for (const e of evs) {
      const key = e.email || e.clientId;
      const g = m.get(key) || { key, email: e.email || null, clientId: e.clientId, lastTs: 0, count: 0, last: null };
      g.count++; if (e.ts > g.lastTs) { g.lastTs = e.ts; g.last = e; } m.set(key, g);
    }
    let arr = [...m.values()];
    if (identifiedOnly || noOrdersOnly) arr = arr.filter(g => g.email);
    if (noOrdersOnly) arr = arr.filter(g => !ordered10d.has(g.email.toLowerCase())); // outreach leads only
    if (countPred) arr = arr.filter(g => countPred(g.count)); // event-count filter
    return arr.sort((a, b) => b.lastTs - a.lastTs);
  }, [inRange, identifiedOnly, idGroups, query, noOrdersOnly, ordered10d]);

  const idCount = useMemoB(() => (identifiedOnly && idGroups) ? idGroups.length : new Set(inRange.filter(e => e.email).map(e => e.email)).size, [inRange, identifiedOnly, idGroups]);
  const anonCount = useMemoB(() => new Set(inRange.filter(e => !e.email).map(e => e.clientId)).size, [inRange]);
  const fmt = window.fmtEventTime, label = window.eventLabel;
  // "live" = activity in the last 5 minutes (server clock差 aside, best-effort)
  const now = events.length ? Math.max(...events.map(e => e.ts)) : 0;
  const isLive = (ts) => now - ts < 5 * 60 * 1000;

  const orderedShown = groups.filter(g => g.email && ordered10d.has(g.email.toLowerCase())).length;

  function customerIdFor(g) {
    if (!g.email) return null;
    const c = (DTF.customers || []).find(c => (c.email || "").toLowerCase() === g.email.toLowerCase());
    return c ? c.id : ("mail:" + g.email);
  }
  // Hash link to the customer profile — lets the name be cmd/middle-clicked into a
  // new tab (mirrors the app's toHash for the customer route).
  function custHref(g) {
    const id = customerIdFor(g);
    if (!id) return null;
    const tail = String(id).startsWith("gid://") ? String(id).split("/").pop() : id;
    return "#/customers/" + encodeURIComponent(tail);
  }
  function openVisitor(g) {
    const id = customerIdFor(g);
    if (id) return go({ view: "customer", id });
    setDetail(g); // anonymous → show all their events in a modal
  }

  return (
    <div className="board">
      <div className="stats">
        <div className="stat"><div className="stat__k">{identifiedOnly ? "Identified (window)" : "Visitors (window)"}</div><div className="stat__v">{identifiedOnly ? idCount : (idCount + anonCount)}</div><div className="stat__d">{identifiedOnly ? (range === "today" ? "today" : range === "week" ? "this week" : "all time") + (idLoading ? " · syncing…" : idCapped ? " · capped 5k" : "") : `${idCount} identified · ${anonCount} anonymous`}</div></div>
        <div className="stat"><div className="stat__k">Events</div><div className="stat__v">{range === "all" ? (total != null ? total : events.length) : inRange.length}</div><div className="stat__d">{range === "all" ? `${events.length} loaded` : (range === "today" ? "today" : "this week") + ` · ${events.length} loaded`} · {events.length ? fmt(now) : "—"}</div></div>
        <div className="stat"><div className="stat__k">Active now</div><div className="stat__v">{groups.filter(g => isLive(g.lastTs)).length}</div><div className="stat__d">last 5 min</div></div>
        <div className="stat"><div className="stat__k">Ordered (10d)</div><div className="stat__v">{orderedShown}</div><div className="stat__d">of {groups.length} shown · rest may need comms</div></div>
      </div>

      <div className="toolbar">
        <div className="seg">
          <button className={range === "today" ? "is-active" : ""} onClick={() => pickRange("today")}>Today</button>
          <button className={range === "week" ? "is-active" : ""} onClick={() => pickRange("week")}>This week</button>
          <button className={range === "all" ? "is-active" : ""} onClick={() => pickRange("all")}>All</button>
        </div>
        <div className="seg">
          <button className={identifiedOnly ? "is-active" : ""} onClick={() => setIdentifiedOnly(true)}>Identified customers</button>
          <button className={!identifiedOnly ? "is-active" : ""} onClick={() => setIdentifiedOnly(false)}>Everyone (incl. anonymous)</button>
        </div>
        <div className="ord-search">
          <Icon name="search" />
          <input type="search" value={query} onChange={e => setQuery(e.target.value)} placeholder="Search title, or events>2 …" />
          {query && <button className="ord-search__clear" title="Clear" onClick={() => setQuery("")}>×</button>}
        </div>
        <label className="kick" style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }} title="Only identified customers with no order in the last 10 days">
          <input type="checkbox" checked={noOrdersOnly} onChange={e => setNoOrdersOnly(e.target.checked)} /> No orders in 10 days
        </label>
        <button className="btn btn--ghost btn--sm" disabled={refreshing} onClick={load}><Icon name="refresh" /> {refreshing ? "Refreshing…" : "Refresh"}</button>
        <label className="kick" style={{ display: "flex", alignItems: "center", gap: 6, marginLeft: 4, cursor: "pointer" }}>
          <input type="checkbox" checked={auto} onChange={e => setAuto(e.target.checked)} /> Auto-refresh (15s)
        </label>
      </div>

      {!loaded ? (
        <div className="empty"><span className="spin spin--dark" /><div className="t">Loading activity…</div></div>
      ) : err ? (
        <div className="empty-state">Couldn’t load activity — {err}</div>
      ) : groups.length === 0 ? (
        <div className="empty-state">No {identifiedOnly ? "identified " : ""}browsing activity {range === "today" ? "today" : range === "week" ? "this week" : "yet"}.</div>
      ) : (
        <div className="tbl-wrap">
        <table className="tbl">
          <thead><tr><th>Visitor</th><th>Watching</th><th>When</th><th className="num">Events</th><th></th></tr></thead>
          <tbody>
            {groups.map(g => {
              const cust = g.email && (DTF.customers || []).find(c => (c.email || "").toLowerCase() === g.email.toLowerCase());
              return (
                <tr key={g.key} onClick={() => openVisitor(g)} style={{ cursor: "pointer" }}>
                  <td>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      {isLive(g.lastTs) && <span className="live-dot" title="Active in last 5 min" />}
                      <div style={{ minWidth: 0 }}>
                        <div style={{ fontWeight: 600, display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
                          {g.email
                            ? <a href={custHref(g)} onClick={e => e.stopPropagation()} style={{ color: "inherit", textDecoration: "none" }} title="Open customer (⌘/middle-click for new tab)">{(cust && cust.name) || g.email}</a>
                            : <span>Anonymous visitor</span>}
                          {cust && DTF.isVip(cust) && <span className="badge badge--violet" title="VIP customer">VIP</span>}
                          {cust && cust.orders > 1 && <span className="badge badge--teal" title={`Repeat customer · ${cust.orders} orders`}>Repeat</span>}
                          {g.email && ordered10d.has(g.email.toLowerCase()) && <span className="badge badge--green" title="Placed an order in the last 10 days">Ordered (10d)</span>}
                          {g.email && !ordered10d.has(g.email.toLowerCase()) && <span className="badge badge--amber" title="No order in the last 10 days — may need outreach">No orders (10d)</span>}
                        </div>
                        <div className="mono dim" style={{ fontSize: 10.5 }}>{g.email || g.clientId.slice(0, 14) + "…"}</div>
                      </div>
                    </div>
                  </td>
                  <td style={{ maxWidth: 420 }}>
                    <div style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{g.last ? label(g.last) : "—"}</div>
                    {g.last && g.last.url && <a className="mono" href={g.last.url} target="_blank" rel="noopener noreferrer" onClick={e => e.stopPropagation()} style={{ fontSize: 10.5, color: "var(--mint-ink)", textDecoration: "underline" }}>open ↗</a>}
                  </td>
                  <td className="mono" style={{ fontSize: 11, whiteSpace: "nowrap" }}>{fmt(g.lastTs)}</td>
                  <td className="num">{g.count}</td>
                  <td style={{ whiteSpace: "nowrap", textAlign: "right" }}>
                    {g.email
                      ? <button className="btn btn--ghost btn--sm" style={isEmailed(g.email) ? { color: "var(--mint-ink)", borderColor: "var(--mint-ink)", background: "var(--mint-wash)" } : undefined} onClick={e => { e.stopPropagation(); setEmailFor(g); }} title={isEmailed(g.email) ? "Already emailed within this window — click to email again" : "Send a short personal email from you"}>
                          <Icon name="mail" /> {isEmailed(g.email) ? "Emailed ✓" : "Email"}</button>
                      : <span className="dim" style={{ fontSize: 10.5 }}>—</span>}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
        </div>
      )}

      {loaded && !err && hasMore && (
        <div ref={moreRef} className="browse-sentinel">
          {loadingMore ? <span className="spin spin--dark" /> : <span className="kick">scroll for more · {events.length} of {total} events</span>}
        </div>
      )}
      {loaded && !err && !hasMore && events.length > 0 && (
        <div className="kick" style={{ textAlign: "center", padding: "12px", color: "var(--txt-3)" }}>all {events.length} events loaded</div>
      )}

      {detail && <VisitorEvents group={detail} onClose={() => setDetail(null)} />}
      {emailFor && <BrowseEmailModal group={emailFor} events={events.filter(e => emailFor.email ? (e.email || "").toLowerCase() === emailFor.email.toLowerCase() : e.clientId === emailFor.clientId)} ordered10d={ordered10d} onClose={(sent) => { if (sent && emailFor.email) setEmailedSet(s => new Set(s).add(emailFor.email.toLowerCase())); setEmailFor(null); }} />}
    </div>
  );
}

window.BrowsingReport = BrowsingReport;
window.BrowseEmailModal = BrowseEmailModal;   // reused on the customer page for re-activation emails
