// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /payments/activity/ · AdminPaymentsController::show('activity') → activityData() — see docs/ISYSTEM_REFERENCE.md §Batch 10.1
/* Traced Aug 2026 (architecture item 2). This screen was built before
   ISYSTEM_REFERENCE existed and its data shape had never been matched to a
   controller. It IS a real isystem section: activityData() builds a read-only
   operator audit feed, paginated, scoped to the selected skin, with
   activityFilters() and its own CSV export at GET /payments/activity/export.
   Two things to reconcile before wiring: the real feed is server-paginated
   (->appends(filters)) rather than sliced client-side, and single-skin
   operators have brandLocked = true, which hides the brand filter entirely. */
/* Activity Feed — operator action history
   Tracks what each operator / support agent did: PSP logins, transaction
   approvals / rejections, method edits, notes left on a player. Designed
   for quick scanning, not deep analysis.

   THAT DESCRIPTION WAS THE MOCK'S, not the schema's. `activity_logs` holds an
   actor, a free-text action, an optional target user, a jsonb detail and an IP
   — there is no action taxonomy and no field-level diff. The five kinds this
   screen used to chip-filter on existed only in `window.MOCK.ACTIVITY`, which
   is the most dangerous place for invented data on the whole build: the page's
   own explainer called itself the compliance source-of-truth while showing six
   invented operators approving invented transactions. */
/* Deterministic avatar colour from the actor's id. PRESENTATION, not data —
   the old version read a `color` and an `avatar` off a MOCK.OP_USERS row, so
   every operator on screen was one of six invented people. Initials come from
   the real username; the colour is a hash of the real id, which is a rendering
   choice and is labelled as one. */
const ACT_AVATAR_COLORS = ["#2563eb", "#7c3aed", "#0891b2", "#16a34a", "#ea580c", "#be123c"];
const actAvatarColor = (id) => ACT_AVATAR_COLORS[Math.abs(Number(id) || 0) % ACT_AVATAR_COLORS.length];
const actInitials = (name) => String(name || "?")
  .split(/[\s._-]+/).filter(Boolean).slice(0, 2).map(x => x[0].toUpperCase()).join("") || "?";

const ACT_PRESET_MS = { "1h": 3600e3, "24h": 24 * 3600e3, "7d": 7 * 86400e3, "all": Infinity };

const Activity = ({ brand }) => {
  window.useLocale && window.useLocale();
  const T = window.T || ((k, fb) => fb || k);
  const [actionFilter, setActionFilter] = useState("all");
  const [userFilter, setUserFilter] = useState("all");
  const [query, setQuery] = useState("");
  const [range, setRange] = useState("24h");
  const [customRange, setCustomRange] = useState(null); // { startMs, endMs, label }
  const [customOpen, setCustomOpen] = useState(false);

  const clearFilters = () => {
    setActionFilter("all"); setUserFilter("all"); setQuery(""); setRange("24h"); setCustomRange(null);
  };
  const anyFilter = actionFilter !== "all" || userFilter !== "all" || query !== "" || range !== "24h";

  /* AN INFINITE FETCH LOOP LIVED HERE, and it was mine.
     `const now = Date.now()` ran on every render and fed `windowStart` /
     `windowEnd` straight into the fetch's dependency array. So: fetch resolves
     -> setState -> re-render -> a new `now` -> the deps changed -> fetch again.
     Forever, against a real database, on the one screen whose table is the
     largest.
     Caught by the console-error stream of tools/smoke.js ("Maximum update depth
     exceeded"), NOT by its pass/fail: all 80 routes rendered, so the run was
     green. A route that renders is not a route that behaves.
     The window is pinned to a timestamp taken when the RANGE changes, not when
     React re-renders. "Last 24h" now means 24h before you chose it, which is
     also what the operator means. */
  const [actAnchor, setActAnchor] = useState(() => Date.now());
  React.useEffect(() => { setActAnchor(Date.now()); }, [range, customRange]);
  const windowStart = range === "custom" && customRange
    ? customRange.startMs
    : Math.max(0, actAnchor - (ACT_PRESET_MS[range] || ACT_PRESET_MS["24h"]));
  const windowEnd = range === "custom" && customRange ? customRange.endMs : actAnchor;

  /* THE FEED. `window.MOCK.ACTIVITY` was a fabricated audit trail — the one
     kind of invented data that is actively dangerous, because this screen's own
     explainer calls itself "the compliance source-of-truth". Six invented
     operators approving invented transactions, under a heading that says the
     entries are immutable evidence.

     THE TIME WINDOW IS SENT TO THE SERVER. An audit log is the table most
     likely to be large, and slicing a page of it client-side means "no activity
     in this window" whenever the window is older than the last N rows. */
  const actFeed = useHrsFetch(() => window.sb.list("activityLogs", {
    limit: 500,
    filters: {
      ...(brand && !brand.isAll && brand.id ? { skin: brand.id } : {}),
      ...(userFilter !== "all" ? { actor: userFilter } : {}),
      ...(actionFilter !== "all" ? { type: actionFilter } : {}),
      ...(query.trim() ? { action: query.trim() } : {}),
      ...(windowStart > 0 ? { from: new Date(windowStart).toISOString() } : {}),
      to: new Date(windowEnd).toISOString(),
    },
  }), [brand && brand.id, brand && brand.isAll, userFilter, actionFilter, query, windowStart, windowEnd]);

  const actTypeFeed = useHrsFetch(() => window.sb.list("activityLogTypes", { limit: 50 }), []);

  const filtered = useMemo(() => (actFeed.data || []).map(r => ({
    id: r.id,
    ts: r.occurred_at ? Date.parse(r.occurred_at) : null,
    actorId: r.actor_id,
    /* An actor_id with ON DELETE SET NULL: the row survives the account. Named
       as a deleted account rather than left blank, because a blank actor in an
       audit trail reads as a system action. */
    actorName: r.actor ? r.actor.username : (r.actor_id ? `user ${r.actor_id}` : "deleted account"),
    actorRole: r.actor ? roleLabelFor(r.actor.user_level) : "—",
    targetName: r.target ? r.target.username : null,
    targetId: r.target_user_id,
    action: r.action || "—",
    typeLabel: r.type ? r.type.label : `type ${r.log_type_id}`,
    typeId: r.log_type_id,
    skinName: r.skin ? r.skin.name : "—",
    ip: r.ip || null,
    /* jsonb. Rendered as compact JSON rather than interpreted: this build does
       not know what any given writer puts in there, and a made-up field name is
       how a diff turns into a claim. */
    detail: (r.detail && Object.keys(r.detail).length) ? JSON.stringify(r.detail) : null,
  })), [actFeed.data]);

  /* The operator dropdown, built from the actors actually present. Was
     MOCK.OP_USERS — six invented names that did not correspond to any account
     that could have taken an action. */
  const actActors = useMemo(() => {
    const seen = new Map();
    filtered.forEach(a => { if (a.actorId && !seen.has(a.actorId)) seen.set(a.actorId, a.actorName); });
    return [...seen.entries()].sort((x, y) => String(x[1]).localeCompare(String(y[1])));
  }, [filtered]);

  // Group by day
  const groups = {};
  filtered.forEach(a => {
    const key = a.ts
      ? new Date(a.ts).toLocaleDateString("en-GB", { weekday: "long", day: "2-digit", month: "short" })
      : "Undated";
    if (!groups[key]) groups[key] = [];
    groups[key].push(a);
  });

  const timeAgo = (ts) => {
    if (!ts) return "—";
    const diff = Date.now() - ts;
    if (diff < 60_000) return "just now";
    if (diff < 3600_000) return `${Math.floor(diff/60_000)}m ago`;
    if (diff < 86400_000) return `${Math.floor(diff/3600_000)}h ago`;
    return `${Math.floor(diff/86400_000)}d ago`;
  };

  /* THE CHIP ROW WAS A TAXONOMY THIS SCHEMA DOES NOT HAVE. Five hardcoded
     kinds — psp_login / approve / reject / method_edit / note — with icons and
     colours, none of which is a value `activity_logs` can hold: `action` is
     free text and the only classification on the row is `log_type_id`, which
     isystem persists as a BOOLEAN and so can separate exactly two things.
     Built from activity_log_types, which is what the database actually
     distinguishes. */
  const ACT_TYPE_CHIPS = useMemo(() => [
    ["all", "All", null, "var(--text-secondary)"],
    ...(actTypeFeed.data || []).map(t => [
      String(t.id), t.label,
      t.code === "block" ? "lock" : "activity",
      t.code === "block" ? "var(--err-600)" : "var(--p-600)",
    ]),
  ], [actTypeFeed.data]);

  return (
    <div className="page">
      <div className="page__header">
        <div>
          <div className="page__title" style={{display:"inline-flex", alignItems:"center"}}>
            {T("page.activity","Activity log")}
            <Tip>
              Immutable audit trail of every back-office action: PSP logins, withdrawal approvals / rejections, payment-method edits, player notes, alarm fires. One row per event with operator id, timestamp (minute precision), and a short diff. Read-only — this log is the compliance source-of-truth.
            </Tip>
          </div>
          <div className="page__subtitle">Logged back-office events — actor, action, affected user · {brand.name}</div>
        </div>
        <div className="page__actions">
          <button className="btn btn--secondary btn--sm"
            onClick={() => {
              if (!window.PAYBO) return;
              const rows = filtered.map(a => ({
                occurred_at: a.ts ? new Date(a.ts).toISOString() : "",
                actor_id: a.actorId || "",
                actor_name: a.actorName,
                actor_role: a.actorRole,
                log_type: a.typeLabel,
                action: a.action,
                target_user_id: a.targetId || "",
                target_username: a.targetName || "",
                skin: a.skinName,
                ip: a.ip || "",
                detail: a.detail || "",
              }));
              const columns = [
                { key:"occurred_at",     label:"occurred_at" },
                { key:"actor_id",        label:"actor_id" },
                { key:"actor_name",      label:"actor_name" },
                { key:"actor_role",      label:"actor_role" },
                { key:"log_type",        label:"log_type" },
                { key:"action",          label:"action" },
                { key:"target_user_id",  label:"target_user_id" },
                { key:"target_username", label:"target_username" },
                { key:"skin",            label:"skin" },
                { key:"ip",              label:"ip" },
                { key:"detail",          label:"detail" },
              ];
              const stamp = new Date().toISOString().slice(0,10);
              window.PAYBO.downloadCSV(`paybo-activity-${stamp}.csv`, rows, columns);
            }}>
            <Icon name="download" size={13}/> {T("btn.exportCSV","Export CSV")}
          </button>
        </div>
      </div>

      {/* THE EXPLAINER DESCRIBED A SCHEMA THAT DOES NOT EXIST — five action
          kinds with field-level diffs, none of which `activity_logs` holds.
          What it holds is an actor, a free-text action, an optional target
          user, a jsonb detail and an IP. Said plainly, including the part that
          matters most: upstream, almost nothing writes to it. */}
      <Explainer compact title="What this is, in plain English"
        bullets={[
          <><strong>Who</strong> — <code>actor_id</code>, the signed-in operator, with their role at the time of reading. <code>ON DELETE SET NULL</code>, so a row outlives the account and shows as "deleted account" rather than blank.</>,
          <><strong>What</strong> — <code>action</code> is free text written by whoever logged the event. It is shown verbatim; this screen does not classify it, because the only classification the table carries is <code>log_type_id</code>, and isystem persists that as a boolean.</>,
          <><strong>To whom</strong> — <code>target_user_id</code> when the action was about somebody. Rows without one are visible to any operator of the brand; rows with one are visible only inside that user's subtree.</>,
          <><strong>Detail</strong> — a <code>jsonb</code> blob, printed as-is. Interpreting it would mean inventing field names, and an invented field name in an audit trail is a claim.</>,
          <><strong>Read-only</strong> — <code>update</code> and <code>delete</code> are revoked on the table itself, not merely absent from the UI.</>,
          <><strong>Expect it to be empty.</strong> Upstream, <code>LogsController::saveLog</code> is a no-op — its body is entirely commented out — so every "audit row written" in the player-create, user-update and coupon-cancel flows writes nothing. There is no history to import.</>,
        ]}>
        Operator audit trail: one row per logged back-office event, with the actor, the action text, the affected user, an IP and a jsonb detail.
      </Explainer>

      <div className="panel" style={{overflow:"hidden"}}>
        {/* Filter bar — search, operator, range */}
        <div style={{
          padding:"12px 14px",
          display:"flex", gap:10, alignItems:"center", flexWrap:"wrap",
          borderBottom:"1px solid var(--border-subtle)"
        }}>
          <div style={{
            position:"relative", flex:"1 1 280px", maxWidth:380,
            background:"#fff", border:"1px solid var(--border-default)", borderRadius:10,
            display:"flex", alignItems:"center"
          }}>
            <Icon name="search" size={13} style={{position:"absolute", left:12, top:"50%", transform:"translateY(-50%)", color:"var(--text-tertiary)"}}/>
            <input value={query} onChange={e=>setQuery(e.target.value)}
              placeholder="Search by operator, Tx ID, player…"
              style={{width:"100%", border:"none", outline:"none", padding:"8px 12px 8px 34px", fontSize:12.5, borderRadius:10, background:"transparent", fontFamily:"inherit"}}/>
          </div>

          <div style={{background:"#fff", border:"1px solid var(--border-default)", borderRadius:10, padding:"4px 4px 4px 10px", display:"inline-flex", alignItems:"center", gap:6}}>
            <Icon name="users" size={12} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11.5, color:"var(--text-tertiary)", fontWeight:600}}>Operator</span>
            <Tip>Filter the log to a single back-office user. Useful when auditing one team member's approvals / rejections, or when investigating a compromised account.</Tip>
            <select value={userFilter} onChange={e=>setUserFilter(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12.5, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"4px 6px"}}>
              <option value="all">All</option>
              {actActors.map(([id, name]) => <option key={id} value={id}>{name}</option>)}
            </select>
          </div>

          <div style={{position:"relative", display:"inline-flex"}}>
            <div style={{background:"#fff", border:"1px solid var(--border-default)", borderRadius:10, padding:4, display:"inline-flex", gap:2}}>
              {[["1h","1h"],["24h","24h"],["7d","7d"],["all","All"]].map(([k,l])=>(
                <button key={k} onClick={()=>{ setRange(k); setCustomOpen(false); }}
                  style={{
                    padding:"5px 10px", border:"none", borderRadius:7, fontSize:12, fontWeight:600, cursor:"pointer",
                    background: range===k ? "var(--p-500)" : "transparent",
                    color: range===k ? "#fff" : "var(--text-secondary)",
                    transition:"all .12s"
                  }}>{l}</button>
              ))}
              <button onClick={()=>{ setRange("custom"); setCustomOpen(o => !o); }}
                title={range === "custom" && customRange ? customRange.label : "Pick a custom range or duration"}
                style={{
                  padding:"5px 10px", border:"none", borderRadius:7, fontSize:12, fontWeight:600, cursor:"pointer",
                  background: range==="custom" ? "var(--p-500)" : "transparent",
                  color: range==="custom" ? "#fff" : "var(--text-secondary)",
                  transition:"all .12s"
                }}>Custom{range==="custom" && customRange ? ` · ${customRange.label}` : ""}</button>
            </div>
            {customOpen && (
              <CustomRangePopover
                initial={customRange ? { ...customRange } : { mode:"last", n: 6, unit:"hour" }}
                onApply={(r) => { setCustomRange(r); setRange("custom"); setCustomOpen(false); }}
                onCancel={() => setCustomOpen(false)}/>
            )}
          </div>

          {anyFilter && (
            <button className="paybo-btn paybo-btn--ghost" style={{padding:"6px 10px"}} onClick={clearFilters}>
              Clear all
            </button>
          )}

          <div style={{marginLeft:"auto", fontSize:12, color:"var(--text-tertiary)", fontWeight:500}}>
            <strong style={{color:"var(--text-primary)"}}>{filtered.length.toLocaleString()}</strong> {filtered.length===1?"event":"events"}
          </div>
        </div>

        {/* Action-type chip row */}
        <div style={{padding:"10px 14px", display:"flex", gap:6, flexWrap:"wrap", borderBottom:"1px solid var(--border-subtle)"}}>
          {ACT_TYPE_CHIPS.map(([k, lab, icon, color]) => {
            const active = actionFilter === k;
            return (
              <button key={k} onClick={() => setActionFilter(k)}
                style={{
                  display:"inline-flex", alignItems:"center", gap:6,
                  padding:"5px 11px", borderRadius:999,
                  border: "1px solid " + (active ? color : "var(--border-default)"),
                  background: active ? color + "14" : "#fff",
                  color: active ? color : "var(--text-secondary)",
                  fontSize:12, fontWeight:600, cursor:"pointer",
                  transition:"all .12s",
                }}>
                {icon && <Icon name={icon} size={11}/>}
                {lab}
              </button>
            );
          })}
        </div>

        {/* Feed */}
        <div style={{padding:"8px 0 14px", maxHeight:"calc(100vh - 280px)", overflow:"auto"}}>
          {actFeed.loading && <HrsSkeleton rows={6} cols={3} />}
          {!actFeed.loading && actFeed.error && <HrsError error={actFeed.error} onRetry={actFeed.retry} />}
          {!actFeed.loading && !actFeed.error && Object.keys(groups).length === 0 && (
            <div style={{padding:"40px 16px", textAlign:"center", color:"var(--text-tertiary)"}}>
              <div style={{display:"inline-flex", flexDirection:"column", alignItems:"center", gap:8}}>
                <div style={{width:44, height:44, borderRadius:12, background:"var(--n-50)", display:"grid", placeItems:"center"}}><Icon name="activity" size={18}/></div>
                <div style={{fontSize:13, fontWeight:600, color:"var(--text-secondary)"}}>No logged activity in this window</div>
                {/* NOT "try a wider range" ALONE. The likeliest reason this is
                    empty is that nothing writes to the table — saying only
                    "expand the range" sends an operator hunting for events that
                    were never recorded. */}
                <div style={{fontSize:12, maxWidth:420, lineHeight:1.5}}>
                  Either nothing was logged in this range, or nothing writes to <code>activity_logs</code> yet —
                  upstream, the function that was supposed to is commented out. Widening the range is worth a try;
                  an empty log is not evidence that nothing happened.
                </div>
              </div>
            </div>
          )}
          {Object.entries(groups).map(([day, items]) => (
            <div key={day}>
              <div style={{padding:"10px 18px 6px", fontSize:10.5, fontWeight:700, textTransform:"uppercase", letterSpacing:".08em", color:"var(--text-tertiary)", display:"flex", alignItems:"center", gap:10, position:"sticky", top:0, background:"linear-gradient(180deg, var(--n-0) 80%, transparent)"}}>
                <span>{day}</span>
                <span style={{flex:1, height:1, background:"var(--border-subtle)"}}/>
                <span style={{color:"var(--text-tertiary)", fontWeight:600, textTransform:"none", letterSpacing:0}}>{items.length}</span>
              </div>
              {items.map(a => (
                <div key={a.id}
                  style={{padding:"10px 18px", display:"flex", gap:12, borderBottom:"1px solid var(--border-subtle)", transition:"background .1s"}}
                  onMouseEnter={e=>e.currentTarget.style.background="var(--n-25)"}
                  onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
                  <div style={{
                    width:30, height:30, borderRadius:999, flexShrink:0,
                    background:actAvatarColor(a.actorId), color:"#fff", display:"grid", placeItems:"center",
                    fontSize:11, fontWeight:700,
                  }}>{actInitials(a.actorName)}</div>
                  <div style={{flex:1, minWidth:0}}>
                    <div style={{fontSize:13, lineHeight:1.4}}>
                      <strong style={{color:"var(--text-primary)"}}>{a.actorName}</strong>
                      <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6, fontWeight:500}}>· {a.actorRole}</span>
                      {/* THE ACTION, VERBATIM. `activity_logs.action` is free
                          text written by whoever logged the event; the old
                          version rendered a verb from a five-value taxonomy
                          that does not exist in this schema, so a row could be
                          shown as "approved" whatever it recorded. */}
                      <span style={{color:"var(--text-secondary)", margin:"0 6px"}}>{a.action}</span>
                      {a.targetName && (
                        <>
                          <strong style={{color:"var(--text-primary)"}}>{a.targetName}</strong>
                          <span className="mono" style={{color:"var(--text-tertiary)", marginLeft:6, fontSize:11}}>#{a.targetId}</span>
                        </>
                      )}
                    </div>
                    <div style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:3, display:"flex", alignItems:"center", gap:8, flexWrap:"wrap"}}>
                      <span>{a.skinName}</span>
                      {a.ip && (<><span>·</span><span className="mono">{a.ip}</span></>)}
                      {a.detail && (
                        <>
                          <span>·</span>
                          <span className="mono" style={{color:"var(--text-secondary)", wordBreak:"break-all"}}>{a.detail}</span>
                        </>
                      )}
                    </div>
                  </div>
                  <div style={{display:"flex", alignItems:"center", gap:8, flexShrink:0}}>
                    <span style={{
                      padding:"3px 8px", borderRadius:999, fontSize:10.5, fontWeight:700, letterSpacing:".03em",
                      background: a.typeId === 1 ? "var(--err-50)" : "var(--p-50)",
                      color: a.typeId === 1 ? "var(--err-600)" : "var(--p-600)",
                      display:"inline-flex", alignItems:"center", gap:4
                    }}>
                      <Icon name={a.typeId === 1 ? "lock" : "activity"} size={10}/>
                      {a.typeLabel}
                    </span>
                    <span style={{fontSize:11, color:"var(--text-tertiary)", minWidth:64, textAlign:"right"}}>{timeAgo(a.ts)}</span>
                  </div>
                </div>
              ))}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

window.Activity = Activity;
