// 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 /reports/business · BusinessReportController — see docs/ISYSTEM_REFERENCE.md §Batch 2 "Business report"
/* Screen actions are ReportsController::business (index, ReportsController.php:7873) and
   ReportsController::getBusinessReport (AJAX data, :8207) — routes admin.reports.business.index /
   .data, routes/admin.php:1518-1519. BusinessReportController itself is NOT routed anywhere: it is
   the aggregation engine that populates the pre-aggregated `business_report` MySQL table this
   screen reads (calcBusinessReport() rolls up casino play from MongoDB + sport coupons, run hourly
   via Jobs/CalcBusinessReport and the /crons/calcBusinessReport route), so figures lag up to ~1h
   behind live play.

   Backend-only, no UI surface: getBusinessReport traces every step to the dedicated `reports` log
   channel under a br_-prefixed request_id (business_report.entry → .inputs.resolved → .period.resolved
   → .vendor_group.start/.done → .single.start/.done → .view.render.start/.done → .response.ready,
   with a business_report.exception catch-all; fxRateFor logs business_report.fx.fallback_one).

   Faithful to the real screen: no sortable columns (fixed SQL order: vendor-group blocks first,
   then single providers name-ASC with provider id 69 forced to the top), no pagination (whole
   result in one table), and NO export — the Excel button is commented out in index.blade.php:187-195
   and getBusinessReport ignores any export param, so no HrsExport is rendered here on purpose.
   Server-only params `provider_ids` (defaults to all active providers) and `user_type` (defaults 2)
   are never sent by the real form and get no filter card here either.

   Known real-platform quirks represented/diverged per the Batch-1 policies:
   - Skin select: real view hardcodes skin id 68 as the `selected` option for every operator
     (index.blade.php:79). Mock skin 68 = Tucasino, preselected — see the field comment + SUGGESTION.
   - Period radios: the Month/Year radios test $period == "mese"/"anno" but the controller only ever
     passes "custom_range" (index.blade.php:101,128), so they can never arrive pre-checked. Moot here
     (select-based control, default custom_range like the controller).
   - `bonus_bet`/`bonus_win` are selected, converted and totalled in PHP but never rendered by the
     report partial — not rendered here either. table_settings.blade.php (per-column show/hide) is an
     orphan blade included nowhere — not built.
   - Profit cells: the controller computes bg-success/bg-danger classes (`converted_classes`) that the
     current partial never applies; the evident intent (pos/neg coloring) is applied here via
     hrs-pos/hrs-neg.
   - No auto-load: fillReport() on page load is commented out (index.blade.php:951) — the report stays
     empty until Search. */

const { useState: hrbzUseState, useMemo: hrbzUseMemo } = React;

/* Deterministic PRNG (mulberry32 + FNV-1a hash) — same convention as sibling pages. */
const hrbzNow = new Date();
const hrbzIso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const hrbzClamp0 = (n) => Math.max(0, n);

/* Auth::user()->getSkins() — super admin sees all skins. Mock ids; skin 68 is deliberately
   mapped to the flagship (Tucasino) so the real view's hardcoded default is representable. */
/* ------------------------------------------------------------------ *
 * Live data. What used to be here: six skins with hardcoded currencies, a
 * six-currency FX table (ARS 1490, PYG 8130 …), four provider "groups" and a
 * dozen singles with invented base/hand/extra fee percentages, and a seeded
 * PRNG producing bet, win and profit per provider per period.
 *
 * Bet, win and profit now come from report_provider_daily; the three cost
 * rates come from user_providers, which is where isystem keeps them
 * (users_providers.percentage / hand_fee / extra_fee). Nothing on this screen
 * is computed from a seed any more, and `profit` is GGR as defined in
 * docs/PROFIT_DEFINITION.md — stakes minus payouts, both real money.
 * ------------------------------------------------------------------ */

/* Provider groups exist upstream as vendor_groups: several providers priced
   with ONE of them as the "base" whose percentage sets the cost for the whole
   group. Read, not declared. */
const hrbzGroupRows = (vendorGroups) => (vendorGroups || []).map(g => ({
  id: Number(g.id),
  name: g.name,
  base: g.base_provider_id == null ? null : Number(g.base_provider_id),
  members: (g.members || []).map(m => Number(m.provider_id)),
}));


const HRBZ_PRESETS = [
  { 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" },
];
const HRBZ_YEARS = (() => { const out = []; for (let y = hrbzNow.getFullYear(); y >= 2021; y--) out.push({ value: String(y), label: String(y) }); return out; })();

/* Resolve the selected period exactly like the controller: preset range / calendar month
   (getDateCalendarioMonth, 2021→current) / year / custom dd-mm-yyyy range. */
const hrbzResolvePeriod = (v) => {
  const t = new Date(hrbzNow); t.setHours(0, 0, 0, 0);
  const shift = (d, n) => { const x = new Date(d); x.setDate(x.getDate() + n); return x; };
  const monday = (d) => shift(d, -((d.getDay() + 6) % 7));
  switch (v.period) {
    case "range":
      switch (v.range_val || "1") {
        case "2": { const y = shift(t, -1); return { from: hrbzIso(y), to: hrbzIso(y) }; }
        case "3": return { from: hrbzIso(monday(t)), to: hrbzIso(t) };
        case "4": { const m = shift(monday(t), -7); return { from: hrbzIso(m), to: hrbzIso(shift(m, 6)) }; }
        case "5": return { from: hrbzIso(new Date(t.getFullYear(), t.getMonth(), 1)), to: hrbzIso(t) };
        case "6": return { from: hrbzIso(new Date(t.getFullYear(), t.getMonth() - 1, 1)), to: hrbzIso(new Date(t.getFullYear(), t.getMonth(), 0)) };
        default: return { from: hrbzIso(t), to: hrbzIso(t) }; // 1 = Today
      }
    case "periodo_mese": { const p = String(v.periodo_mese || "").split("|"); return { from: p[0] || "", to: p[1] || "" }; }
    case "periodo_anno": {
      const y = Number(v.periodo_anno) || t.getFullYear();
      return { from: `${y}-01-01`, to: y === t.getFullYear() ? hrbzIso(t) : `${y}-12-31` };
    }
    default: { // custom_range — controller defaults $start = 01/m/Y, $end = today
      const r = v.range || {};
      return { from: r.from || hrbzIso(new Date(t.getFullYear(), t.getMonth(), 1)), to: r.to || hrbzIso(t) };
    }
  }
};

/* Build the report the way getBusinessReport does: per provider/day figures from `business_report`
   (× skins.reports_multiplier — 1 here), skin-admin users_providers terms, players_report bet
   counts for hand-fee providers, then group rollups. Amounts are generated in the skin's currency;
   fx = rate(target,date)/rate(skin,date) is applied ONLY when cumulate=1 AND selected ≠ skin
   currency, and the displayed currency is the selected one when cumulate, else the skin's (:8489). */
/* Builds the report from live rows. Same shape the table already renders, so
   nothing below this changes — only where the numbers come from.

   COST MODEL, reproduced from isystem:
     base   every provider in a GROUP is priced with the BASE provider's
            percentage, not its own; negative totals clamp to 0
     hand   per-bet fee x bet count, charged only where hand_fee > 0
     extra  group-level all-or-nothing: charged only if the group's profit for
            the whole period is positive */
const hrbzClampCost = (n) => Math.max(0, n);

const hrbzBuildLive = ({ rows, rates, groups, skin, from, to, cur }) => {
  const rateOf = (pid) => rates[pid] || { percentage: 0, hand_fee: 0, extra_fee: 0 };

  /* One row per provider, summed over the period. report_provider_daily is
     already per-day, so this is the only aggregation the browser does — over
     rows it has, not over rows it would have to fetch. */
  const byProvider = {};
  (rows || []).forEach(r => {
    const id = Number(r.provider_id);
    const p = byProvider[id] || (byProvider[id] = {
      id, name: r.provider_name, bet: 0, win: 0, profit: 0, betsCount: 0,
    });
    p.bet += Number(r.stake) || 0;
    p.win += Number(r.payout) || 0;
    p.profit += Number(r.ggr) || 0;
    p.betsCount += Number(r.bet_count) || 0;
  });

  const figures = (id) => {
    const p = byProvider[id] || { id, name: `#${id}`, bet: 0, win: 0, profit: 0, betsCount: 0 };
    const rate = rateOf(id);
    return {
      ...p,
      pct: Number(rate.percentage) || 0,
      hand: Number(rate.hand_fee) || 0,
      extra: Number(rate.extra_fee) || 0,
      betsCost: (Number(rate.hand_fee) || 0) * p.betsCount,
      premium: (Number(rate.extra_fee) || 0) > 0 ? p.profit * (Number(rate.extra_fee) || 0) / 100 : 0,
    };
  };

  const grouped = new Set();
  const groupRows = (groups || []).map(g => {
    g.members.forEach(m => grouped.add(m));
    const children = g.members.map(figures);
    const baseRate = rateOf(g.base);
    const basePct = Number(baseRate.percentage) || 0;
    const sum = (k) => children.reduce((s, c) => s + (c[k] || 0), 0);
    const profit = sum("profit");
    /* Every member priced at the BASE provider's percentage — the quirk that
       makes a group a group. */
    const baseCost = hrbzClampCost(children.reduce((s, c) => s + c.profit * basePct / 100, 0));
    const premium = profit > 0 ? hrbzClampCost(sum("premium")) : 0;
    const betsCost = sum("betsCost");
    return { rid: `group_${g.id}`, type: "group", name: g.name, basePct, baseId: g.base,
      bet: sum("bet"), win: sum("win"), profit, baseCost, betsCost, premium,
      totalCost: baseCost + betsCost + premium, children };
  }).filter(g => g.children.length);

  const singles = Object.keys(byProvider).map(Number).filter(id => !grouped.has(id))
    .map(figures).map(p => {
      const baseCost = hrbzClampCost(p.profit * p.pct / 100);
      return { rid: String(p.id), type: "single", name: p.name, basePct: p.pct,
        bet: p.bet, win: p.win, profit: p.profit, baseCost, betsCost: p.betsCost,
        betsCount: p.betsCount, premium: p.premium, totalCost: baseCost + p.betsCost + p.premium };
    }).sort((x, y) => x.name.localeCompare(y.name));

  const all = [...groupRows, ...singles];
  const tsum = (k) => all.reduce((s, r) => s + (r[k] || 0), 0);
  const totals = { bet: tsum("bet"), win: tsum("win"), profit: tsum("profit"),
    baseCost: hrbzClampCost(tsum("baseCost")), betsCost: tsum("betsCost"), premium: tsum("premium") };
  totals.totalCost = hrbzClampCost(totals.baseCost + totals.betsCost + totals.premium);

  return { cur, skin, from, to, rows: all, totals };
};


const BusinessReport = () => {
  window.useLocale && window.useLocale();

  /* Reference reads. The skin list and the currency list were literals; both
     are tables. Currencies come from the same place the Currencies screen
     reads, so the two cannot disagree about which currencies exist. */
  const skinFeed = useHrsFetch(() => window.sb.list("skins", { limit: 500 }), []);
  const curFeed = useHrsFetch(() => window.sb.list("currencies", { limit: 200 }), []);
  const groupFeed = useHrsFetch(() => window.sb.list("vendorGroups", { limit: 200 }), []);
  const meRef = React.useRef(null);
  const [me, setMe] = hrbzUseState(null);
  React.useEffect(() => {
    let alive = true;
    Promise.resolve(window.sb.me()).then(r => { if (alive && r && r.ok) { meRef.current = r.data; setMe(r.data); } });
    return () => { alive = false; };
  }, []);

  const skinOptions = hrbzUseMemo(
    () => (skinFeed.data || []).map(x => ({ value: String(x.id), label: x.name, cur: x.currency })),
    [skinFeed.data]);
  const currencyOptions = hrbzUseMemo(
    () => (curFeed.data || []).map(c => c.code).filter(Boolean),
    [curFeed.data]);

  const fields = (draft) => [
    { key: "skin_id", label: "Skin", type: "select", icon: "flag", placeholder: "- Select -",
      options: skinOptions,
      /* Real view hardcodes skin id 68 as the `selected` option for every operator
         (index.blade.php:79) — represented: 68 = Tucasino, the platform's flagship skin, which is
         the evident intent of the hardcode (land on the main skin instead of "- Select -").
         The select itself renders only for isadmin() || isCustomCare() (index.blade.php:74);
         everyone else gets skin_id forced to their own skin server-side (:8211-8213). */
      // <!-- SUGGESTION: replace the hardcoded `68` in the Blade with the logged-in operator's own skin_id (or the first of getSkins()) so the default follows the operator instead of one magic skin. -->
      /* The real view hardcodes skin 68 as `selected` for every operator.
         Reproduced as intent, not as the magic number: land on the operator's
         OWN skin, which is what the hardcode was reaching for.
         <!-- SUGGESTION: replace the hardcoded 68 in the Blade with the operator's skin_id. --> */
      defaultValue: me ? String(me.skin_id) : "",
      tip: <>Shown only to Super Admin / Customer Care on the real page — every other role has <code>skin_id</code> forced to their own skin by the data endpoint. The real view preselects skin 68 for everyone (hardcoded).</> },
    { key: "period", label: "Period", type: "select", icon: "calendar", defaultValue: "custom_range",
      options: [{ value: "range", label: "Preset range" }, { value: "periodo_mese", label: "Month" },
        { value: "periodo_anno", label: "Year" }, { value: "custom_range", label: "Custom range" }],
      tip: <>A radio group on the real page. Its Month/Year radios compare against <code>"mese"</code>/<code>"anno"</code> while the controller only ever passes <code>custom_range</code>, so they can never arrive pre-checked (index.blade.php:101, 128).</> },
    { key: "range_val", label: "Preset", type: "select", icon: "calendar", options: HRBZ_PRESETS,
      defaultValue: "1", hidden: draft.period !== "range" },
    { key: "periodo_mese", label: "Month", type: "month-period", icon: "calendar", mode: "calendar",
      fromYear: 2021, defaultValue: hrsPeriodOptions("calendar", 2021)[0].value, hidden: draft.period !== "periodo_mese" },
    { key: "periodo_anno", label: "Year", type: "select", icon: "calendar", options: HRBZ_YEARS,
      defaultValue: String(hrbzNow.getFullYear()), hidden: draft.period !== "periodo_anno" },
    { key: "range", label: "Custom range", type: "daterange", icon: "calendar",
      defaultValue: { from: hrbzIso(new Date(hrbzNow.getFullYear(), hrbzNow.getMonth(), 1)), to: hrbzIso(hrbzNow), fromTime: "", toTime: "" },
      hidden: draft.period !== "custom_range" },
    { key: "currency", label: "Currency", type: "select", icon: "credit_card", options: currencyOptions,
      defaultValue: "EUR", // default = logged operator's currency (set via JS on the real page)
      tip: <>Distinct currencies from the per-date EUR-based <code>currencies</code> rates table. Takes effect only while Cumulable is on.</> },
    { key: "cumulate", label: "Cumulable", type: "toggle", defaultValue: true,
      tip: <>Converts every figure to the selected currency with per-date EUR-based rates (exact date → carry-forward → 1.0 fallback); when off, amounts stay in the skin's own currency. Defaults ON and refetches immediately on toggle, like the real page (index.blade.php:945).</> },
  ];

  const defaults = () => { const d = {}; fields({ period: "custom_range" }).forEach(f => { d[f.key] = f.defaultValue; }); return d; };
  const [draft, setDraft] = hrbzUseState(defaults);
  const [applied, setApplied] = hrbzUseState(null); // no auto-load: fillReport() on load is commented out
  /* The report itself: one read of report_provider_daily for the resolved
     period, and one of user_providers for the cost rates. Both server-side —
     the aggregation this screen does is over the rows it already has, never
     over rows it would have to fetch to count. */
  const period = hrbzUseMemo(() => (applied ? hrbzResolvePeriod(applied) : { from: null, to: null }), [applied]);
  const provFeed = useHrsFetch(
    () => (applied && period.from
      ? window.sb.list("reportProviders", { limit: 5000,
          filters: { skin: applied.skin_id, from: period.from, to: period.to } })
      : Promise.resolve({ ok: true, data: [], meta: {} })),
    [applied && applied.skin_id, period.from, period.to]);
  const rateFeed = useHrsFetch(
    () => (me ? window.sb.list("userProviders", { limit: 1000, filters: { user: me.id } })
              : Promise.resolve({ ok: true, data: [], meta: {} })),
    [me && me.id]);

  const data = hrbzUseMemo(() => {
    if (!applied || !period.from) return null;
    const skin = skinOptions.find(x => x.value === String(applied.skin_id));
    if (!skin) return null;
    const rates = {};
    (rateFeed.data || []).forEach(r => { rates[Number(r.provider_id)] = r; });
    /* CUMULABLE converts every figure into the selected currency using per-date
       EUR-based rates. That conversion is not wired: currency_latest_rate holds
       the rates but this report needs the rate ON EACH DAY, which is a join
       inside the aggregate rather than a lookup here. Until it is, figures stay
       in the skin's own currency and the label says which — a converted-looking
       number produced with the wrong rate is worse than an unconverted one.
       <!-- SUGGESTION: join currency_rates by day inside report_revenue_daily so a cumulated report converts with the rate that applied on each date, as isystem does (exact date -> carry-forward -> 1.0). --> */
    return hrbzBuildLive({
      rows: provFeed.data || [], rates,
      groups: hrbzGroupRows(groupFeed.data),
      skin: { id: Number(skin.value), name: skin.label, cur: skin.cur },
      from: period.from, to: period.to,
      cur: skin.cur,
    });
  }, [applied, period.from, period.to, provFeed.data, rateFeed.data, groupFeed.data, skinOptions]);

  const reportBusy = provFeed.loading || rateFeed.loading;
  const reportError = provFeed.error || rateFeed.error || skinFeed.error;

  const money = (n) => hrsMoney(n, data ? data.cur : "EUR"); // every money cell is suffixed with currency_to_show

  const onChange = (k, v) => {
    setDraft(d => ({ ...d, [k]: v }));
    // Real page: flipping the Cumulable switch immediately refetches the report.
    if (k === "cumulate" && applied) setApplied(a => ({ ...a, cumulate: v }));
  };
  const onSearch = (v) => {
    if (!v.skin_id) { hrsToast("Select skin", "Mirrors the real endpoint: 400 {\"error\":\"Select skin\"} when no skin survives the non-super-admin override."); return; }
    if (v.period === "periodo_mese" && !v.periodo_mese) { hrsToast("Select the dates", "Mirrors the real endpoint's die(\"seleziona le date!\") on an empty month. (label inferred — the real response is a hardcoded Italian string)"); return; }
    setApplied({ ...v });
  };
  const onReset = () => { setDraft(defaults()); setApplied(null); };

  const columns = [
    // Real table renders an ID column ("group_<id>" / provider_id) hidden via .col_id{display:none} CSS.
    { key: "rid", label: "ID", hidden: true },
    { key: "name", label: "Name", render: r => r.type === "group"
        ? <span className="hrbz-gname"><b>{r.name}</b><span className="hrbz-gcount">{r.children.length} providers</span></span>
        : r.name },
    { key: "bet", label: "Bet", align: "right", render: r => money(r.bet) },
    { key: "win", label: "Win", align: "right", render: r => money(r.win) },
    // converted_classes (bg-success/bg-danger) is computed by the controller but never applied by
    // the current partial — evident intent applied here.
    { key: "profit", label: "Profit", align: "right", render: r => money(r.profit),
      cellClass: r => (r.profit > 0 ? "hrs-pos" : "hrs-neg") },
    { key: "basePct", label: "Base cost %", align: "right", render: r => hrsPct(r.basePct) },
    { key: "baseCost", label: "Base cost", align: "right", render: r => money(r.baseCost) },
    { key: "betsCost", label: "Bets cost", align: "right", render: r => money(r.betsCost) },
    { key: "premium", label: "Premium cost", align: "right", render: r => money(r.premium) },
    { key: "totalCost", label: "Total cost", align: "right", render: r => money(r.totalCost) },
  ];

  const rows = data ? data.rows : [];
  const totals = data ? {
    _label: "Totals", // __('backend.totals')
    bet: money(data.totals.bet), win: money(data.totals.win), profit: money(data.totals.profit),
    basePct: "", baseCost: money(data.totals.baseCost), betsCost: money(data.totals.betsCost),
    premium: money(data.totals.premium), totalCost: money(data.totals.totalCost),
  } : null;

  /* Vendor-group drill rows: the real partial renders "└ name" sub-provider rows toggled from
     d-none by the group row. Sub rows show Bet/Win/Profit, Bets cost with "(N bets)" and Premium
     cost with "(x.xx%)"; their Base cost cell is commented out and Total cost is empty — both are
     priced at group level (base provider's percentage / group all-or-nothing premium). */
  const rowDetail = (r) => r.type === "group" ? (
    <div className="hrbz-drill">
      <table className="hrbz-subtable">
        <thead><tr><th>Provider</th><th className="hrs-al-r">Bet</th><th className="hrs-al-r">Win</th><th className="hrs-al-r">Profit</th><th className="hrs-al-r">Bets cost</th><th className="hrs-al-r">Premium cost</th></tr></thead>
        <tbody>
          {r.children.map(c => (
            <tr key={c.id}>
              <td className="hrbz-subname">└ {c.name}{c.id === r.baseId && <span className="hrbz-basechip" title="base_provider_id — this provider's percentage prices the whole group's Base cost">base provider</span>}</td>
              <td className="hrs-al-r">{money(c.bet)}</td>
              <td className="hrs-al-r">{money(c.win)}</td>
              <td className={"hrs-al-r " + (c.profit > 0 ? "hrs-pos" : "hrs-neg")}>{money(c.profit)}</td>
              <td className="hrs-al-r">{money(c.betsCost)}<span className="hrbz-subnote">({hrsInt(c.betsCount)} bets)</span></td>
              <td className="hrs-al-r">{money(c.premium)}<span className="hrbz-subnote">({hrsPct(c.extra)})</span></td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="hrbz-drillnote">Base cost and Total cost are blank on sub-provider rows in the real report — both are charged at group level, so sub-row columns deliberately don't sum to the group row.</div>
    </div>
  ) : (
    <div className="hrbz-drillnote">Single provider — not part of a Vendors Group; the real report only expands group rows.</div>
  );

  return (
    <HrsShell
      title="Business Report" /* hardcoded English heading on the real page, not a lang key */
      gate={["support_report", "support_report_business"]}
      gateNote={<> — Customer Care only; <code>support_report_business</code> is not assignable anywhere in the BO UI, so CC is effectively locked out unless the perm row is inserted by hand. The sidebar entry renders only for Super Admin / Skin Admin, and the data endpoint forces <code>skin_id</code> to the caller's own skin for every non-super-admin.</>}
      explainer={{ bullets: [
        <>One row per provider for the selected skin and period: <b>Bet</b>, <b>Win</b>, <b>Profit</b> (Bet − Win) and what the platform charges the skin for that play — <b>Base cost</b> (Profit × contract %), <b>Bets cost</b> (per-bet hand fee × bet count, fee'd providers only), <b>Premium cost</b> (Profit × extra %), and their <b>Total cost</b>.</>,
        <>Vendors Groups collapse related providers into one expandable row: the whole group's Base cost is priced with the <i>base provider's</i> percentage, and its Premium is charged only when the group's total profit for the period is positive.</>,
        <>Figures come from the pre-aggregated <code>business_report</code> rollups refreshed hourly, so the report can lag up to ~1h behind live play. Amounts live in the skin's currency; <b>Cumulable</b> converts them to the selected currency with per-date EUR-based rates.</>,
      ] }}
    >
      <HrsFilters
        fields={fields(draft)} values={draft}
        onChange={onChange} onSearch={onSearch} onReset={onReset}
        resultLabel={applied && data ? `${rows.length} rows · ${data.cur}` : "—"}
      />
      {/* Three states, three answers. A report that renders zero rows because
          the read failed reads as "this skin earned nothing", which is a
          number an operator will act on. */}
      {reportError && <HrsError error={reportError} onRetry={() => { provFeed.retry(); rateFeed.retry(); }} />}
      {!reportError && applied && reportBusy && <HrsSkeleton rows={8} cols={8} />}
      {!reportError && !(applied && reportBusy) && (
      <HrsTable
        columns={columns} rows={rows} totals={totals} rowKey="rid" rowDetail={rowDetail}
        /* No sortable columns and no pagination on the real screen: fixed SQL order (groups first,
           then singles name-ASC with provider 69 pinned) and the whole result in one table. */
        empty={applied
          ? "No results found." /* __('backend.no_results_found') */
          : "Choose a skin and a period, then press Search to load the report."}
        renderCard={r => (
          <>
            <div className="hrs-card__top">
              <b>{r.name}{r.type === "group" ? <span className="hrbz-gcount">group</span> : null}</b>
              <span className={r.profit > 0 ? "hrs-pos" : "hrs-neg"}>{money(r.profit)}</span>
            </div>
            <div className="hrs-card__grid">
              <span>Bet</span><b>{money(r.bet)}</b>
              <span>Win</span><b>{money(r.win)}</b>
              <span>Base cost %</span><b>{hrsPct(r.basePct)}</b>
              <span>Base cost</span><b>{money(r.baseCost)}</b>
              <span>Bets cost</span><b>{money(r.betsCost)}</b>
              <span>Premium cost</span><b>{money(r.premium)}</b>
              <span>Total cost</span><b>{money(r.totalCost)}</b>
            </div>
            {r.type === "group" && (
              <details className="hrbz-carddrill">
                <summary>{r.children.length} providers in this group</summary>
                {r.children.map(c => (
                  <div key={c.id} className="hrbz-cardsub">
                    <b>└ {c.name}{c.id === r.baseId ? " · base provider" : ""}</b>
                    <span>Bet {money(c.bet)} · Win {money(c.win)} · Profit {money(c.profit)} · Bets cost {money(c.betsCost)} ({hrsInt(c.betsCount)} bets) · Premium {money(c.premium)} ({hrsPct(c.extra)})</span>
                  </div>
                ))}
              </details>
            )}
          </>
        )}
      />
      )}
      {/* NO export block: the real Excel button is commented out (index.blade.php:187-195), the
          form's hidden export=1 input + target="_blank" are vestigial, and getBusinessReport
          ignores any export param — so no HrsExport here. */}
      {/* index.blade.php also @includes the shared admin.reports.modals.newMessage send-message
          modal, but nothing on this page triggers it — not built. */}
    </HrsShell>
  );
};

/* Loads after src/pages/HostReports.jsx, deliberately replacing its legacy BusinessReport global. */
window.BusinessReport = BusinessReport;
