// NO PROD JSON API (bucket C) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /reports/netwin/ · ReportsController — see docs/ISYSTEM_REFERENCE.md §Batch 2 "NetWin"
/* NetWin — Report ▾ → NetWin (sidebar label hardcoded "NetWin"; the SHOP-level
   sidebar variant is "Net Win", with a space, and carries NO per-entry gate).

   Real surface: ReportsController::netwin() (filter page, period="mese",
   range=1) + ::getNetwinReport() (AJAX partial, admin.reports.netwin.all)
   + ::statsCosts() (costs modal body, legacy unnamed route
   GET /reports/commissions/statsCosts). Data: business_report facts joined
   to users (on shop_id) + skins, scoped to the caller's subtree via
   user_path LIKE, every money figure ×skins.reports_multiplier.

   Fidelity notes (cites are the reference section unless said otherwise):
   - Filters, card columns, drill-down, shops table, costs modal and the
     no-auto-load behavior follow the reference 1:1. The real screen has NO
     export (netwin.js sends is_esporta but the controller never reads it and
     the #xls_export button doesn't exist in the view), NO sorting, NO
     pagination and NO bulk actions — none are added here.
   - THREE profit formulas coexist and are represented honestly:
       1. Card/row Profit = SUM(business_report.profit × multiplier) — the
          STORED column, not totBet − totWin (sport open/closed-bet
          accounting). Mock data drifts profit from bet−win only when Sport
          is in the selected categories, so Casino-only tabs reconcile.
       2. Costs-modal Profit column = bet + bonus_bet − win (bonus_win
          EXCLUDED — statsCosts.blade.php).
       3. Cost basis (importo_da_pagare) = pct/100 ×
          GREATEST(bet + bonus_bet − win − bonus_win, 0) per provider.
     <!-- SUGGESTION: pick one profit definition (or label each column with
          its formula) — operators currently reconcile three different
          "Profit"s across one screen and its modal. -->
   - statsCosts is FULLY UNGATED: legacy unnamed route, no
     support_report_netwin check, no user_path containment — any admin-panel
     session can query any user_id. Represented as an honesty note in the
     gate Tip and inside the modal; the prototype still only opens it from
     in-subtree rows.
     <!-- SUGGESTION: move statsCosts under the netwin. route group and add
          the same customer-care gate + str_starts_with(user_path) check the
          data endpoint already enforces. -->
   - KNOWN BUG (implemented as evident intent, per build policy): missing
     dates make getNetwinReport die("seleziona le date!") — untranslated
     Italian plain text in a 200 body. The prototype validates and toasts a
     proper English message instead.
     <!-- SUGGESTION: return a localized 422 JSON error instead of die(). -->
   - The real "Business report" link on skin-admin rows is a hardcoded
     /reports/business href that drops every selected filter; the main-card
     variant also hides the Costs value entirely for skin admins even though
     getUserNetWinInfo computed it. Mirrored (navigation carries nothing).
     <!-- SUGGESTION: carry user_id + resolved dates + tab over to the
          Business report as query params. -->
   - Quirks kept: only the Month radio can ever be pre-checked (controller
     passes period="mese" but the view compares against "range"/"anno"/
     "custom_range" for the other three); the range dropdown pre-selects
     Today (range=1); Poker is hardcoded off ($show_poker = false) yet the
     server's ALL mapping still includes cats.CATEGORY_POKER=5; the user
     dropdown is a plain pre-populated select because initSerachUser2 targets
     the nonexistent form id #netwin-report-form (the form is literally
     id="business-report-form", copy-pasted from the Business report).
   - Hidden data hooks in the real rows (user_level_container,
     user_parent_container, per-shop total_comission_container) exist only
     as unwired export/copy hooks — not rendered here.
   - Subnet expander mirrors elenco_utenti_sottostanti(): each "+" refetches
     GET /reports/netwin/getNetwinReport with user_id=<row>&level=<level+1>
     and injects the partial (levels > 0 skip header/main card); here the
     next level renders inline. Expander only when getCountUsers(id) > 0.
   - Shops table has NO totals row on the real screen (last-row/gran-totale
     CSS ships unused). None added.
     <!-- SUGGESTION: the shops table is the one place a totals row is
          genuinely missed — the CSS for it already exists. -->
   - usersLevels() display names are skin-customizable (custom_shop_name …).
     Prototype uses the class-based defaults (Super Admin / Admin / Master /
     Agent / Promoter) with SHOP rendered "Cashier" per the reference's
     "C avatar = Cashier" note — labels inferred.
   Deterministic PRNG data; no fetching, no localStorage. */

const { useState: hrnwUseState, useMemo: hrnwUseMemo } = React;

/* ---------- tiny helpers (hrnw-unique; see CLAUDE.md name policy) ---------- */
const hrnwPad = (n) => String(n).padStart(2, "0");
const hrnwDMY = (d) => `${hrnwPad(d.getDate())}/${hrnwPad(d.getMonth() + 1)}/${d.getFullYear()}`;
const hrnwISO = (d) => `${d.getFullYear()}-${hrnwPad(d.getMonth() + 1)}-${hrnwPad(d.getDate())}`;
/* The seed hash and the deterministic PRNG lived here and are gone with the
   generator. Deleted rather than left: this report prices what an operator is
   billed, and a dormant generator on that path is one line from being live. */
/* Cross-page navigation — same pushState+popstate pattern as HostDashboard's
   hdNavTo. The real skin-admin link is a bare /reports/business href. */
const hrnwNavTo = (routeId) => {
  try {
    const path = window.pathForActive && window.pathForActive(routeId);
    if (path) {
      if (window.location.pathname !== path) window.history.pushState({ active: routeId }, "", path);
      window.dispatchEvent(new PopStateEvent("popstate"));
      return;
    }
  } catch (_e) { /* fall through */ }
  hrsToast("Business report", "No prototype page is registered for this route yet.");
};

/* ---------- roles (UsersController::usersLevels) — labels inferred ---------- */
const HRNW_LEVELS = {
  superadmin: { name: "Super Admin" },
  admin:      { name: "Admin" },     // ADMIN(2) — skin admin; 4th cell = Business report link
  master:     { name: "Master" },
  agent:      { name: "Agent" },
  promoter:   { name: "Promoter" },
  shop:       { name: "Cashier" },   // SHOP(20) — "C" avatar per reference
};

/* ---------- enums (netwin.blade.php selects) ---------- */
const HRNW_TABS = [
  { value: "all", label: "ALL" },            // backend.all_selections
  { value: "sport", label: "Sport" },        // skin-gated show_sport, bypassed for admins
  { value: "casino", label: "Casino" },
  { value: "casinolive", label: "Casino Live" },
  // Poker option never renders: $show_poker = false; hardcoded (view :68).
  { value: "virtual", label: "Virtual" },
];
const HRNW_RANGES = [
  { value: "1", label: "Today" }, { value: "2", label: "Yesterday" },
  { value: "3", label: "This week" }, { value: "4", label: "Previous week" },
  { value: "5", label: "This month" }, { value: "6", label: "Previous month" },
];
/* getDateCalendarioMonth(): plain calendar months from 2021, newest first. */
const HRNW_MONTH_OPTS = hrsPeriodOptions("calendar", 2021);
const HRNW_YEARS = (() => { const y = new Date().getFullYear(), out = []; for (let i = y; i >= 2021; i--) out.push({ value: String(i), label: String(i) }); return out; })();

/* ---------- mock network tree ----------
   business_report facts live on SHOPS (join on shop_id); every node's totals
   aggregate its whole subtree's shops — players never appear as rows.
   getChildUsers excludes SHOP/ADMINISTRATION/CUSTOMER_CARE/AFFILIATE and
   anything ≥ PLAYER; shops render separately via getUserShops.
   shops: [username, monthlyBetMillions, costFlag?] — costFlag "cobanco"
   marks a commission profile with cobanco==1 (shows the Costs link). */
/* THE NETWORK, FROM `networkUsers` RATHER THAN FROM A LITERAL.
   ------------------------------------------------------------------------
   `HRNW_TREE` was a hand-written hierarchy — eleven skin admins, their masters
   and agents, and a `shops` array per node with a weight per shop. Every figure
   on this report was that weight multiplied through `hrnwShopBase`, a
   mulberry32 seeded on the shop's name. It even carried a deliberate special
   case: any shop whose name began "acaray" was given a payout rate above 1 so
   the loss-highlighting branch had something to highlight.

   The real hierarchy is the ltree `path` on `users`. A node's SHOPS are its
   children at SHOP level (20) and its KIDS are the rest, which is the same
   split the screen already renders — cards for sub-users, a table for shops. */
const HRNW_SHOP_LEVEL = 20;

const hrnwLevelKey = (lvl) => ({
  0: "superadmin", 2: "admin", 8: "master", 10: "agent", 15: "promoter", 20: "shop",
}[Number(lvl)] || "agent");

/* One pass over a flat user list, parented by `parent_id`. Users whose parent
   is not in the list (outside the caller's subtree, or filtered out) become
   roots — dropping them would silently shrink the network instead of showing
   what RLS actually returned. */
const hrnwBuildTree = (users) => {
  const byId = new Map();
  (users || []).forEach(u => byId.set(String(u.id), {
    id: u.id, u: u.username, lvl: hrnwLevelKey(u.user_level), level: Number(u.user_level),
    path: String(u.path || ""),
    /* "At cost" upstream means a cobanco / at-cost commission profile. The
       closest real signal is a commission assignment carrying `special_mode`;
       nodes without one read "Not at cost", as they do upstream.
       UNCLEAR-14: whether special_mode is exactly the at-cost flag, or whether
       a named profile should decide it. Nothing is invented either way — a
       node with no assignment is simply not at cost. */
    cost: (u.commission && u.commission.length ? u.commission[0].special_mode : null) || null,
    shops: [], kids: [],
  }));
  const roots = [];
  byId.forEach(n => {
    const src = (users || []).find(x => String(x.id) === String(n.id));
    const parent = src && src.parent_id != null ? byId.get(String(src.parent_id)) : null;
    if (!parent) { roots.push(n); return; }
    (n.level >= HRNW_SHOP_LEVEL ? parent.shops : parent.kids).push(n);
  });
  if (roots.length === 1) return roots[0];
  /* More than one root means the caller can see several disjoint branches.
     A synthetic holder rather than picking one arbitrarily. */
  return { id: null, u: "(network)", lvl: "superadmin", level: 0, path: "",
           cost: null, shops: [], kids: roots };
};

const hrnwFindNode = (u, n) => {
  if (!n) return null;
  if (String(n.id) === String(u) || n.u === u) return n;
  for (const k of (n.kids || []).concat(n.shops || [])) { const hit = hrnwFindNode(u, k); if (hit) return hit; }
  return null;
};

/* WHICH TAB MAPS TO WHICH `vertical`.
   `report_type_class.vertical` is derived from transaction TYPES and has three
   values — sport, casino, exchange. Casino Live and Virtual are a GAME-category
   split upstream, a dimension this ledger does not carry on the entry, so those
   tabs have no source. Same gap as the Daily report; recorded there as
   UNCLEAR-13 and D5. ALL passes no vertical filter, which is every category. */
const HRNW_VERTICAL = { sport: ["sport"], casino: ["casino"] };
const HRNW_NO_SOURCE = {
  casinolive: "Casino Live is split by GAME category upstream; this ledger classifies a bet by transaction TYPE, which does not distinguish it from Casino. No source yet — casino's rows under this heading would be a wrong answer that looks right.",
  virtual:    "Virtual is split by GAME category upstream; this ledger classifies a bet by transaction TYPE, which does not distinguish it from Casino. No source yet.",
};

/* Subtree totals. WAS `hrnwTotals` walking the literal tree and multiplying a
   per-shop weight by a seeded PRNG.

   Now: sum the `report_user_daily` rows whose `user_path` is the node's path or
   below it. The rows already carry one line per (user, day, vertical, funding),
   so a node's subtree total is a prefix match — which is what the ltree path is
   for, and why the totals of a parent and its children cannot disagree here the
   way two independently-generated numbers could.

   PROFIT IS bet − win. isystem's card reads the STORED business_report.profit,
   which is not always bet − win because sport open/closed-bet accounting drifts
   from it — the screen's own explainer says three profit formulas coexist. We
   have no stored-profit column and will not fabricate the drift, so the card
   states the derivation it actually performs. Divergence, deliberate. */
const hrnwTotals = (node, rows) => {
  const base = node && node.path ? String(node.path) : "";
  let bet = 0, win = 0;
  (rows || []).forEach(r => {
    const p = String(r.user_path || "");
    if (base && p !== base && !p.startsWith(base + ".")) return;
    bet += Number(r.stake)  || 0;
    win += Number(r.payout) || 0;
  });
  return { bet, win, profit: bet - win };
};

/* ---------- costs (statsCosts / getProvidersCostSQLNew) ----------
   Per provider: importo_da_pagare = (percentage/100) ×
   GREATEST(bet + bonus_bet − win − bonus_win, 0), percentage from
   users_providers. Modal columns re-add bonus_bet to Bet and bonus_win to Win,
   but the Profit column excludes bonus_win — formula divergence kept on
   purpose (see header).

   WAS `hrnwCostRows`: four to seven providers drawn at random from a list of
   ten invented names, with an invented percentage between 6 and 14. The
   percentage is what an operator is BILLED; inventing it is inventing an
   invoice.

   Now `report_user_provider_daily` for the volumes and `user_providers` for the
   rate. A provider with volume but no configured percentage is returned with
   `pct: null` and no cost — NOT with a default rate, because a plausible
   default here is a number somebody could pay. */
const hrnwCostRows = (node, provRows, rateRows) => {
  const base = node && node.path ? String(node.path) : "";
  const rateFor = new Map();
  (rateRows || []).forEach(r => rateFor.set(String(r.provider_id), r));

  const byProv = new Map();
  (provRows || []).forEach(r => {
    const p = String(r.user_path || "");
    if (base && p !== base && !p.startsWith(base + ".")) return;
    const k = String(r.provider_id);
    const acc = byProv.get(k) || { id: r.provider_id, name: r.provider_name || `Provider ${k}`,
                                   bet: 0, win: 0, bonusBet: 0, bonusWin: 0 };
    const stake = Number(r.stake) || 0, payout = Number(r.payout) || 0;
    if (r.funding === "bonus") { acc.bonusBet += stake; acc.bonusWin += payout; }
    else { acc.bet += stake; acc.win += payout; }
    byProv.set(k, acc);
  });

  return [...byProv.values()].map(a => {
    const cfg = rateFor.get(String(a.id));
    const pct = cfg && cfg.percentage != null ? Number(cfg.percentage) : null;
    return {
      name: a.name,
      pct,
      bet: a.bet + a.bonusBet,                    // "Bet" = bet + bonus_bet
      win: a.win + a.bonusWin,                    // "Win" = win + bonus_win
      profit: a.bet + a.bonusBet - a.win,         // "Profit" excludes bonus_win, as upstream
      costs: pct == null ? null
           : (pct / 100) * Math.max(a.bet + a.bonusBet - a.win - a.bonusWin, 0),
    };
  }).sort((x, y) => (y.costs || 0) - (x.costs || 0));
};
const hrnwSumCosts = (rows) => rows.reduce((a, r) => a + (r.costs || 0), 0);

/* ---------- date resolution (getNetwinReport :1322-1443) ---------- */
/* hrnwResolveDates returns Date objects; PostgREST wants YYYY-MM-DD. Built
   from local getFullYear/getMonth/getDate rather than toISOString, which
   converts to UTC first and silently shifts the boundary day for anyone east
   or west of Greenwich — a report that quietly starts a day late. */
const hrnwIsoDate = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;

const hrnwResolveDates = (a) => {
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const shift = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
  if (a.period === "range") {
    const v = a.range_val || "1";
    if (v === "1") return [today, today];
    if (v === "2") return [shift(today, -1), shift(today, -1)];
    const mon = shift(today, -((today.getDay() + 6) % 7));
    if (v === "3") return [mon, shift(mon, 6)];             // rangeWeek()
    if (v === "4") return [shift(mon, -7), shift(mon, -1)]; // previous Mon–Sun
    if (v === "5") return [new Date(today.getFullYear(), today.getMonth(), 1), new Date(today.getFullYear(), today.getMonth() + 1, 0)];
    return [new Date(today.getFullYear(), today.getMonth() - 1, 1), new Date(today.getFullYear(), today.getMonth(), 0)];
  }
  if (a.period === "periodo_anno") { const y = parseInt(a.anno, 10); return [new Date(y, 0, 1), new Date(y, 11, 31)]; }
  if (a.period === "custom_range") return [new Date(a.custom.from + "T00:00:00"), new Date(a.custom.to + "T00:00:00")];
  const parts = (a.mese || "").split("|");                  // periodo_mese "start|end"
  return [new Date(parts[0] + "T00:00:00"), new Date(parts[1] + "T00:00:00")];
};

/* ---------- small building blocks ---------- */
const HrnwAvatar = ({ lvl, small }) => (
  <span className={`hrnw-av hrnw-av--${lvl}${small ? " hrnw-av--sm" : ""}`} title={HRNW_LEVELS[lvl].name}>
    {HRNW_LEVELS[lvl].name.charAt(0)}
  </span>
);

const HrnwCell = ({ k, v, cls }) => (
  <div className={`hrnw-cell${cls ? " " + cls : ""}`}><small>{k}</small><span>{v}</span></div>
);

/* 4th cell rules (getUserNetWinInfo :1027-1041 + all.blade.php): skin admin
   → hardcoded Business report link (Costs computed but hidden); cobanco/
   at-cost profile → red Costs amount opening the modal; else "Not at cost". */
const HrnwInfo = ({ node, totals, onCosts, bare, costData }) => {
  let body;
  if (node.lvl === "admin") {
    body = <button className="hrnw-biz" onClick={() => hrnwNavTo("report-business")} title="Opens the Business report — the real link drops all selected filters">Business report</button>;
  } else if (node.cost) {
    const sum = hrnwSumCosts(hrnwCostRows(node, costData && costData.prov, costData && costData.rates));
    body = <button className="hrnw-cost" onClick={() => onCosts(node, totals)} title="Costs breakdown by provider">{hrsMoney(sum)}</button>;
  } else {
    body = <span className="hrnw-noc">Not at cost</span>;
  }
  return bare ? body : <div className="hrnw-cell hrnw-cell--info"><small>{" "}</small>{body}</div>;
};

/* Shops table (all.blade.php :183-241) — "C" avatar = Cashier. Real table
   has NO totals row (gran-totale CSS ships unused) — none added. */
const HrnwShops = ({ node, rows: dayRows, costData, onCosts }) => {
  const rows = (node.shops || []).map(sh => ({
    u: sh.u, cost: sh.cost || null, ...hrnwTotals(sh, dayRows),
  }));
  if (!rows.length) return null;
  return (
    <div className="hrnw-shopsblock">
      {/* caption added for scanability — the real partial renders the table with no heading */}
      <div className="hrnw-shopshead">Shops</div>
      <HrsTable
        rowKey="u"
        columns={[
          { key: "u", label: "Username", render: (r) => <span className="hrnw-shopuser"><HrnwAvatar lvl="shop" small /> {r.u}</span> },
          { key: "bet", label: "Bet", align: "right", render: (r) => hrsMoney(r.bet) },
          { key: "win", label: "Win", align: "right", render: (r) => hrsMoney(r.win) },
          { key: "profit", label: "Profit", align: "right", render: (r) => hrsMoney(r.profit), cellClass: (r) => r.profit < 0 ? "hrs-neg" : "hrs-pos" },
          { key: "info", label: "Info", align: "center", render: (r) => <HrnwInfo bare node={{ u: r.u, lvl: "shop", cost: r.cost }} totals={r} onCosts={onCosts} /> },
        ]}
        rows={rows}
        renderCard={(r) => (
          <React.Fragment>
            <div className="hrs-card__top"><b><HrnwAvatar lvl="shop" small /> {r.u}</b><span className={r.profit < 0 ? "hrs-neg" : "hrs-pos"}>{hrsMoney(r.profit)}</span></div>
            <div className="hrs-card__grid">
              <span>Bet</span><b>{hrsMoney(r.bet)}</b>
              <span>Win</span><b>{hrsMoney(r.win)}</b>
              <span>Info</span><b><HrnwInfo bare node={{ u: r.u, lvl: "shop", cost: r.cost }} totals={r} onCosts={onCosts} /></b>
            </div>
          </React.Fragment>
        )}
        empty="No shops on this level."
      />
    </div>
  );
};

/* One "NetWin Subnet" card per non-shop child; "+" mirrors
   elenco_utenti_sottostanti() → refetch with user_id=<row>&level=<level+1>,
   rendered inline here. Expander only when getCountUsers(id) > 0. */
const HrnwRow = ({ node, rows, costData, onCosts }) => {
  const [open, setOpen] = hrnwUseState(false);
  const t = hrnwUseMemo(() => hrnwTotals(node, rows), [node, rows]);
  const hasSub = (node.kids || []).length > 0 || (node.shops || []).length > 0;
  return (
    <React.Fragment>
      <div className="hrnw-row">
        <div className="hrnw-id">
          <HrnwAvatar lvl={node.lvl} />
          <div><small>Username</small><b>{node.u}</b></div>
        </div>
        <HrnwCell k="Total bet" v={hrsMoney(t.bet)} />
        <HrnwCell k="Total win" v={hrsMoney(t.win)} />
        <HrnwCell k="Profit" v={hrsMoney(t.profit)} cls={t.profit < 0 ? "hrs-neg" : "hrs-pos"} />
        <HrnwInfo node={node} totals={t} onCosts={onCosts} costData={costData} />
        {hasSub && (
          <button className="hrnw-expand" title={open ? "Collapse subnet" : "Expand subnet"} onClick={() => setOpen(o => !o)}>
            <Icon name={open ? "chevron_down" : "chevron_right"} size={14} />
          </button>
        )}
      </div>
      {open && (
        <div className="hrnw-sub">
          <HrnwLevel node={node} rows={rows} costData={costData} onCosts={onCosts} />
        </div>
      )}
    </React.Fragment>
  );
};

const HrnwLevel = ({ node, rows, costData, onCosts }) => {
  const kids = node.kids || [];
  const hasShops = (node.shops || []).length > 0;
  return (
    <React.Fragment>
      {kids.map(k => <HrnwRow key={k.u} node={k} rows={rows} costData={costData} onCosts={onCosts} />)}
      {hasShops && <HrnwShops node={node} rows={rows} costData={costData} onCosts={onCosts} />}
      {!kids.length && !hasShops && <div className="hrnw-none">No sub-users under this account.</div>}
    </React.Fragment>
  );
};

/* Costs modal (statsCosts.blade.php into #costsModal). Endpoint honesty: the
   route is ungated (see header). Totals row has no % total; footer shows
   settings.last_br_update as d/m/Y G:i (or "never"). */
const HrnwCostsModal = ({ target, onClose }) => {
  const rows = hrnwUseMemo(() => hrnwCostRows(target.node, target.costData && target.costData.prov, target.costData && target.costData.rates), [target]);
  const tot = rows.reduce((a, r) => ({ bet: a.bet + r.bet, win: a.win + r.win, profit: a.profit + r.profit, costs: a.costs + (r.costs || 0) }), { bet: 0, win: 0, profit: 0, costs: 0 });
  const lu = new Date(); // mock settings.last_br_update — nightly cron
  return (
    <div className="bp-modal-scrim" onClick={onClose}>
      <div className="hrnw-modal" onClick={(e) => e.stopPropagation()}>
        <div className="hrnw-modal__head">
          <div>
            <div className="hrnw-modal__title">Costs — {target.name}</div>
            <div className="hrnw-modal__sub">{target.periodLabel} · {target.tabLabel}</div>
          </div>
          <button className="hrs-x" onClick={onClose} title="Close"><Icon name="x" size={13} /></button>
        </div>
        <div className="hrnw-modal__body">
          <HrsTable
            rowKey="name"
            dense
            columns={[
              { key: "name", label: "Provider" },
              { key: "bet", label: "Bet", align: "right", render: (r) => hrsMoney(r.bet) },
              { key: "win", label: "Win", align: "right", render: (r) => hrsMoney(r.win) },
              { key: "profit", label: "Profit", align: "right", render: (r) => hrsMoney(r.profit), cellClass: (r) => r.profit < 0 ? "hrs-neg" : "hrs-pos" },
              { key: "pct", label: "% cost", align: "right", render: (r) => hrsPct(r.pct), renderTotal: () => "" },
              { key: "costs", label: "Costs", align: "right", render: (r) => <span className="hrnw-costval">{hrsMoney(r.costs)}</span> },
            ]}
            rows={rows}
            totals={{ _label: "Totals", bet: hrsMoney(tot.bet), win: hrsMoney(tot.win), profit: hrsMoney(tot.profit), costs: hrsMoney(tot.costs) }}
          />
          <div className="hrnw-modal__note">
            Profit here = bet + bonus bet − win (bonus win excluded) — the modal's own formula; it will not
            match the card's stored <code>business_report.profit</code>. The cost applies the % to
            GREATEST(bet + bonus bet − win − bonus win, 0) per provider.
          </div>
          <div className="hrnw-modal__note hrnw-modal__note--warn">
            Real endpoint honesty: <code>GET /reports/commissions/statsCosts</code> is a legacy unnamed route with
            no <code>support_report_netwin</code> gate and no subtree containment check — any admin-panel session
            can pass any user_id.
          </div>
        </div>
        <div className="hrnw-modal__foot">Last update: {hrnwDMY(lu)} 6:{hrnwPad(5)}</div>
      </div>
    </div>
  );
};

/* ==================================================================
   NetWinReport — page (top-level name intentionally overrides the legacy
   placeholder in HostReports.jsx; this file loads after it, so it wins).
   ================================================================== */
const NetWinReport = () => {
  window.useLocale && window.useLocale();

  const hrnwDefaults = () => {
    const now = new Date();
    return {
      tab: "all",
      user: "admin",
      // Controller passes period="mese"/range=1; only the Month radio can
      // ever be pre-checked (view compares the others against different
      // literals) — so Month checked + Today pre-selected is the real default.
      period: "periodo_mese",
      range_val: "1",
      mese: HRNW_MONTH_OPTS.length ? HRNW_MONTH_OPTS[0].value : "",
      anno: String(now.getFullYear()),
      // Custom pickers prefilled with first/last day of the current month (d/m/Y on the real screen).
      custom: { from: hrnwISO(new Date(now.getFullYear(), now.getMonth(), 1)), to: hrnwISO(new Date(now.getFullYear(), now.getMonth() + 1, 0)) },
    };
  };

  const [draft, setDraft] = hrnwUseState(hrnwDefaults);
  const [applied, setApplied] = hrnwUseState(null); // null = not searched yet (no auto-load; the on-ready call is commented out in netwin.js)
  const [costsFor, setCostsFor] = hrnwUseState(null);

  /* Declared before anything reads them — in-browser Babel makes a
     use-before-declaration read `undefined` rather than raise (tdzcheck). */
  const usersFeed = useHrsFetch(() => window.sb.list("networkUsers", { limit: 500 }), []);
  const tree = hrnwUseMemo(() => hrnwBuildTree(usersFeed.data || []), [usersFeed.data]);

  /* WAS `hrnwUserOptions`, walked from the literal tree. Level < SHOP(20):
     upstream's getUsers(..., SHOP_LEVEL) returns self plus every subtree user
     below shop, so shops and cashiers never appear in this picker. */
  const hrnwUserOptions = ((usersFeed.data || [])
    .filter(u => Number(u.user_level) < HRNW_SHOP_LEVEL)
    .map(u => ({ value: String(u.id), label: `${u.username} (${(HRNW_LEVELS[hrnwLevelKey(u.user_level)] || {}).name || u.user_level})` })));

  /* The day rows for the whole visible network, once. Every card and every
     shop row is a prefix filter over these — so a parent's total and the sum
     of its children come from the SAME rows and cannot disagree, which two
     independently-generated figures could. */
  const dayFeed = useHrsFetch(() => {
    if (!applied || HRNW_NO_SOURCE[applied.tab]) {
      return Promise.resolve({ ok: true, data: [], meta: {}, source: "live" });
    }
    const [from, to] = hrnwResolveDates(applied);
    const filters = { from: hrnwIsoDate(from), to: hrnwIsoDate(to) };
    const v = HRNW_VERTICAL[applied.tab];
    if (v) filters.verticals = v;           // ALL passes none, which is every category
    return window.sb.list("reportUserDaily", { limit: 5000, filters });
  }, [applied]);
  const dayRows = dayFeed.data || [];

  /* Provider volumes and the configured rates, for the Costs modal. Fetched
     with the report rather than on open, so the modal cannot show a spinner
     over a figure the card already claims to know. */
  const costFeed = useHrsFetch(() => {
    if (!applied || HRNW_NO_SOURCE[applied.tab]) {
      return Promise.resolve({ ok: true, data: { prov: [], rates: [] }, meta: {}, source: "live" });
    }
    const [from, to] = hrnwResolveDates(applied);
    return Promise.all([
      window.sb.list("reportUserProviderDaily", { limit: 5000,
        filters: { from: hrnwIsoDate(from), to: hrnwIsoDate(to) } }),
      window.sb.list("userProviders", { limit: 2000 }),
    ]).then(([prov, rates]) => {
      const bad = [prov, rates].find(r => !r.ok);
      if (bad) return bad;
      return { ok: true, meta: {}, source: "live", data: { prov: prov.data, rates: rates.data } };
    });
  }, [applied]);

  const setMode = (mode) => setDraft(d => ({ ...d, period: mode }));

  /* Radio + control per period mode — one radio group period=range|
     periodo_mese|periodo_anno|custom_range, exactly the real four modes.
     Changing a mode's control also checks its radio (affordance only; the
     real screen requires clicking the radio separately). defaultValue is
     pinned to the live draft value so these cards never emit filter pills —
     the mode radios are the state, not removable pills. */
  const hrnwModeRow = (mode, control) => (
    <div className="hrnw-modrow">
      <input type="radio" className="hrnw-radio" checked={draft.period === mode} onChange={() => setMode(mode)} title="Use this period type" />
      {control}
    </div>
  );

  const fields = [
    { key: "tab", label: "Category", type: "select", icon: "grid", options: HRNW_TABS, defaultValue: "all",
      tip: <>Options are skin-gated (<code>show_sport</code> / <code>show_casino</code> / <code>show_casinolive</code> / <code>show_virtual</code>), bypassed for admins and affiliates. Poker is hardcoded off in the view — yet the server's ALL mapping still includes the poker category (id 5).</> },
    { key: "user", label: "User", type: "select", icon: "user", options: hrnwUserOptions, defaultValue: "admin", width: 220,
      tip: <>Self + all subtree users below SHOP level — shops/cashiers and players never appear here. Disabled for affiliate logins on the real screen. Out-of-subtree user_ids get a JSON 403 from the data endpoint.</> },
    { key: "range_val", label: "Period", type: "custom", defaultValue: draft.range_val,
      render: () => hrnwModeRow("range", (
        <select className="hrs-fctl" value={draft.range_val} onChange={(e) => setDraft(d => ({ ...d, range_val: e.target.value, period: "range" }))}>
          {HRNW_RANGES.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
        </select>
      )) },
    { key: "mese", label: "Month", type: "custom", defaultValue: draft.mese,
      render: () => hrnwModeRow("periodo_mese", (
        <select className="hrs-fctl" value={draft.mese} onChange={(e) => setDraft(d => ({ ...d, mese: e.target.value, period: "periodo_mese" }))}>
          {HRNW_MONTH_OPTS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
        </select>
      )) },
    { key: "anno", label: "Year", type: "custom", defaultValue: draft.anno,
      render: () => hrnwModeRow("periodo_anno", (
        <select className="hrs-fctl" value={draft.anno} onChange={(e) => setDraft(d => ({ ...d, anno: e.target.value, period: "periodo_anno" }))}>
          {HRNW_YEARS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
        </select>
      )) },
    { key: "custom", label: "Custom range", type: "custom", defaultValue: draft.custom,
      render: () => hrnwModeRow("custom_range", (
        <div className="hrs-rangerow">
          <input type="date" className="hrs-fctl hrs-fctl--date" value={draft.custom.from} onChange={(e) => setDraft(d => ({ ...d, custom: { ...d.custom, from: e.target.value }, period: "custom_range" }))} />
          <span className="hrs-rangesep">→</span>
          <input type="date" className="hrs-fctl hrs-fctl--date" value={draft.custom.to} onChange={(e) => setDraft(d => ({ ...d, custom: { ...d.custom, to: e.target.value }, period: "custom_range" }))} />
        </div>
      )) },
  ];

  const doSearch = (v) => {
    const d = v || draft;
    // Real endpoint: missing dates → die("seleziona le date!") — plain-text,
    // Italian, HTTP 200. Evident intent implemented (see header SUGGESTION).
    if (d.period === "custom_range" && (!d.custom.from || !d.custom.to)) {
      hrsToast("Select the dates", "Both start and end dates are required for a custom range.");
      return;
    }
    if (d.period === "periodo_mese" && !d.mese) {
      hrsToast("Select the dates", "Pick a month before searching.");
      return;
    }
    setApplied({ ...d, custom: { ...d.custom } });
  };
  const doReset = () => { setDraft(hrnwDefaults()); setApplied(null); };

  const view = hrnwUseMemo(() => {
    if (!applied) return null;
    const [from, to] = hrnwResolveDates(applied);
    const root = hrnwFindNode(applied.user, tree) || tree;
    if (!root) return null;
    return { from, to, root, totals: hrnwTotals(root, dayRows) };
  }, [applied, tree, dayRows]);

  const periodLabel = view ? `${hrnwDMY(view.from)} → ${hrnwDMY(view.to)}` : "";
  const tabLabel = (HRNW_TABS.find(t => t.value === (applied && applied.tab)) || { label: "ALL" }).label;
  const openCosts = (node, totals) => setCostsFor({ node, name: node.u, totals, costData: costFeed.data, periodLabel, tabLabel });

  const rowCount = view ? (view.root.kids || []).length + (view.root.shops || []).length : 0;

  return (
    <HrsShell
      title="NetWin"
      subtitle="Subtree bet / win / profit per network node, from the nightly business_report aggregates"
      gate={["support_report", "support_report_netwin"]}
      gateNote={<> Both gates bind Customer Care only — every other role passes the <code>!isCustomCare()</code> short-circuit — and are re-checked server-side (403) on the page and data endpoints. The SHOP-level sidebar variant ("Net Win") has no per-entry gate, and the Costs modal endpoint <code>GET /reports/commissions/statsCosts</code> is fully ungated: no netwin permission, no subtree check.</>}
      explainer={{
        bullets: [
          <>One card per network node: <b>Total bet / Total win / Profit</b> aggregated over the node's whole subtree from the <code>business_report</code> facts (players roll up into ancestors; every figure ×<code>skins.reports_multiplier</code>). The expander drills one level down — sub-users as cards, shops (cashiers) in their own table.</>,
          <><b>Three profit formulas coexist</b>, as on the real platform: card Profit is the <i>stored</i> <code>business_report.profit</code> — not necessarily Total bet − Total win (sport open/closed-bet accounting); the Costs modal's Profit column is bet + bonus bet − win (bonus win excluded); the cost amount itself is % × GREATEST(bet + bonus bet − win − bonus win, 0).</>,
          <>4th cell rules: skin admins link to the <b>Business report</b> (the real link drops every selected filter); nodes on a cobanco / at-cost commission profile show a clickable red <b>Costs</b> amount; everyone else reads <b>Not at cost</b>.</>,
          <>No export, no sorting, no pagination on the real screen — the whole level renders at once (one query per row), and nothing loads until Search.</>,
        ],
      }}
    >
      <HrsFilters
        fields={fields}
        values={draft}
        onChange={(k, v) => setDraft(d => ({ ...d, [k]: v }))}
        onSearch={doSearch}
        onReset={doReset}
        resultLabel={applied ? `${rowCount} rows` : "—"}
      />

      {usersFeed.error && <HrsError error={usersFeed.error} onRetry={usersFeed.retry} />}
      {dayFeed.error && <HrsError error={dayFeed.error} onRetry={dayFeed.retry} />}
      {costFeed.error && <HrsError error={costFeed.error} onRetry={costFeed.retry} />}

      {applied && HRNW_NO_SOURCE[applied.tab] && (
        <div className="panel hrnw-empty">
          <Icon name="info" size={22} style={{ opacity: 0.5 }} />
          <div>{HRNW_NO_SOURCE[applied.tab]}</div>
        </div>
      )}

      {applied && !HRNW_NO_SOURCE[applied.tab] && (dayFeed.loading || usersFeed.loading) && (
        <HrsSkeleton rows={6} cols={4} />
      )}

      {!applied && (
        <div className="panel hrnw-empty">
          <Icon name="search" size={22} style={{ opacity: 0.4 }} />
          <div>Choose a period and press <b>Search</b> — the report only loads on demand, exactly like the real screen.</div>
        </div>
      )}

      {view && !dayFeed.loading && !dayFeed.error && !HRNW_NO_SOURCE[applied.tab] && (
        <React.Fragment>
          {/* Header strip + main-user card (level 0 only) — the main card IS the report's totals */}
          <div className="hrnw-headwrap">
            <div className="hrnw-period">NetWin <b>{hrnwDMY(view.from)}</b> To <b>{hrnwDMY(view.to)}</b></div>
            <div className="hrnw-main">
              <div className="hrnw-id">
                <HrnwAvatar lvl={view.root.lvl} />
                <div>
                  <small>Username</small>
                  <b>{view.root.u}</b>
                  {/* user_level_container / user_parent_container are hidden data hooks in the real card */}
                  <span className="hrnw-lvltag">{HRNW_LEVELS[view.root.lvl].name}</span>
                </div>
              </div>
              <HrnwCell k="Total bet" v={hrsMoney(view.totals.bet)} />
              <HrnwCell k="Total win" v={hrsMoney(view.totals.win)} />
              <HrnwCell k="Profit" v={hrsMoney(view.totals.profit)} cls={view.totals.profit < 0 ? "hrs-neg" : "hrs-pos"} />
              <HrnwInfo node={view.root} totals={view.totals} onCosts={openCosts} />
            </div>
          </div>

          <HrsSection title="NetWin Subnet" sub="One card per non-shop child — expand to load the next level, exactly one level at a time.">
            <div className="hrnw-rows">
              <HrnwLevel node={view.root} rows={dayRows} costData={costFeed.data} onCosts={openCosts} />
            </div>
          </HrsSection>
        </React.Fragment>
      )}

      {costsFor && <HrnwCostsModal target={costsFor} onClose={() => setCostsFor(null)} />}
    </HrsShell>
  );
};

window.NetWinReport = NetWinReport; // overrides the legacy HostReports.jsx placeholder (load order)
