// NO PROD JSON API (bucket C) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /reports/costs/ · ReportsController (commissions endpoint, costreport=1) — see docs/ISYSTEM_REFERENCE.md §Batch 2 "Provider costs"
/* Provider Costs — Report ▾ rebuild on the shared Hrs* report shell (src/report-shell.jsx).
   Defines top-level `CostReport` (exact name — app.jsx `case "report-costs"`); this file
   loads AFTER the legacy src/pages/HostReports.jsx, so this definition wins.

   Traceability (all cites ReportsController.php / docs/ISYSTEM_REFERENCE.md §"Provider Costs"):
   - Page shell: GET /reports/costs/ → ReportsController::costs (L122-137, thin).
   - DEAD ROUTE, kept as a note only: GET /reports/costs/getCostsReport →
     ReportsController@getCostsReport (routes/admin.php:L1581-1583) references a method that
     does not exist anywhere in ReportsController — hitting it 500s (BadMethodCallException).
     The page never calls it; the real data engine is the *Commissions* endpoint
     GET /reports/commissions/getCommissionsReport called with hardcoded `tab=all` +
     `costreport=1` (costs.js:27-36), rendering the shared commissions/all.blade.php partial
     with all commission fields forced to 0/hidden.
   - Filters 1:1: `User` (self + subtree, 0 < user_level < SHOP_LEVEL(20), default = auth
     user) and `Period` (CALENDAR months from getDateCalendarioMonth(), 2021→current year,
     value "Y-m-d|Y-m-d", default = month containing today) — hence hrsPeriodOptions
     ("calendar", 2021) here, NOT the first-Monday "commission" months. No other filters,
     no sortable columns, no pagination (full one-level tree per request; the `+` expander
     lazily loads level+1 via the same endpoint — HrsTable rowDetail below). No auto-load:
     the initial generaReportcosts() call is commented out (costs.js:53).
   - Row shape 1:1 (all.blade.php cost mode): hierarchy rows = Username · Total bet ·
     Total win · Bonus bet · Jackpot · Profit (red/green) · then EITHER a `Business report`
     link (ADMIN/skin-level rows) OR red `Costs` link (show_costs) OR literal `Not at cost`
     (backend.not_at_cost). Shops table = Username · Bet · Win · Bonus bet · Jackpot ·
     Profit · same info cell. ⓘ per row → infoCommissionProfile modal; Costs link →
     statsCosts modal (Provider / Bet / Win / Profit / % cost (backend.cost_percentage) /
     Costs + Totals row + "Last update: <last_br_update>").
   - "At cost" semantics: profilo_provvigionale −1 = Master AND whole subnet at cost
     (descendants inherit is_cost_network), −2 = Master only; settled as a per-provider
     percentage of GGR instead of commissions. That role (−1/−2 Master), isadmin() or
     isSkinAdmin() is the whole page gate — surfaced in the header Tip.
   - `Business report` links: the real anchor is a plain `/business` href that DROPS every
     selected filter; same here — hrpcNavTo pushes the app's canonical path for
     "report-business" with no state (the app's pushState + PopStateEvent convention, see
     hdNavTo in HostDashboard.jsx / app.jsx popstate handler).

   Known-bug policy divergences (evident intent implemented, per CLAUDE.md build policies):
   - Cost formula discrepancy: the real page total uses %/100 × GREATEST((bet+bonus_bet) −
     (win+bonus_win), 0) per provider (getUserCostsByCategoryReport L258) while the modal's
     per-provider rows use %/100 × GREATEST(bet − win + bonus_win, 0) (L246), so the modal
     cannot sum to the page total whenever bonus play exists. One formula (the aggregate
     one) is used for both here, so the modal Totals row equals the red page figure.
     <!-- SUGGESTION: align getProvidersCostSQLNew's `providers` branch (L246) with the
          aggregate branch (L258) so the statsCosts modal rows sum to the page total. -->
   - Export: the real XLSX (ANY /reports/costs/excel → excelExportProviderCostsReport,
     L4930) is a client-side DOM scrape (costs.js fnExcelReport) — it only contains rows
     currently EXPANDED on screen, exports formatted numbers as text, and the endpoint has
     no permission check and trusts attacker-supplied cell values. This rebuild exports the
     full filtered subtree (all levels + cashiers) regardless of what is expanded, with
     numeric cells, keeping the real column set (User Type / Username / Parent Username /
     Total Bet / Total Win / Bonus bet / Total profit / Costs; costs_report.xlsx → .csv
     in this prototype).
     <!-- SUGGESTION: server-render the export from the same query as the report (re-run
          getCommissionsReport data for the requested user/period) instead of scraping the
          DOM — fixes collapsed-rows omission, text-formatted numbers, the missing
          permission check, and the client-supplied-cell-values hole in one move. -->
   - Italian leaks rendered as evident-intent English: child-row cost prefix `Costi:` (top
     card says `Costs:`) → "Costs:" everywhere; infoCommissionProfile header `% Costo` →
     "% cost" (the committed backend.cost_percentage label); `NON IMPOSTATO` → "Not set".
     <!-- SUGGESTION: route "Costi:", "% Costo" and "NON IMPOSTATO" through backend.* keys
          (cost, cost_percentage, not_set) instead of hardcoded Italian. -->
   - Real-page dead baggage intentionally NOT reproduced: Highcharts/jsPDF/pdfmake/
     DataTables-buttons CDN scripts loaded-but-unused; floatThead bound to a nonexistent
     `table.table-prov` (sticky header no-op); initSerachUser2 wired to `#costs-report-form`
     while the form id is actually `business-report-form` (copy-paste from Business report).
   - infoCommissionProfile's commission-profile branch (non-at-cost users) is summarized
     here as Category → Profile-name rows; the real modal renders per-category percentage
     ranges via showPercType() (1062-line partial) — simplification noted in the modal.

   Mock data: deterministic PRNG (mulberry32 seeded from username|period|provider) over a
   fixed hierarchy that reuses the legacy prototype's skin-admin names, so every load and
   every Search of the same period renders identical figures. */

const { useState: hrpcUseState, useMemo: hrpcUseMemo } = React;

/* ---------- deterministic PRNG + formatting ---------- */
/* The hash and the PRNG are gone with the volumes they seeded. */
const hrpcR2 = (n) => Math.round(n * 100) / 100;
const hrpcDmy = (iso) => { const p = String(iso || "").split("-"); return p.length === 3 ? `${p[2]}/${p[1]}/${p[0]}` : iso; };

/* usersLevels() display labels (UsersController.php:1188) — 20 renders "Shop" in the
   committed default lang; the excel color map aliases it "Cashier". */
const HRPC_LEVELS = { 0: "Super Admin", 2: "Skin Access", 8: "Master", 10: "Agent", 15: "Promoter", 20: "Shop" };

/* Providers behind the per-provider cost percentages (users_providers /
   profili_cobanco_providers). Cost aggregation runs over casino/casinolive/virtual/sport. */
const HRPC_PROVIDERS = [
  { id: "pragmatic", name: "Pragmatic Play", cat: "Casino" },
  { id: "pgsoft", name: "PG Soft", cat: "Casino" },
  { id: "amigo", name: "Amigo Gaming", cat: "Casino" },
  { id: "evolution", name: "Evolution", cat: "Casino Live" },
  { id: "ezugi", name: "Ezugi", cat: "Casino Live" },
  { id: "kiron", name: "Kiron Virtuals", cat: "Virtual" },
  { id: "sportsbook", name: "Sportsbook", cat: "Sport" },
];

/* `settings.last_br_update` — written by the hourly business_report aggregation cron
   (BusinessReportController.php:852); shown in the statsCosts modal footer, d/m/Y G:i. */
/* WAS A HARDCODED TIMESTAMP — "07/08/2026 6:12", presented as the freshness of
   the aggregation every figure on the page comes from. There is no hourly
   aggregation job here: the views read `ledger_entries` directly, so the
   figures are as fresh as the last transaction and there is no stamp to show.
   Saying that is the honest version of a freshness indicator. */

/* THE FIXED HIERARCHY AND EVERY VOLUME IN IT ARE GONE. `HRPC_TREE_DEF` named
   twenty operators that do not exist, `hrpcOwnVol` invented a bet, a win, a
   bonus bet and a jackpot per (user, period, provider) from a seeded PRNG, and
   `hrpcPctFor` invented the COST PERCENTAGE — the number this report multiplies
   by to say what a network owes the platform.

   Three real sources replace them:

     report_user_provider_daily   volumes per (user, provider, day, funding)
     user_providers.percentage    the actual per-provider cost rate (007)
     users                        the hierarchy, by ltree path

   `funding` is what makes the bonus split real rather than a fraction of the
   total: the view already separates real stakes from bonus stakes, because
   `report_type_class` classifies the type ids that way.

   JACKPOT IS NOT SUBTRACTED TWICE, and this is a divergence worth naming. A
   jackpot payout reaches the ledger through post_transaction as an ordinary
   casino payout (type 6, kind=payout) — so it is ALREADY inside `payout` and
   therefore inside Win. Upstream's card formula is
   `bet − win − bonus_bet − jackpot`, which assumes jackpot sits outside win.
   Subtracting it here would remove the same money twice. The column still
   renders, sourced from `jackpot_wins`, as the informational figure it is.
   <!-- SUGGESTION: if jackpot must be excluded from Win for cost purposes, give jackpot payouts their own transaction type so report_type_class can classify them separately. Today they are indistinguishable from any other casino payout in the ledger. -->

   THE COST FORMULA IS UNCHANGED and is the one the reference states:
   `pct/100 × GREATEST((bet + bonus_bet) − (win + bonus_win), 0)` per provider.
   GREATEST at zero matters: a provider the network lost money on costs nothing,
   it does not generate a credit. */

/* Levels that can head a subtree. A shop has no descendants, so it is a row and
   never a root. */
const HRPC_ROOT_LEVELS = [0, 2, 8, 10, 15];

/* Fold report_user_provider_daily into one row per provider, splitting on
   `funding`. Real and bonus are different money and the cost formula uses both
   halves separately. */
const hrpcFoldProviders = (rows) => {
  const by = {};
  (rows || []).forEach(r => {
    const id = String(r.provider_id);
    const a = by[id] || (by[id] = {
      id, provider: r.provider_name || ("Provider " + id),
      bet: 0, win: 0, bonusBet: 0, bonusWin: 0, jackpot: 0,
    });
    const stake = Number(r.stake) || 0;
    const payout = Number(r.payout) || 0;
    if (r.funding === "bonus") { a.bonusBet += stake; a.bonusWin += payout; }
    else { a.bet += stake; a.win += payout; }
  });
  return Object.keys(by).map(k => {
    const a = by[k];
    a.bet = hrpcR2(a.bet); a.win = hrpcR2(a.win);
    a.bonusBet = hrpcR2(a.bonusBet); a.bonusWin = hrpcR2(a.bonusWin);
    return a;
  });
};

/* ---------- cross-page navigation (Business report links) ----------
   The real anchor is a plain `/business` href that drops every selected filter. Same
   mechanism as the rest of the prototype: push the canonical path for the target page id
   and let app.jsx's popstate handler resolve it (no state carried — faithful). */
const hrpcNavTo = (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", "Route not registered in this build.");
};

/* ---------- node assembly ----------
   One row per operator, with its SUBTREE volumes. The rows come back keyed by
   `user_path`, so a node's subtree is ltree containment over what was fetched —
   the same predicate the database would use, applied to a set already narrowed
   to the chosen root and period.

   `at cost` is read from user_commission_profile_assignments: −1 puts the master
   AND its whole subnet at cost, −2 only the master. That inheritance is why
   `showCosts` is computed by walking down from each −1 node rather than per
   row — the same rule 052 refuses a profile under. */
const hrpcBuildNodes = (users, volRows, rateRows, jackRows) => {
  const rateBy = {};
  (rateRows || []).forEach(r => {
    const u = String(r.user_id);
    (rateBy[u] = rateBy[u] || {})[String(r.provider_id)] = Number(r.percentage) || 0;
  });
  const nodes = (users || []).map(u => ({
    u: u.username,
    id: Number(u.id),
    path: String(u.path || ""),
    level: Number(u.user_level),
    levelName: HRPC_LEVELS[Number(u.user_level)] || "Shop",
    parentU: u.parent ? u.parent.username : "",
    atCost: u.commission && u.commission[0] && u.commission[0].special_mode != null
      ? Number(u.commission[0].special_mode) : 0,
    profile: u.commission && u.commission[0] && u.commission[0].profile
      ? u.commission[0].profile.name : null,
    kids: [], shops: [],
  }));
  const byPath = {};
  nodes.forEach(n => { byPath[n.path] = n; });

  /* −1 propagates down the subtree, −2 does not. Walked here rather than
     recomputed per row so the rule lives in one place. */
  const costNet = {};
  nodes.forEach(n => { if (n.atCost === -1) costNet[n.path] = true; });
  nodes.forEach(n => {
    n.showCosts = n.atCost === -1 || n.atCost === -2
      || Object.keys(costNet).some(p => p !== n.path && n.path.indexOf(p + ".") === 0);
  });

  /* Subtree volumes: every fetched row whose user_path is this node or below. */
  const inSubtree = (rowPath, nodePath) =>
    rowPath === nodePath || String(rowPath).indexOf(nodePath + ".") === 0;
  nodes.forEach(n => {
    const mine = (volRows || []).filter(r => inSubtree(r.user_path, n.path));
    const provs = hrpcFoldProviders(mine);
    const jack = (jackRows || [])
      .filter(j => j.__path && inSubtree(j.__path, n.path))
      .reduce((a, j) => a + (Number(j.win_amount) || 0), 0);
    let bet = 0, win = 0, bonusBet = 0, bonusWin = 0;
    provs.forEach(v => { bet += v.bet; win += v.win; bonusBet += v.bonusBet; bonusWin += v.bonusWin; });
    n._provs = provs;
    n._t = {
      bet: hrpcR2(bet), win: hrpcR2(win),
      bonusBet: hrpcR2(bonusBet), bonusWin: hrpcR2(bonusWin),
      jackpot: hrpcR2(jack),
      /* JACKPOT IS NOT SUBTRACTED. It is already inside `win` — a jackpot payout
         posts as an ordinary casino payout — so removing it here would take the
         same money out twice. See the header note. */
      profit: hrpcR2(bet - win - bonusBet),
    };
    const pct = rateBy[String(n.id)] || {};
    n._costRows = provs.map(v => {
      const b = hrpcR2(v.bet + v.bonusBet);
      const w = hrpcR2(v.win + v.bonusWin);
      if (b <= 0) return null;
      const profit = hrpcR2(b - w);
      const p = pct[v.id];
      return {
        id: v.id, provider: v.provider, bet: b, win: w, profit,
        /* NULL, not 0, when the node has no rate on file for this provider. A
           0% cost is a rate somebody set; no row is nobody having set one, and
           the two produce the same cost figure by different meanings. */
        pct: p == null ? null : p,
        cost: p == null ? null : hrpcR2(Math.max(profit, 0) * p / 100),
      };
    }).filter(Boolean);
    n._costTotal = hrpcR2(n._costRows.reduce((a, r) => a + (r.cost || 0), 0));
    n._costUnrated = n._costRows.filter(r => r.pct == null).length;
  });

  nodes.forEach(n => {
    const parent = nodes.find(x => x.path !== n.path && n.path.indexOf(x.path + ".") === 0
                                && n.path.split(".").length === x.path.split(".").length + 1);
    if (!parent) return;
    (n.level === 20 ? parent.shops : parent.kids).push(n);
  });
  return { nodes, byPath };
};

const HRPC_DEFAULT_PERIOD = hrsPeriodOptions("calendar", 2021)[0].value;
const hrpcFields = (userOptions) => [
  { key: "user", label: "User", type: "select", icon: "user", options: userOptions, defaultValue: "",
    tip: <>Self + entire subtree with <code>0 &lt; user_level &lt; 20</code> (cashiers excluded), scoped to the caller's skins — <code>UsersController::getUsers()</code>. Defaults to the logged-in user.</> },
  { key: "period", label: "Period", type: "month-period", mode: "calendar", fromYear: 2021, icon: "calendar", defaultValue: HRPC_DEFAULT_PERIOD,
    tip: <>Calendar months from <code>getDateCalendarioMonth()</code> (2021 → current year) — unlike the Commissions report's first-Monday "commission months". Value posts as <code>"Y-m-d|Y-m-d"</code>.</> },
];

/* ---------- small building blocks ---------- */
const HrpcUserCell = ({ node }) => (
  <span className="hrpc-user">
    <span className="hrpc-user__badge">{node.levelName.charAt(0)}</span>
    <span><span className="hrpc-user__u">{node.u}</span><br /><span className="hrpc-user__lvl">{node.levelName}</span></span>
  </span>
);

/* Info cell — the three real variants of the cost-mode cell (all.blade.php:76-81):
   skin-admin rows get a Business report link; show_costs rows the red Costs figure
   (real child rows prefix it "Costi:" — evident-intent "Costs:", see header); the rest
   the literal `Not at cost` (backend.not_at_cost). */
const HrpcCostCell = ({ node, period, onCosts }) => node.level === 2
  ? <button className="hrpc-bizlink" onClick={() => hrpcNavTo("report-business")} title="Opens the Business report — like the real link, no filters are carried over.">Business report</button>
  : node.showCosts
    ? <button className="hrpc-costlink" onClick={() => onCosts(node)} title="Open the per-provider costs breakdown (statsCosts)">Costs: {hrsMoney(node._costTotal)}</button>
    : <span className="hrpc-notcost">Not at cost</span>;

const HrpcInfoBtn = ({ node, onProfile }) => (
  <button className="hrpc-info" title="Commission profile (infoCommissionProfile)" onClick={() => onProfile(node)}><Icon name="info" size={14} /></button>
);

/* Drill-down body injected by the rowDetail expander — the real `+` re-hits
   getCommissionsReport with user_id=<child>&level=n&costreport=1 and injects the same
   partial (child cards + the child's cashiers table) into #subusers<id>. */
const HrpcDrill = ({ node, period, onCosts, onProfile }) => (
  <div className="hrpc-drill">
    {node.kids.length > 0 && <>
      <div className="hrpc-drill__sub">Network under {node.u}</div>
      <HrpcNetTable rows={node.kids} period={period} onCosts={onCosts} onProfile={onProfile} />
    </>}
    {node.shops.length > 0 && <>
      <div className="hrpc-drill__sub">Cashiers under {node.u}</div>
      <HrpcShopsTable rows={node.shops} period={period} onCosts={onCosts} onProfile={onProfile} />
    </>}
    {node.kids.length === 0 && node.shops.length === 0 &&
      /* The real page only renders the + expander when getCountUsers(id) > 0; the shared
         HrsTable shows the chevron on every row, so childless rows expand to this note. */
      <div className="hrpc-drill__empty">No users below {node.u}.</div>}
  </div>
);

/* "Network commissions" hierarchy table (the real page's per-child cards, one row per
   non-cashier descendant; recursion via rowDetail = the real one-level-at-a-time drill). */
const HrpcNetTable = ({ rows, period, onCosts, onProfile, empty }) => {
  const data = rows;
  return (
    <HrsTable
      rowKey="u"
      columns={[
        { key: "u", label: "Username", render: r => <HrpcUserCell node={r} /> },
        { key: "bet", label: "Total bet", align: "right", render: r => hrsMoney(r._t.bet) },
        { key: "win", label: "Total win", align: "right", render: r => hrsMoney(r._t.win) },
        { key: "bonusBet", label: "Bonus bet", align: "right", render: r => hrsMoney(r._t.bonusBet) },
        { key: "jackpot", label: "Jackpot", align: "right", render: r => hrsMoney(r._t.jackpot) },
        { key: "profit", label: "Profit", align: "right", render: r => hrsMoney(r._t.profit), cellClass: r => r._t.profit >= 0 ? "hrs-pos" : "hrs-neg" },
        { key: "costs", label: "Costs", align: "center", render: r => <HrpcCostCell node={r} period={period} onCosts={onCosts} /> },
        { key: "info", label: "", align: "center", width: 42, render: r => <HrpcInfoBtn node={r} onProfile={onProfile} /> },
      ]}
      rows={data}
      empty={empty || "No network users below this user."}
      rowDetail={r => <HrpcDrill node={r} period={period} onCosts={onCosts} onProfile={onProfile} />}
      renderCard={r => <>
        <div className="hrs-card__top"><b>{r.u}</b><span className={r._t.profit >= 0 ? "hrs-pos" : "hrs-neg"}>{hrsMoney(r._t.profit)}</span></div>
        <div className="hrs-card__grid">
          <span>Level</span><b>{r.levelName}</b>
          <span>Total bet</span><b>{hrsMoney(r._t.bet)}</b>
          <span>Total win</span><b>{hrsMoney(r._t.win)}</b>
          <span>Bonus bet</span><b>{hrsMoney(r._t.bonusBet)}</b>
          <span>Jackpot</span><b>{hrsMoney(r._t.jackpot)}</b>
          <span>Costs</span><b><HrpcCostCell node={r} period={period} onCosts={onCosts} /></b>
          <span>Profile</span><b><HrpcInfoBtn node={r} onProfile={onProfile} /></b>
        </div>
        {(r.kids.length > 0 || r.shops.length > 0) && (
          <details className="hrpc-carddrill"><summary>Sub-users &amp; cashiers</summary>
            <HrpcDrill node={r} period={period} onCosts={onCosts} onProfile={onProfile} />
          </details>
        )}
      </>}
    />
  );
};

/* Cashiers table (table.responsive-t in the real partial — direct SHOP(20) children of
   the shown user; no totals row on the real screen either). */
const HrpcShopsTable = ({ rows, period, onCosts, onProfile }) => {
  const data = rows;
  return (
    <HrsTable
      rowKey="u"
      dense
      columns={[
        { key: "u", label: "Username", render: r => <HrpcUserCell node={r} /> },
        { key: "bet", label: "Bet", align: "right", render: r => hrsMoney(r._t.bet) },
        { key: "win", label: "Win", align: "right", render: r => hrsMoney(r._t.win) },
        { key: "bonusBet", label: "Bonus bet", align: "right", render: r => hrsMoney(r._t.bonusBet) },
        { key: "jackpot", label: "Jackpot", align: "right", render: r => hrsMoney(r._t.jackpot) },
        { key: "profit", label: "Profit", align: "right", render: r => hrsMoney(r._t.profit), cellClass: r => r._t.profit >= 0 ? "hrs-pos" : "hrs-neg" },
        { key: "costs", label: "", align: "center", render: r => r.showCosts
          ? <button className="hrpc-costlink" onClick={() => onCosts(r)}>Costs: {hrsMoney(r._costTotal)}</button>
          : <span className="hrpc-notcost">Not at cost</span> },
        { key: "info", label: "", align: "center", width: 42, render: r => <HrpcInfoBtn node={r} onProfile={onProfile} /> },
      ]}
      rows={data}
      empty="No cashiers directly under this user."
      renderCard={r => <>
        <div className="hrs-card__top"><b>{r.u}</b><span className={r._t.profit >= 0 ? "hrs-pos" : "hrs-neg"}>{hrsMoney(r._t.profit)}</span></div>
        <div className="hrs-card__grid">
          <span>Bet</span><b>{hrsMoney(r._t.bet)}</b>
          <span>Win</span><b>{hrsMoney(r._t.win)}</b>
          <span>Bonus bet</span><b>{hrsMoney(r._t.bonusBet)}</b>
          <span>Jackpot</span><b>{hrsMoney(r._t.jackpot)}</b>
          <span>Costs</span><b>{r.showCosts ? hrsMoney(r._costTotal) : "Not at cost"}</b>
        </div>
      </>}
    />
  );
};

/* ---------- modals (shared .bp-modal chrome) ---------- */
const HrpcModal = ({ title, onClose, children }) => (
  <div className="bp-modal-scrim" onClick={onClose}>
    <div className="bp-modal hrpc-modal" onClick={e => e.stopPropagation()}>
      <div className="bp-modal__head">
        <div className="bp-modal__title">{title}</div>
        <button className="btn btn--ghost btn--icon" onClick={onClose} title="Close"><Icon name="x" size={15} /></button>
      </div>
      {children}
    </div>
  </div>
);

/* statsCosts → #costsModal "Costs details" (title hardcoded English on the real page).
   Columns 1:1: Provider / Bet / Win / Profit / % cost (backend.cost_percentage) / Costs;
   Totals row (no % total) + "Last update" freshness footer. Bet = bet+bonus_bet,
   Win = win+bonus_win (see formula note in the header). */
const HrpcCostsModal = ({ node, period, onClose }) => {
  const rows = node._costRows;
  const tot = rows.reduce((a, r) => ({ bet: a.bet + r.bet, win: a.win + r.win, profit: a.profit + r.profit, cost: a.cost + r.cost }), { bet: 0, win: 0, profit: 0, cost: 0 });
  return (
    <HrpcModal title="Costs details" onClose={onClose}>
      <div className="hrpc-modal__who">{node.u} · {node.levelName} · {hrpcDmy(period.split("|")[0])} – {hrpcDmy(period.split("|")[1])}</div>
      <HrsTable
        rowKey="id"
        dense
        columns={[
          { key: "provider", 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-pos" : "hrs-neg" },
          { key: "pct", label: "% cost", align: "right", render: r => hrsPct(r.pct) },
          { key: "cost", label: "Costs", align: "right", render: r => hrsMoney(r.cost) },
        ]}
        rows={rows}
        totals={{ _label: "Totals", bet: hrsMoney(hrpcR2(tot.bet)), win: hrsMoney(hrpcR2(tot.win)), profit: hrsMoney(hrpcR2(tot.profit)), pct: "", cost: hrsMoney(hrpcR2(tot.cost)) }}
        empty="No provider volume in this period."
      />
      <div className="hrpc-lastupd">Live from the ledger <Tip size={12}>Upstream every figure here comes from an hourly <code>business_report</code> aggregation and this line shows its <code>last_br_update</code> stamp. This build has no aggregation job — <code>report_user_provider_daily</code> reads <code>ledger_entries</code> directly, so the figures are as fresh as the last transaction and there is no staleness to report.</Tip></div>
    </HrpcModal>
  );
};

/* infoCommissionProfile → #commissionProfileModal, real title ":username :tab profile"
   (tab is hardcoded `all` on this page). At-cost users (is_perc: ADMIN level, profile
   −1/−2, or cost network) get the provider % list from users_providers; everyone else the
   commission-profile view (summarized here — see header note). */
const HrpcProfileModal = ({ node, onClose }) => {
  const isPerc = node.level === 2 || node.atCost === -1 || node.atCost === -2 || node.showCosts;
  return (
    <HrpcModal title={`${node.u} all profile`} onClose={onClose}>
      {isPerc ? (
        <>
          <HrsTable
            rowKey="id"
            dense
            columns={[
              { key: "provider", label: "Provider" },
              /* Real header is hardcoded Italian "% Costo" — evident-intent English. */
              { key: "pct", label: "% cost", align: "right", render: r => hrsPct(r.pct) },
            ]}
            rows={/* EMBED-OK: `r` is a mapped cost row — provider is the provider NAME. */ node._costRows.map(r => ({ id: r.id, provider: r.provider, pct: r.pct }))}
          />
          <div className="hrpc-profile-note">"At cost" user — per-provider cost percentages (<code>users_providers.percentage</code>) instead of a commission profile. The real endpoint accepts any <code>user_id</code> with no permission or hierarchy check.</div>
        </>
      ) : (
        <>
          <HrsTable
            rowKey="id"
            dense
            columns={[
              { key: "cat", label: "Category" },
              { key: "prof", label: "Profile", render: r => r.prof || <span className="hrpc-notcost">Not set</span> },
            ]}
            rows={["Sport", "Casino", "Casino live", "Virtual"].map(c => ({ id: c, cat: c, prof: node.profile }))}
          />
          <div className="hrpc-profile-note">Summarized view — the real modal renders per-category percentage ranges via <code>showPercType()</code>; its empty fallback is the hardcoded Italian "NON IMPOSTATO" (rendered here as "Not set", evident intent).</div>
        </>
      )}
    </HrpcModal>
  );
};

/* ---------- export (real: ANY /reports/costs/excel → costs_report.xlsx; see header) ---------- */
const hrpcExportRows = (node, period) => {
  const out = [];
  const push = (n) => {
    const t = n._t;
    out.push({
      type: n.levelName, u: n.u, parent: n.parentU,
      bet: t.bet.toFixed(2), win: t.win.toFixed(2), bonus: t.bonusBet.toFixed(2), profit: t.profit.toFixed(2),
      costs: (n.level !== 2 && n.showCosts) ? n._costTotal.toFixed(2) : "",
    });
  };
  const walk = (n) => { push(n); n.kids.forEach(walk); n.shops.forEach(push); };
  walk(node);
  return out;
};
const HRPC_EXPORT_HEADERS = [
  { key: "type", label: "User Type" }, { key: "u", label: "Username" }, { key: "parent", label: "Parent Username" },
  { key: "bet", label: "Total Bet" }, { key: "win", label: "Total Win" }, { key: "bonus", label: "Bonus bet" },
  { key: "profit", label: "Total profit" }, { key: "costs", label: "Costs" },
];

/* ==================================================================
   CostReport — the page
   ================================================================== */
const CostReport = () => {
  window.useLocale && window.useLocale();
  const [draft, setDraft] = hrpcUseState({ user: "", period: HRPC_DEFAULT_PERIOD });
  const [applied, setApplied] = hrpcUseState(null); // null = not searched yet (no-auto-load)
  const [costsFor, setCostsFor] = hrpcUseState(null);
  const [profileFor, setProfileFor] = hrpcUseState(null);

  const period = applied ? applied.period : null;
  const [startIso, endIso] = period ? period.split("|") : ["", ""];

  /* THE HIERARCHY, THE VOLUMES, THE RATES AND THE JACKPOTS — four feeds, none
     of them invented. The user list loads immediately (it fills the filter);
     the other three wait for Search, so the page still does not auto-load. */
  const userFeed = useHrsFetch(() => window.sb.list("networkUsers", { limit: 2000 }), []);
  const volFeed = useHrsFetch(
    () => (applied
      ? window.sb.list("reportUserProviderDaily", { limit: 5000, filters: { from: startIso, to: endIso } })
      : Promise.resolve({ ok: true, data: [] })),
    [applied, startIso, endIso]);
  const rateFeed = useHrsFetch(
    () => (applied
      ? window.sb.list("userProviders", { limit: 5000 })
      : Promise.resolve({ ok: true, data: [] })),
    [applied]);
  const jackFeed = useHrsFetch(
    () => (applied
      ? window.sb.list("jackpotWins", { limit: 2000, filters: { from: startIso, to: endIso } })
      : Promise.resolve({ ok: true, data: [] })),
    [applied, startIso, endIso]);

  const busy = volFeed.loading || rateFeed.loading || jackFeed.loading || userFeed.loading;
  const err = volFeed.error || rateFeed.error || jackFeed.error || userFeed.error;

  /* jackpot_wins carries a user_id but not a path, so the path is joined in
     here from the user feed — a subtree sum needs the path, and asking the
     server for it per row would be one request per jackpot. */
  const built = hrpcUseMemo(() => {
    const pathById = {};
    (userFeed.data || []).forEach(u => { pathById[Number(u.id)] = String(u.path || ""); });
    const jacks = (jackFeed.data || []).map(j => Object.assign({}, j, { __path: pathById[Number(j.user_id)] || null }));
    return hrpcBuildNodes(userFeed.data || [], volFeed.data || [], rateFeed.data || [], jacks);
  }, [userFeed.data, volFeed.data, rateFeed.data, jackFeed.data]);

  const userOptions = hrpcUseMemo(
    () => (userFeed.data || [])
      .filter(u => HRPC_ROOT_LEVELS.indexOf(Number(u.user_level)) >= 0)
      .map(u => ({ value: String(u.path || ""), label: `${u.username} (${HRPC_LEVELS[Number(u.user_level)] || "Shop"})` })),
    [userFeed.data]);

  const byPath = built.byPath;
  const top = applied ? (byPath[applied.user] || null) : null;
  const topT = top ? top._t : null;
  const exportRows = top ? hrpcExportRows(top, period) : [];

  return (
    <HrsShell
      title="Provider Costs"
      gate={<>Real-platform access is <b>role-gated, not permission-gated</b>: <code>isadmin()</code> OR <code>isSkinAdmin()</code> OR a Master with <code>profilo_provvigionale</code> −1/−2 ("at cost") — checked identically in the sidebar and <code>ReportsController::costs()</code>. The Report dropdown itself is additionally gated <code>support_report</code> for Customer Care.{" "}</>}
      gateNote={<>Honesty note: the data endpoint actually called (<code>getCommissionsReport?costreport=1</code>) re-checks only the <em>Commissions</em> gates (<code>support_report</code> + <code>support_report_commissions</code>) plus a <code>user_path</code> descendant check — never the −1/−2 role gate — and the <code>statsCosts</code> / <code>infoCommissionProfile</code> modal endpoints accept any <code>user_id</code> with no permission or hierarchy check at all.</>}
      explainer={{
        title: "What this report shows, in plain English",
        bullets: [
          <>A settlement statement for <b>"at cost"</b> networks: users with no commission profile settle by paying the platform a per-provider <b>percentage of GGR</b> ("provider cost"). <code>profilo_provvigionale = −1</code> puts a Master <i>and its whole subnet</i> at cost; <code>−2</code> puts only the Master at cost.</>,
          <>Under the hood it is the <b>Commissions report with commissions stripped out</b> — same endpoint, same partial, toggled solely by the client-supplied <code>costreport=1</code> query param. Commission KPIs are forced to 0 and hidden; the commissions cell becomes a red <b>Costs</b> figure, <i>Not at cost</i>, or a <b>Business report</b> link on skin-admin rows.</>,
          <>Every figure comes from the hourly <code>business_report</code> aggregation (× the skin's <code>reports_multiplier</code>); the Costs modal footer shows the <code>last_br_update</code> freshness stamp. Periods are <b>calendar months</b> (<code>getDateCalendarioMonth()</code>), unlike the Commissions report's first-Monday "commission months".</>,
          <>The report does not auto-load — press <b>Search</b>. Each row expander drills one hierarchy level at a time, exactly like the real page's <b>＋</b>.</>,
        ],
      }}
    >
      <HrsFilters
        fields={hrpcFields(userOptions)}
        values={draft}
        onChange={(k, v) => setDraft(d => ({ ...d, [k]: v }))}
        onSearch={(v) => setApplied({ user: v.user || (userOptions[0] && userOptions[0].value) || "", period: v.period || HRPC_DEFAULT_PERIOD })}
      />

      {applied && busy && <HrsSkeleton rows={8} cols={7} />}
      {applied && !busy && err && <HrsError error={err} onRetry={() => { volFeed.retry(); rateFeed.retry(); jackFeed.retry(); }} />}
      {applied && !busy && !err && !top && (
        <HrsEmpty>That operator is not in view — the hierarchy feed is scoped to your own subtree.</HrsEmpty>
      )}
      {top && !busy && !err && (
        <>
          {/* Real strip: "Commissions from … To …" (#start_container/#end_container) —
              the shared Commissions partial keeps that label even in cost mode. */}
          <div className="hrpc-periodline">
            Commissions from <b>{hrpcDmy(startIso)}</b> To <b>{hrpcDmy(endIso)}</b>{" "}
            <Tip size={12}>Label text comes from the shared Commissions partial — the real page reads "Commissions from … To …" even on the Provider Costs screen.</Tip>
          </div>

          {/* Top summary card for the selected user (whole-subtree totals). */}
          <HrsKpis
            items={[
              { key: "user", label: "User", value: top.u, sub: top.levelName, tone: "brand" },
              { key: "bet", label: "Total bet", value: hrsMoney(topT.bet) },
              { key: "win", label: "Total win", value: hrsMoney(topT.win) },
              { key: "bonus", label: "Bonus bet", value: hrsMoney(topT.bonusBet) },
              { key: "jackpot", label: "Jackpot", value: hrsMoney(topT.jackpot) },
              { key: "profit", label: "Profit", value: hrsMoney(topT.profit), tone: topT.profit >= 0 ? "ok" : "err" },
              { key: "costs", label: "Costs", tone: top.level !== 2 && top.showCosts ? "err" : "neutral",
                value: <HrpcCostCell node={top} period={period} onCosts={setCostsFor} />,
                tip: <>The real top card replaces this cell with a <b>Business report</b> link for skin-admin users and shows <i>Not at cost</i> when the user holds a commission profile.</> },
            ]}
            note="* Top-card figures cover the selected user's entire subtree for the searched month; they refresh only on Search."
          />

          <HrsSection title="Network commissions" sub="One row per non-cashier user directly below the selected user — expand a row to load the next level, as the real page does.">
            <HrpcNetTable rows={top.kids} period={period} onCosts={setCostsFor} onProfile={setProfileFor} />
          </HrsSection>

          {/* Cashiers of the SELECTED user (getUserShops: user_level=20, parent_id=user).
              Section heading is prototype chrome — the real partial renders this table
              directly below the cards with no title. */}
          {top.shops.length > 0 && (
            <HrsSection title="Cashiers" sub={`Direct cashiers of ${top.u}.`}>
              <HrpcShopsTable rows={top.shops} period={period} onCosts={setCostsFor} onProfile={setProfileFor} />
            </HrsSection>
          )}
        </>
      )}

      {!top && (
        <HrsTable
          columns={[
            { key: "u", label: "Username" },
            { key: "bet", label: "Total bet", align: "right" },
            { key: "win", label: "Total win", align: "right" },
            { key: "bonusBet", label: "Bonus bet", align: "right" },
            { key: "jackpot", label: "Jackpot", align: "right" },
            { key: "profit", label: "Profit", align: "right" },
            { key: "costs", label: "Costs", align: "center" },
          ]}
          rows={[]}
          empty="Choose a User and Period, then press Search — the report does not auto-load (the real page's initial load call is commented out, costs.js:53)."
        />
      )}

      <HrsExport
        count={exportRows.length}
        filename="costs_report.csv"
        onCsv={() => hrsCsv(exportRows, HRPC_EXPORT_HEADERS, "costs_report.csv")}
        note={<>Real export: <code>costs_report.xlsx</code> built from a client-side DOM scrape — only currently-expanded rows, numbers as text, no server-side permission check. This rebuild exports the full filtered subtree (evident intent; see file header).</>}
      />

      {costsFor && <HrpcCostsModal node={costsFor} period={period || HRPC_DEFAULT_PERIOD} onClose={() => setCostsFor(null)} />}
      {profileFor && <HrpcProfileModal node={profileFor} onClose={() => setProfileFor(null)} />}
    </HrsShell>
  );
};

window.CostReport = CostReport;
