// 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/{transactions,deposits,withdrawals}/ · AdminPaymentsController::show() → transactionListData() — see docs/ISYSTEM_REFERENCE.md §Batch 10.1
/* Traced Aug 2026 (architecture item 2). One list serving three sections:
   FORCED_TYPE_BY_SECTION pins the type for deposits and withdrawals, which is
   why /payments/deposits and /payments/withdrawals render this same file.
   Writes go to POST /payments/transactions/action.
   The `to_confirm` STATUS TAB below is a filter over transactions. It is NOT
   the To Confirm SECTION (/payments/to-confirm), which queues
   payment_cascade_holds — a different table and a different entity. See
   PayboToConfirm.jsx and Batch 10.2. */
/* Transactions list */
const TX_COLUMNS = [
  { key:"id",          label:"ID",          required:true  },
  { key:"internal_id", label:"Internal ID", required:false },
  { key:"brand",       label:"Brand",       required:false },
  { key:"type",        label:"Type",        required:false },
  { key:"player",      label:"Player",      required:false },
  { key:"user_id",     label:"User ID",     required:false },
  { key:"method",      label:"Method",      required:false },
  { key:"provider",    label:"Provider",    required:false },
  { key:"tx_id",       label:"Tx ID",       required:false },
  { key:"origin_id",   label:"Origin ID",   required:false },
  { key:"amount",      label:"Amount",      required:true  },
  { key:"fee",         label:"Fee",         required:false },
  { key:"status",      label:"Status",      required:true  },
  { key:"promo_code",  label:"Promo code",  required:false },
  { key:"env",         label:"Env",         required:false },
  { key:"created",     label:"Created",     required:false },
];
const TX_COLUMN_DEFAULTS = TX_COLUMNS.map(c => c.key); // all on by default

/* ONE ROW SHAPE, FROM TWO TABLES. A deposit request and a withdrawal request
   are different tables with different status lists and different columns —
   `deposit_requests` has a promocode and a payer document, `withdrawal_requests`
   has payout details and a paid_at. This screen shows them in one list, so they
   are mapped onto one shape here and the mapping is the only place that knows
   which side a row came from.

   THE STATUS SETS ARE NOT THE SAME and must not be merged. Each table has its
   own lookup (`deposit_request_statuses` / `withdrawal_request_statuses`), so
   the filter offers the union with the side shown, rather than a single
   invented list of eight strings that matched neither. */
const txRowFromDeposit = (r) => ({
  id: String(r.id),
  rowKind: "deposit",
  brand: r.skin_id == null ? null : Number(r.skin_id),
  brand_name: r.skin ? r.skin.name : "",
  brand_short: r.skin ? String(r.skin.name || "").slice(0, 3).toUpperCase() : "",
  internal_id: r.token || "",
  method: r.method_id == null ? null : Number(r.method_id),
  method_name: r.method ? r.method.name : "",
  method_kind: r.method ? r.method.code : "",
  provider_id: r.provider_id == null ? null : Number(r.provider_id),
  provider_name: r.provider ? r.provider.name : "",
  user_id: r.user ? r.user.username : String(r.user_id),
  user_name: r.user ? r.user.username : "",
  transaction_id: r.provider_reference || "",
  amount: Number(r.amount) || 0,
  /* THE FEE ACTUALLY CHARGED, from 029's `fee_amount` — not a rate recomputed
     at read time, which produces a number that was never charged. NULL until
     the PSP reports it on settlement, and null renders "—": a 0.00 would say
     the transfer was free. */
  fee: r.fee_amount == null ? null : Number(r.fee_amount),
  currency: r.currency || (r.user ? r.user.currency : "") || "",
  type: "Deposit",
  status: r.status ? r.status.code : "",
  status_label: r.status ? r.status.label : "",
  /* `is_test` is the real column. The old row carried an invented
     prod/staging "environment" — this platform has no such split. */
  environment: r.is_test ? "test" : "live",
  promo_code: r.promocode || null,
  created_at: Date.parse(r.created_at) || 0,
  updated_at: Date.parse(r.updated_at || r.created_at) || 0,
  _raw: r,
});
const txRowFromWithdrawal = (r) => ({
  id: String(r.id),
  rowKind: "withdrawal",
  brand: r.skin_id == null ? null : Number(r.skin_id),
  brand_name: r.skin ? r.skin.name : "",
  brand_short: r.skin ? String(r.skin.name || "").slice(0, 3).toUpperCase() : "",
  internal_id: r.token || "",
  method: r.method_id == null ? null : Number(r.method_id),
  method_name: r.method ? r.method.name : "",
  method_kind: r.method ? r.method.code : "",
  provider_id: r.provider_id == null ? null : Number(r.provider_id),
  provider_name: r.provider ? r.provider.name : "",
  user_id: r.user ? r.user.username : String(r.user_id),
  user_name: r.user ? r.user.username : "",
  transaction_id: r.token || "",
  amount: Number(r.amount) || 0,
  fee: r.fee_amount == null ? null : Number(r.fee_amount),
  currency: r.currency || (r.user ? r.user.currency : "") || "",
  type: "Withdrawal",
  status: r.status ? r.status.code : "",
  status_label: r.status ? r.status.label : "",
  /* 029 added is_test here too — before it, a test PAYOUT was
     indistinguishable from a real one in every report that summed them. */
  environment: r.is_test ? "test" : "live",
  /* Withdrawals never carry one — a promo code is a deposit-side construct,
     which is a fact about the schema here rather than a 22% chance. */
  promo_code: null,
  created_at: Date.parse(r.created_at) || 0,
  updated_at: Date.parse(r.updated_at || r.created_at) || 0,
  _raw: r,
});

const Transactions = ({ brand, initialTab, onOpenDetail }) => {
  /* WAS `window.MOCK` — 500 generated transactions with invented brands,
     methods, player names, phone numbers, promo codes and fees. */
  const txDepFeed = useHrsFetch(() => window.sb.list("depositRequests", { limit: 2000 }), []);
  const txWdFeed = useHrsFetch(() => window.sb.list("withdrawalRequests", { limit: 2000 }), []);
  const txMethodFeed = useHrsFetch(() => window.sb.list("paymentMethods", { limit: 300 }), []);
  const txSkinFeed = useHrsFetch(() => window.sb.list("skins", { limit: 200 }), []);
  const txBusy = txDepFeed.loading || txWdFeed.loading;
  const txErr = txDepFeed.error || txWdFeed.error;
  const TRANSACTIONS = useMemo(
    () => (txDepFeed.data || []).map(txRowFromDeposit)
      .concat((txWdFeed.data || []).map(txRowFromWithdrawal))
      .sort((a, b) => b.created_at - a.created_at),
    [txDepFeed.data, txWdFeed.data]);
  const METHODS = useMemo(
    () => (txMethodFeed.data || []).map(m => ({ id: Number(m.id), name: m.name, kind: m.code })),
    [txMethodFeed.data]);
  /* The status list is the UNION of what the two tables actually returned, not
     a hand-written set. A status nobody has ever recorded is not offered. */
  const STATUS_KEYS = useMemo(() => {
    const seen = new Map();
    TRANSACTIONS.forEach(t => { if (t.status && !seen.has(t.status)) seen.set(t.status, t.status_label || t.status); });
    return Array.from(seen.keys());
  }, [TRANSACTIONS]);
  const STATUSES = STATUS_KEYS;

  const [query, setQuery] = useState("");
  const [selected, setSelected] = useState(null);
  const [statusFilter, setStatusFilter] = useState("all");
  const [methodFilter, setMethodFilter] = useState("all");
  const [timeframe, setTimeframe] = useState("14d");
  const [customRange, setCustomRange] = useState(null); // { startMs, endMs, label }
  const [customOpen, setCustomOpen] = useState(false);
  const [envFilter, setEnvFilter] = useState("all");
  const [typeFilter, setTypeFilter] = useState("all");
  const [providerFilter, setProviderFilter] = useState("all");
  // Per-page Brand filter, only meaningful when the top-right selector is
  // on "All brands". When the operator picks a specific brand at the top
  // right (auto-login), this filter is hidden and the page is locked.
  const [brandFilter, setBrandFilter] = useState("all");
  const BRANDS = useMemo(
    () => (txSkinFeed.data || []).map(s => ({ id: Number(s.id), name: s.name, short: String(s.name || "").slice(0, 3).toUpperCase(), currency: s.currency })),
    [txSkinFeed.data]);
  useEffect(() => { if (!brand.isAll) setBrandFilter("all"); }, [brand.id, brand.isAll]);
  /* THE PROVIDER IS ON THE ROW, not looked up from a routing table. Both
     request tables carry `provider_id` — the provider that actually handled the
     request — so `lookupMethodProvider` was answering "which provider would we
     route this method to?", a different and sometimes wrong question: a request
     routed to a cascade fallback is handled by one provider and would have been
     labelled with another. */
  const txProvFeed = useHrsFetch(() => window.sb.list("paymentProviders", { limit: 300 }), []);
  const providers = useMemo(
    () => (txProvFeed.data || []).map(p => ({ id: Number(p.id), name: p.name })),
    [txProvFeed.data]);
  const providerOf = (t) => (t.provider_id == null ? null : { id: t.provider_id, name: t.provider_name });
  const [minAmount, setMinAmount] = useState("");
  const [maxAmount, setMaxAmount] = useState("");
  // Promo code filter — four modes matching the Reports page:
  //   "all"  = ignore (default)
  //   "any"  = only transactions that carry any promo code
  //   "none" = only transactions without a promo code
  //   "code" = match exactly the operator-typed promo code in promoCodeInput
  const [promoMode, setPromoMode] = useState("all");
  const [promoCodeInput, setPromoCodeInput] = useState("");
  const promoCodeNeedle = promoCodeInput.trim().toUpperCase();
  // Distinct promo codes seen across all transactions — drives the
  // autocomplete datalist for "Specific code".
  const ALL_PROMO_CODES = useMemo(() => {
    const set = new Set();
    for (const t of TRANSACTIONS) if (t.promo_code) set.add(t.promo_code);
    return Array.from(set).sort();
  }, [TRANSACTIONS]);
  const [page, setPage] = useState(0);
  const PAGE_SIZE = 50;
  // Column visibility — persisted to localStorage so the operator's
  // preference survives reloads. Required columns can't be hidden.
  const [visibleCols, setVisibleCols] = useState(() => {
    try {
      const stored = pbStore.get("pb-tx-columns", null);
      if (Array.isArray(stored) && stored.length) return stored;
    } catch (_) {}
    return TX_COLUMN_DEFAULTS;
  });
  useEffect(() => { pbStore.set("pb-tx-columns", visibleCols); }, [visibleCols]);
  const isVisible = (key) => visibleCols.includes(key);
  const toggleCol = (key) => {
    const col = TX_COLUMNS.find(c => c.key === key);
    if (col?.required) return;
    setVisibleCols(cur => cur.includes(key) ? cur.filter(k => k !== key) : [...cur, key]);
  };
  const resetCols = () => setVisibleCols(TX_COLUMN_DEFAULTS);
  const visibleColCount = visibleCols.length;
  const [colMenuOpen, setColMenuOpen] = useState(false);
  /* Local "within" tab (All / Balanced / Pending / Needs review).
     DECLARED ABOVE the effect that lists it as a dependency — it used to sit
     twelve lines below. In a real module that is a ReferenceError; through
     in-browser Babel the dependency was simply `undefined` on every render, so
     the effect never saw the tab change and the page stayed on whatever page
     number it was on when the operator switched tabs. Silent, and found by
     tools/tdzcheck.js rather than by anyone using it. */
  const [tab, setTab] = useState("all");

  // Reset to first page whenever filter inputs change.
  useEffect(() => { setPage(0); }, [query, statusFilter, methodFilter, timeframe, envFilter, typeFilter, minAmount, maxAmount, providerFilter, brandFilter, promoMode, promoCodeInput, tab]);

  // Derive active page from nav
  const activePage = initialTab === "deposits" ? "deposits"
                   : initialTab === "withdrawals" ? "withdrawals"
                   : "transactions";

  const forcedType = activePage === "deposits" ? "Deposit"
                   : activePage === "withdrawals" ? "Withdrawal"
                   : null;


  // Timeframe windows (in ms)
  const tfMs = { "15m":15*60_000, "30m":30*60_000, "1h":3600_000, "12h":12*3600_000, "24h":24*3600_000, "7d":7*86400_000, "14d":14*86400_000, "30d":30*86400_000, "all": Infinity };
  const now = Date.now();

  const inTimeframe = (ts) => {
    if (timeframe === "custom" && customRange) return ts >= customRange.startMs && ts <= customRange.endMs;
    if (timeframe === "all") return true;
    return (now - ts) <= tfMs[timeframe];
  };

  // Base scope: brand (top-right auto-login OR in-page filter) + type
  // (page) + timeframe + env.
  const scoped = TRANSACTIONS.filter(t => {
    if (!brand.isAll && t.brand !== brand.id) return false;
    if (brand.isAll && brandFilter !== "all" && t.brand !== brandFilter) return false;
    if (forcedType && t.type !== forcedType) return false;
    if (!inTimeframe(t.created_at)) return false;
    /* `live` / `test` from deposit_requests.is_test — a real column. It used to
       be an invented prod/staging split at a 92/8 ratio. */
    if (envFilter !== "all" && t.environment !== envFilter) return false;
    return true;
  });

  // Live search across id, internal id, transaction id, origin, player name, phone, user id
  const q = query.trim().toLowerCase();
  const searched = !q ? scoped : scoped.filter(t =>
    (t.id||"").toLowerCase().includes(q) ||
    (t.internal_id||"").toLowerCase().includes(q) ||
    (t.transaction_id||"").toLowerCase().includes(q) ||
    (t.transaction_origin_id||"").toLowerCase().includes(q) ||
    (t.user_name||"").toLowerCase().includes(q) ||
    (t.user_id||"").toLowerCase().includes(q) ||
    (t.phone||"").toLowerCase().includes(q)
  );

  // Every filter except the intra-page status tab — this is the pool the
  // tab badges count against, so a badge always matches what clicking that
  // tab actually shows (previously the badges counted off `searched` alone,
  // ignoring status/method/type/amount/provider/promo filters).
  const preTab = searched.filter(t => {
    if (statusFilter !== "all" && t.status !== statusFilter) return false;
    if (methodFilter !== "all" && t.method !== methodFilter) return false;
    if (typeFilter !== "all" && t.type !== typeFilter) return false;
    if (providerFilter !== "all") {
      const p = providerOf(t);
      if (!p || p.id !== providerFilter) return false;
    }
    const minN = parseFloat(minAmount); const maxN = parseFloat(maxAmount);
    if (!isNaN(minN) && t.amount < minN) return false;
    if (!isNaN(maxN) && t.amount > maxN) return false;
    if (promoMode === "any"  && !t.promo_code) return false;
    if (promoMode === "none" && !!t.promo_code) return false;
    if (promoMode === "code") {
      if (!t.promo_code) return false;
      if (promoCodeNeedle && (t.promo_code || "").toUpperCase() !== promoCodeNeedle) return false;
    }
    return true;
  });

  // Tab filtering (intra-page)
  let filtered = preTab.filter(t => {
    if (tab === "balanced" && !(t.status === "balanced" || t.status === "completed")) return false;
    if (tab === "to_confirm" && !(t.status === "to_confirm" || t.status === "review")) return false;
    if (tab === "pending" && t.status !== "pending") return false;
    if (tab === "failed" && !(t.status === "failed" || t.status === "declined" || t.status === "rejected")) return false;
    return true;
  });

  // QW3: on the To Confirm tab, apply the spec-mandated queue sort
  // (Amount DESC, then created_at ASC) instead of plain chronological.
  if (tab === "to_confirm" && window.PAYBO?.queueSort) {
    filtered = window.PAYBO.queueSort(filtered);
  }

  // Tab counts off of preTab so they always match what selecting that tab
  // would actually show, given every other active filter.
  const cnt = (pred) => preTab.filter(pred).length;
  const counts = {
    all: preTab.length,
    balanced: cnt(t => t.status === "balanced" || t.status === "completed"),
    to_confirm: cnt(t => t.status === "to_confirm" || t.status === "review"),
    pending: cnt(t => t.status === "pending"),
    failed: cnt(t => t.status === "failed" || t.status === "declined" || t.status === "rejected"),
  };

  const pageTitle = activePage === "deposits" ? "Deposits"
                  : activePage === "withdrawals" ? "Withdrawals"
                  : "Transactions";
  const pageSub = activePage === "deposits" ? `All incoming payments across ${brand.name}`
                : activePage === "withdrawals" ? `All outgoing payouts across ${brand.name}`
                : `All deposits and withdrawals across ${brand.name}`;

  // Timeframe hero chip options
  const TIMEFRAMES = [
    ["15m","Last 15 min"], ["30m","Last 30 min"], ["1h","Last 1 hour"],
    ["12h","Last 12 hours"], ["24h","Last 24 hours"], ["7d","Last 7 days"],
    ["14d","Last 14 days"], ["30d","Last 30 days"], ["all","All time"],
    ["custom", customRange ? `Custom · ${customRange.label}` : "Custom…"],
  ];

  // Page-specific in-context help — explains what each scope does.
  const pageTip = activePage === "deposits"
    ? <>This page shows every <strong>Deposit</strong> — money coming <em>into</em> the casino from a player. Filter by status (Balanced / To-Confirm / Pending / Failed), method, provider, country, amount, date range. Click a row to see the full audit trail.</>
    : activePage === "withdrawals"
    ? <>This page shows every <strong>Withdrawal</strong> — money going <em>out</em> to a player. <strong>To-Confirm</strong> withdrawals are paused for manual approval; they sort by Amount descending so the biggest ones surface first. Click a row to approve / reject / cascade.</>
    : <>This page shows every <strong>Transaction</strong> on the platform — deposits and withdrawals together. Use the tabs to slice by status, the filters to slice by method / provider / country / amount / date, and the gear at the right to hide columns you don't need.</>;

  return (
    <div className="page">
      <div className="page__header">
        <div>
          <div className="page__title" style={{display:"inline-flex", alignItems:"center"}}>
            {pageTitle}
            <Tip>{pageTip}</Tip>
          </div>
          <div className="page__subtitle">{pageSub}</div>
        </div>
        <div className="page__actions">
          <button className="btn btn--secondary btn--sm"
            onClick={() => {
              if (!window.PAYBO) return;
              const stamp = new Date().toISOString().slice(0,10);
              const scope = initialTab === "deposits" ? "deposits"
                          : initialTab === "withdrawals" ? "withdrawals"
                          : tab;
              window.PAYBO.downloadCSV(`paybo-transactions-${scope}-${stamp}.csv`, filtered, [
                { key:"id",           label:"id" },
                { key:"internal_id",  label:"internal_id" },
                { key:"created_at",   label:"created_at_iso", get:(t)=>new Date(t.created_at).toISOString() },
                { key:"user_id",      label:"user_id" },
                { key:"user_name",    label:"player" },
                { key:"brand_name",   label:"brand" },
                { key:"method_name",  label:"method" },
                { key:"type",         label:"type" },
                { key:"amount",       label:"amount" },
                { key:"approved_amount", label:"approved_amount" },
                { key:"fee",          label:"fee" },
                { key:"currency",     label:"currency" },
                { key:"status",       label:"status" },
                { key:"promo_code",   label:"promo_code" },
                { key:"reason_code",  label:"reason_code" },
                { key:"provider_ref", label:"provider_ref" },
                { key:"ip_address",   label:"ip" },
                { key:"environment",  label:"environment" },
              ]);
            }}>
            <Icon name="download" size={13}/> Export CSV
          </button>
          <div style={{position:"relative"}}>
            <button className="btn btn--secondary btn--sm"
              onClick={() => setColMenuOpen(o => !o)}
              title="Show / hide columns">
              <Icon name="settings" size={13}/> Columns
              <span style={{
                marginLeft:6, fontSize:10.5, fontWeight:700, padding:"1px 6px",
                borderRadius:999, background:"var(--n-75)", color:"var(--text-secondary)",
                fontVariantNumeric:"tabular-nums",
              }}>{visibleColCount}/{TX_COLUMNS.length}</span>
            </button>
            {colMenuOpen && (
              <ColumnVisibilityPopover
                columns={TX_COLUMNS}
                visible={visibleCols}
                onToggle={toggleCol}
                onReset={resetCols}
                onClose={() => setColMenuOpen(false)}/>
            )}
          </div>
          {/* This button shipped with no onClick at all — clicking it did
              literally nothing, silently. There is nothing to refresh without
              a server (the list is static mock data), so it names what it
              needs instead of pretending to reload. */}
          <NoBackend className="btn btn--secondary btn--sm" what="Refresh"
            need="the transactions list endpoint — the prototype list is static"><Icon name="refresh" size={13}/> Refresh</NoBackend>
        </div>
      </div>

      <Explainer compact title="What this is, in plain English"
        bullets={[
          <><strong>Deposit</strong> — money coming <em>into</em> the casino from a player.</>,
          <><strong>Withdrawal</strong> — money going <em>out</em> to a player.</>,
          <><strong>To-Confirm</strong> = paused for manual operator approval. <strong>Approved</strong> = the auto-approval engine or an operator approved the transaction (it is now passed to the PSP). <strong>Pending</strong> = sent and waiting on the PSP webhook. <strong>Balanced</strong> = terminal success — the PSP confirmed the transaction settled. <strong>Failed</strong> = declined / rejected / errored at any stage.</>,
          <><strong>Net</strong> on totals lines = Deposits − Withdrawals.</>,
        ]}>
        The full transactions log. Use the filter cards below to narrow by status, method, provider, country, amount, and date. Click a row to open the full audit trail (status timeline, routing decision, retries).
      </Explainer>

      {/* --- Hero filter strip --- */}
      <div className="filter-hero">
        {/* Search card */}
        <div className="filter-hero__card filter-hero__card--search">
          <div className="filter-hero__label">
            <Icon name="search" size={11}/> Search
          </div>
          <div style={{position:"relative"}}>
            <input
              value={query}
              onChange={e => setQuery(e.target.value)}
              className="filter-hero__input"
              placeholder="Transaction ID, player name, user ID, phone…"
            />
            {query && (
              <button
                onClick={() => setQuery("")}
                className="filter-hero__clear"
                title="Clear">
                <Icon name="x" size={11}/>
              </button>
            )}
          </div>
        </div>

        {/* Timeframe card */}
        <div className="filter-hero__card" style={{position:"relative"}}>
          <div className="filter-hero__label">
            <Icon name="calendar" size={11}/> Timeframe
            <Tip>Narrows the list to transactions whose creation time falls inside this window. "All time" disables the filter. "Custom…" opens a date-range picker for arbitrary windows.</Tip>
          </div>
          <select
            value={timeframe}
            onChange={e => {
              const v = e.target.value;
              setTimeframe(v);
              if (v === "custom") setCustomOpen(true);
            }}
            className="filter-hero__select">
            {TIMEFRAMES.map(([k,l]) => <option key={k} value={k}>{l}</option>)}
          </select>
          {customOpen && (
            <CustomRangePopover
              initial={customRange ? { ...customRange } : { mode:"range" }}
              onApply={(r) => { setCustomRange(r); setTimeframe("custom"); setCustomOpen(false); }}
              onCancel={() => { setCustomOpen(false); if (!customRange) setTimeframe("14d"); }}/>
          )}
        </div>

        {/* Status card */}
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="flag" size={11}/> Status
            <Tip>
              The lifecycle state of the transaction.<br/>
              <strong>To-Confirm</strong> = paused for manual operator approval (mostly withdrawals). <strong>Approved</strong> = the auto-approval engine or an operator approved it — now in the PSP's hands. <strong>Pending</strong> = sent to the PSP, waiting on a webhook. <strong>Balanced</strong> = terminal success — the PSP confirmed the transaction settled. <strong>Failed</strong> = declined / rejected / errored at any stage — the cascade may have already retried.
            </Tip>
          </div>
          <select
            value={statusFilter}
            onChange={e => setStatusFilter(e.target.value)}
            className="filter-hero__select">
            <option value="all">All statuses</option>
            {STATUS_KEYS.map(k => <option key={k} value={k}>{STATUSES[k].label}</option>)}
          </select>
        </div>

        {/* Brand card — only shown when the top-right is on "All brands".
            When a specific brand is selected at the top right, that pick
            acts as auto-login and locks the page. */}
        {brand.isAll && (
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="flag" size={11}/> Brand
            <Tip>Limit the list to one specific brand. Hidden when the top-right selector has already picked a single brand (that pick auto-logs you in to that tenant and locks every other brand filter).</Tip>
          </div>
          <select
            value={brandFilter}
            onChange={e => setBrandFilter(e.target.value)}
            className="filter-hero__select">
            <option value="all">All brands</option>
            {BRANDS.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
          </select>
        </div>
        )}

        {/* Method card */}
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="credit_card" size={11}/> Method
            <Tip>The payment instrument the player used (Visa, Mastercard, Apple Pay, Bank wire, Crypto…). Mapped from the brand's Payment Methods catalog.</Tip>
          </div>
          <select
            value={methodFilter}
            onChange={e => setMethodFilter(e.target.value)}
            className="filter-hero__select">
            <option value="all">All methods</option>
            {METHODS.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
          </select>
        </div>

        {/* Provider card */}
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="globe" size={11}/> Provider
            <Tip>The PSP (Stripe / Adyen / Checkout / Worldpay / Trustly …) that actually processed the transaction. Decided by the route's chain at request time, after the cascade has run.</Tip>
          </div>
          <select
            value={providerFilter}
            onChange={e => setProviderFilter(e.target.value)}
            className="filter-hero__select">
            <option value="all">All providers</option>
            {providers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </select>
        </div>

        {/* Environment card */}
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="sliders" size={11}/> Environment
            <Tip>Production = real money. Staging = test transactions only (sandbox PSP credentials). Use to keep test traffic from polluting the live operations view.</Tip>
          </div>
          <select
            value={envFilter}
            onChange={e => setEnvFilter(e.target.value)}
            className="filter-hero__select">
            <option value="all">All environments</option>
            {/* LIVE / TEST, from deposit_requests.is_test — a real column that
                marks a request raised against a test account. It was Production
                / Staging, an environment split this platform does not have. */}
            <option value="live">Live</option>
            <option value="test">Test</option>
          </select>
        </div>

        {/* Type card — Deposit vs Withdrawal */}
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="arrow_down_up" size={11}/> Type
            <Tip>
              <strong>Deposits</strong> = money coming into the casino from a player. <strong>Withdrawals</strong> = money the casino pays out to a player. "All types" shows both — useful when you want every transaction touching a single player or PSP.
            </Tip>
          </div>
          <select
            value={typeFilter}
            onChange={e => setTypeFilter(e.target.value)}
            className="filter-hero__select">
            <option value="all">All types</option>
            <option value="Deposit">Deposits only</option>
            <option value="Withdrawal">Withdrawals only</option>
          </select>
        </div>

        {/* Amount range card — min + max */}
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="arrow_up" size={11}/> Amount range
            <Tip>Filter to transactions whose absolute amount falls inside [min, max] in the brand's currency. Leave a side blank for an open-ended range. Useful for finding VIP-size deposits or low-value bot traffic.</Tip>
          </div>
          <div style={{display:"flex", gap:4, alignItems:"center"}}>
            <input value={minAmount} onChange={e=>setMinAmount(e.target.value)}
              placeholder="min" className="filter-hero__select" style={{width:64}}/>
            <span style={{color:"var(--text-tertiary)", fontSize:11}}>—</span>
            <input value={maxAmount} onChange={e=>setMaxAmount(e.target.value)}
              placeholder="max" className="filter-hero__select" style={{width:64}}/>
          </div>
        </div>

        {/* Promo code card — four modes (all / any / none / specific).
            When "Specific code" is picked an autocomplete input appears
            with the pool of codes seen in the live transactions log. */}
        <div className="filter-hero__card">
          <div className="filter-hero__label">
            <Icon name="percent" size={11}/> Promo code
            <Tip>Filter to transactions that did (or didn't) use a promotion / bonus code. Pick <strong>Any code applied</strong> to see every promo-driven transaction, <strong>No code applied</strong> to exclude bonus traffic, or <strong>Specific code…</strong> and type a code (e.g. <code>WELCOME100</code>) to see exactly which transactions used it. Promo eligibility per method is configured in Payment methods → General → Promotions.</Tip>
          </div>
          {promoMode !== "code" ? (
            <select
              value={promoMode}
              onChange={e => setPromoMode(e.target.value)}
              className="filter-hero__select">
              <option value="all">All transactions</option>
              <option value="any">Any code applied</option>
              <option value="none">No code applied</option>
              <option value="code">Specific code…</option>
            </select>
          ) : (
            <div style={{display:"flex", gap:4, alignItems:"center"}}>
              <input
                value={promoCodeInput}
                onChange={e=>setPromoCodeInput(e.target.value)}
                list="paybo-tx-promo-codes"
                placeholder="e.g. WELCOME100"
                className="filter-hero__select"
                style={{flex:1, fontFamily:"var(--font-mono)", textTransform:"uppercase", fontWeight:700, letterSpacing:".03em"}}/>
              <datalist id="paybo-tx-promo-codes">
                {ALL_PROMO_CODES.map(c => <option key={c} value={c}/>)}
              </datalist>
              <button
                onClick={() => { setPromoMode("all"); setPromoCodeInput(""); }}
                className="filter-hero__clear"
                style={{position:"static"}}
                title="Reset promo filter">
                <Icon name="x" size={11}/>
              </button>
            </div>
          )}
        </div>

        {/* Results card */}
        <div className="filter-hero__card filter-hero__card--result">
          <div className="filter-hero__label"><Icon name="chart" size={11}/> Results</div>
          <div className="filter-hero__value">
            {filtered.length.toLocaleString()}
            <span className="filter-hero__value-sub">of {scoped.length.toLocaleString()}</span>
          </div>
        </div>
      </div>

      {/* Active filter chips (clearable) */}
      {(query || statusFilter !== "all" || methodFilter !== "all" || envFilter !== "all" || timeframe !== "14d" || typeFilter !== "all" || minAmount !== "" || maxAmount !== "" || promoMode !== "all") && (
        <div style={{display:"flex", gap:6, flexWrap:"wrap", alignItems:"center", marginBottom:10, marginTop:-4}}>
          <span style={{fontSize:11, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em", fontWeight:600, marginRight:4}}>Active:</span>
          {query && <FilterPill label={`“${query}”`} onClear={() => setQuery("")}/>}
          {timeframe !== "14d" && <FilterPill label={TIMEFRAMES.find(t=>t[0]===timeframe)[1]} onClear={() => setTimeframe("14d")}/>}
          {statusFilter !== "all" && <FilterPill label={STATUSES[statusFilter]?.label} onClear={() => setStatusFilter("all")}/>}
          {methodFilter !== "all" && <FilterPill label={METHODS.find(m=>m.id===methodFilter)?.name} onClear={() => setMethodFilter("all")}/>}
          {typeFilter !== "all" && <FilterPill label={typeFilter + "s"} onClear={() => setTypeFilter("all")}/>}
          {(minAmount !== "" || maxAmount !== "") && <FilterPill label={`${minAmount || "0"} — ${maxAmount || "∞"}`} onClear={() => { setMinAmount(""); setMaxAmount(""); }}/>}
          {envFilter !== "all" && <FilterPill label={envFilter === "test" ? "Test" : "Live"} onClear={() => setEnvFilter("all")}/>}
          {promoMode === "any"  && <FilterPill label="Promo: any code"  onClear={() => setPromoMode("all")}/>}
          {promoMode === "none" && <FilterPill label="Promo: no code"   onClear={() => setPromoMode("all")}/>}
          {promoMode === "code" && <FilterPill label={`Promo: ${promoCodeInput || "(any)"}`} onClear={() => { setPromoMode("all"); setPromoCodeInput(""); }}/>}
          <button
            onClick={() => { setQuery(""); setStatusFilter("all"); setMethodFilter("all"); setEnvFilter("all"); setTimeframe("14d"); setTab("all"); setTypeFilter("all"); setMinAmount(""); setMaxAmount(""); setPromoMode("all"); setPromoCodeInput(""); }}
            style={{fontSize:11.5, color:"var(--p-600)", background:"none", border:"none", cursor:"pointer", fontWeight:600, padding:"4px 8px"}}>
            Clear all
          </button>
        </div>
      )}

      <div className="panel" style={{overflow:"hidden"}}>
        {/* Tabs */}
        <div style={{padding:"0 12px", borderBottom:"1px solid var(--border-default)", display:"flex", gap:2, alignItems:"flex-end", overflowX:"auto"}}>
          {[
            ["all", "All", counts.all, null],
            ["balanced", "Balanced", counts.balanced, "var(--ok-500)"],
            ["to_confirm", "To confirm", counts.to_confirm, "var(--info-500)"],
            ["pending", "Pending", counts.pending, "var(--warn-500)"],
            ["failed", "Failed / Declined / Rejected", counts.failed, "var(--err-500)"],
          ].map(([id, label, count, dot]) => (
            <div key={id} onClick={()=>setTab(id)}
                 style={{
                   padding:"10px 12px", fontSize:13, fontWeight:500, cursor:"pointer",
                   color: tab===id ? "var(--text-primary)" : "var(--text-secondary)",
                   borderBottom: tab===id ? "2px solid var(--p-600)" : "2px solid transparent",
                   marginBottom:-1, display:"flex", alignItems:"center", gap:7, whiteSpace:"nowrap",
                 }}>
              {dot && <span style={{width:6, height:6, borderRadius:999, background:dot}}/>}
              {label}
              <span style={{fontSize:11, fontWeight:600, background: tab===id?"var(--p-50)":"var(--n-75)", color: tab===id?"var(--p-700)":"var(--text-tertiary)", padding:"1px 6px", borderRadius:999, fontVariantNumeric:"tabular-nums"}}>{count}</span>
            </div>
          ))}
        </div>

        {/* Table */}
        <div style={{maxHeight:"calc(100vh - 420px)", overflow:"auto"}}>
          <table className="data-table">
            <thead>
              <tr>
                {isVisible("id")          && <th>ID</th>}
                {isVisible("internal_id") && <th>Internal ID</th>}
                {isVisible("brand")       && <th>Brand</th>}
                {isVisible("type")        && <th>Type</th>}
                {isVisible("player")      && <th>Player</th>}
                {isVisible("user_id")     && <th>User ID</th>}
                {isVisible("method")      && <th>Method</th>}
                {isVisible("provider")    && <th>Provider</th>}
                {isVisible("tx_id")       && <th>Tx ID</th>}
                {isVisible("origin_id")   && <th>Origin ID</th>}
                {isVisible("amount")      && <th style={{textAlign:"right"}}>Amount</th>}
                {isVisible("fee")         && <th style={{textAlign:"right"}}>Fee</th>}
                {isVisible("status")      && <th>Status</th>}
                {isVisible("promo_code")  && <th>Promo code</th>}
                {isVisible("env")         && <th>Env</th>}
                {isVisible("created")     && <th>Created</th>}
                <th style={{width:40}}></th>
              </tr>
            </thead>
            <tbody>
              {/* THREE STATES, NOT ONE. "No transactions match your filters" was
                  shown while the feed was still loading and after it had failed
                  — a claim about the data made before the data arrived. */}
              {txBusy && (
                <tr><td colSpan={visibleColCount + 1} style={{padding:"40px 20px", textAlign:"center", color:"var(--text-tertiary)"}}>
                  Loading transactions…
                </td></tr>
              )}
              {!txBusy && txErr && (
                <tr><td colSpan={visibleColCount + 1} style={{padding:"24px 20px"}}>
                  <HrsError error={txErr} onRetry={() => { txDepFeed.retry(); txWdFeed.retry(); }} />
                </td></tr>
              )}
              {!txBusy && !txErr && filtered.length === 0 && (
                <tr>
                  <td colSpan={visibleColCount + 1} style={{padding:"40px 20px", textAlign:"center", color:"var(--text-tertiary)"}}>
                    <Icon name="search" size={22} style={{opacity:.4, marginBottom:8}}/>
                    <div style={{fontSize:13.5, fontWeight:500, color:"var(--text-secondary)"}}>
                      {TRANSACTIONS.length ? "No transactions match your filters" : "No deposit or withdrawal requests yet"}
                    </div>
                    <div style={{fontSize:12, marginTop:2}}>
                      {TRANSACTIONS.length ? "Try clearing a filter or widening the timeframe." : "Requests appear here as players raise them."}
                    </div>
                  </td>
                </tr>
              )}
              {!txBusy && !txErr && filtered.slice(page*PAGE_SIZE, page*PAGE_SIZE + PAGE_SIZE).map(t => (
                <tr key={t.id} className={selected===t.id?"selected":""}
                    onClick={()=>{ setSelected(t.id); onOpenDetail && onOpenDetail(t); }}
                    style={{cursor:"pointer"}}>
                  {isVisible("id")          && <td><CopyableId value={t.id} display={highlight(t.id, q)}/></td>}
                  {isVisible("internal_id") && <td><CopyableId value={t.internal_id} display={highlight(t.internal_id, q)} style={{fontSize:11.5}} color="var(--text-tertiary)"/></td>}
                  {isVisible("brand")       && (
                    <td>
                      <span style={{display:"inline-flex", alignItems:"center", gap:6}}>
                        <span style={{width:16, height:16, borderRadius:4, background:t.brand_color, color:"#fff", fontSize:9, fontWeight:700, display:"grid", placeItems:"center"}}>{t.brand_short}</span>
                        <span style={{fontSize:12}}>{t.brand_name.split(" ")[0]}</span>
                      </span>
                    </td>
                  )}
                  {isVisible("type")        && <td><TypeChip type={t.type}/></td>}
                  {isVisible("player")      && <td style={{maxWidth:130, overflow:"hidden", textOverflow:"ellipsis"}}>{highlight(t.user_name, q)}</td>}
                  {isVisible("user_id")     && <td><CopyableId value={t.user_id} display={highlight(t.user_id, q)} style={{fontSize:11.5}} color="var(--text-primary)"/></td>}
                  {isVisible("method")      && (
                    <td style={{fontSize:12}}>
                      <span style={{display:"inline-flex", alignItems:"center", gap:6}}>
                        <span style={{width:6, height:6, borderRadius:999, background:t.method_color}}/>
                        {t.method_name.split(" ")[0]}
                      </span>
                    </td>
                  )}
                  {isVisible("provider")    && (() => {
                    const prov = providerOf(t);
                    return (
                      <td style={{fontSize:12}}>
                        {prov ? (
                          <span className="chip" style={{fontSize:10.5, fontWeight:600, padding:"2px 8px", borderRadius:999, background:"var(--p-50)", color:"var(--p-700)"}}>{prov.name}</span>
                        ) : <span style={{color:"var(--text-tertiary)"}}>—</span>}
                      </td>
                    );
                  })()}
                  {isVisible("tx_id")       && <td><CopyableId value={t.transaction_id} display={t.transaction_id.slice(0,14) + "…"} style={{fontSize:11}} color="var(--text-tertiary)"/></td>}
                  {isVisible("origin_id")   && <td><CopyableId value={t.transaction_origin_id} style={{fontSize:11}} color="var(--text-tertiary)"/></td>}
                  {isVisible("amount")      && <td style={{textAlign:"right", fontWeight:600}}><Money amount={t.amount} currency={t.currency}/></td>}
                  {/* "—" until the PSP reports it. `fee_amount` (029) is what
                      was actually charged, so a null means "not settled yet"
                      and a 0.00 would say the transfer was free. */}
                  {isVisible("fee")         && <td style={{textAlign:"right", color:"var(--text-tertiary)"}}>{t.fee == null ? "—" : <Money amount={t.fee} currency={t.currency}/>}</td>}
                  {isVisible("status")      && <td><StatusChip status={t.status}/></td>}
                  {isVisible("promo_code")  && (
                    <td>
                      {t.promo_code
                        ? <span title={`Promo code applied: ${t.promo_code}`} style={{display:"inline-flex", alignItems:"center", gap:5, fontSize:10.5, fontWeight:700, padding:"2px 8px", borderRadius:999, background:"var(--p-50)", color:"var(--p-700, #1e3a8a)", fontFamily:"var(--font-mono)", letterSpacing:".03em"}}>
                            <Icon name="check" size={9}/> {t.promo_code}
                          </span>
                        : <span style={{fontSize:11, color:"var(--text-tertiary)"}}>—</span>}
                    </td>
                  )}
                  {isVisible("env")         && <td>{t.environment === "test" ? <span className="chip chip--info" style={{fontSize:10}}>TEST</span> : <span className="chip chip--neutral" style={{fontSize:10}}>LIVE</span>}</td>}
                  {isVisible("created")     && <td style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{formatTs(t.created_at)}</td>}
                  <td>
                    {/* Approve / reject / flag all move real money and write an
                        audit row, so the menu is never simulated. Copy ID is
                        already available from the row's own id cell. */}
                    <NoBackend className="btn btn--ghost btn--icon btn--sm"
                      what="Row menu (approve / reject / flag)"
                      need="the transaction state-change endpoint + audit write">
                      <Icon name="more" size={13}/>
                    </NoBackend>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        {/* Footer pagination */}
        {(() => {
          const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
          const from = filtered.length === 0 ? 0 : page * PAGE_SIZE + 1;
          const to   = Math.min(filtered.length, (page + 1) * PAGE_SIZE);
          return (
            <div style={{padding:"10px 14px", borderTop:"1px solid var(--border-default)", display:"flex", alignItems:"center", gap:10, background:"var(--n-25)"}}>
              <div style={{fontSize:12, color:"var(--text-tertiary)"}}>
                Showing <strong style={{color:"var(--text-primary)"}}>{from.toLocaleString()}–{to.toLocaleString()}</strong> of <strong style={{color:"var(--text-primary)"}}>{filtered.length.toLocaleString()}</strong>
                <span style={{marginLeft:10}}>Page {page+1} of {totalPages}</span>
              </div>
              <div style={{marginLeft:"auto", display:"flex", gap:6}}>
                <button className="btn btn--secondary btn--sm" disabled={page === 0} onClick={() => setPage(p => Math.max(0, p - 1))}>
                  <Icon name="chevron_left" size={12}/> Prev
                </button>
                <button className="btn btn--secondary btn--sm" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>
                  Next <Icon name="chevron_right" size={12}/>
                </button>
              </div>
            </div>
          );
        })()}
      </div>
    </div>
  );
};

/* Small clearable filter pill */
const FilterPill = ({ label, onClear }) => (
  <span style={{
    display:"inline-flex", alignItems:"center", gap:6,
    padding:"3px 4px 3px 10px",
    background:"var(--p-50)", color:"var(--p-700)",
    border:"1px solid color-mix(in oklab, var(--p-500) 22%, transparent)",
    borderRadius:999, fontSize:11.5, fontWeight:600,
  }}>
    {label}
    <button onClick={onClear} title="Remove" style={{
      width:16, height:16, borderRadius:999, border:"none",
      background:"color-mix(in oklab, var(--p-500) 18%, transparent)",
      color:"var(--p-700)", cursor:"pointer",
      display:"inline-grid", placeItems:"center"
    }}>
      <Icon name="x" size={9}/>
    </button>
  </span>
);

/* Highlight matches in a string */
const highlight = (str, q) => {
  if (!q || !str) return str;
  const idx = String(str).toLowerCase().indexOf(q);
  if (idx === -1) return str;
  const s = String(str);
  return (
    <>{s.slice(0, idx)}<mark style={{background:"color-mix(in oklab, var(--warn-500) 30%, transparent)", color:"inherit", padding:"0 1px", borderRadius:2}}>{s.slice(idx, idx+q.length)}</mark>{s.slice(idx+q.length)}</>
  );
};

/* Small popover for toggling individual columns on/off. Dismisses on
   click-outside or Escape. The "Required" pill marks columns that are
   essential (ID / Amount / Status) and cannot be hidden. */
const ColumnVisibilityPopover = ({ columns, visible, onToggle, onReset, onClose }) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose && onClose(); };
    const onEsc = (e) => { if (e.key === "Escape") onClose && onClose(); };
    const t = setTimeout(() => {
      document.addEventListener("mousedown", onDoc);
      document.addEventListener("keydown", onEsc);
    }, 0);
    return () => {
      clearTimeout(t);
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onEsc);
    };
  }, [onClose]);
  return (
    <div ref={ref} role="dialog" aria-label="Columns"
      style={{
        position:"absolute", zIndex:60, top:"calc(100% + 6px)", right:0,
        width:260, background:"#fff",
        border:"1px solid var(--border-default)", borderRadius:12,
        boxShadow:"0 24px 48px -12px rgba(15,20,32,.22), 0 0 0 1px rgba(15,20,32,.04)",
        overflow:"hidden",
      }}>
      <div style={{padding:"10px 12px", borderBottom:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", gap:8, background:"linear-gradient(180deg, var(--n-25), #fff)"}}>
        <Icon name="settings" size={12} style={{color:"var(--text-secondary)"}}/>
        <div style={{flex:1}}>
          <div style={{fontSize:12.5, fontWeight:700}}>Columns</div>
          <div style={{fontSize:10.5, color:"var(--text-tertiary)"}}>Show or hide what's on the table</div>
        </div>
        <button onClick={onClose} title="Close"
          style={{width:22, height:22, padding:0, border:"none", borderRadius:5, background:"transparent", color:"var(--text-tertiary)", cursor:"pointer", display:"grid", placeItems:"center"}}>
          <Icon name="x" size={10}/>
        </button>
      </div>
      <div style={{padding:"6px 4px", maxHeight:320, overflow:"auto"}}>
        {columns.map(c => {
          const on = visible.includes(c.key);
          return (
            <label key={c.key}
              style={{
                display:"flex", alignItems:"center", gap:10,
                padding:"7px 10px", borderRadius:6,
                cursor: c.required ? "not-allowed" : "pointer",
                opacity: c.required ? 0.7 : 1,
                background: "transparent",
              }}
              onMouseEnter={e => { if (!c.required) e.currentTarget.style.background = "var(--n-25)"; }}
              onMouseLeave={e => { e.currentTarget.style.background = "transparent"; }}>
              <input type="checkbox" checked={on} disabled={c.required}
                onChange={() => onToggle(c.key)}
                style={{accentColor:"var(--p-500)", margin:0}}/>
              <span style={{flex:1, fontSize:13, fontWeight:on ? 600 : 500, color: on ? "var(--text-primary)" : "var(--text-secondary)"}}>{c.label}</span>
              {c.required && (
                <span style={{fontSize:9.5, fontWeight:700, padding:"2px 6px", borderRadius:999, background:"var(--n-75)", color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>
                  Required
                </span>
              )}
            </label>
          );
        })}
      </div>
      <div style={{padding:"8px 10px", borderTop:"1px solid var(--border-subtle)", background:"var(--n-25)", display:"flex", gap:6, justifyContent:"space-between", alignItems:"center"}}>
        <button onClick={onReset}
          style={{padding:"5px 10px", border:"1px solid var(--border-default)", borderRadius:6, background:"#fff", fontSize:11.5, fontWeight:600, color:"var(--text-secondary)", cursor:"pointer"}}>
          Reset to defaults
        </button>
        <span style={{fontSize:10.5, color:"var(--text-tertiary)", fontWeight:600}}>
          Saved automatically
        </span>
      </div>
    </div>
  );
};

window.Transactions = Transactions;
