// NO PROD JSON API (bucket C) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /reports/affiliates/ · ReportsController::affiliates — see docs/ISYSTEM_REFERENCE.md §Batch 9.3
/* Report ▾ → Affiliates. NEW SCREEN (Batch 9).
   URL-only: no sidebar entry renders for it anywhere.

   Real surface:
     · GET /reports/affiliates/                    ReportsController::affiliates
     · GET /reports/affiliates/getAffiliatesReport ::getAffiliatesReport  -> {html}
     · ANY /reports/affiliates/excel               ::excelAffiliatesExport
     Blades: admin/reports/affiliates.blade.php (page)
             admin/reports/affiliates/affiliates.blade.php (cards)

   NO PERMISSION CHECK AT ALL. Every other report action on ReportsController
   opens with the isCustomCare() + support_report_* guard; `affiliates` has
   none. Any session the route middleware admits can read any affiliate's
   commission numbers. Stated in the gate note, because a screen that looks
   like its neighbours but is not gated like them is worth saying out loud.
   <!-- SUGGESTION: add the same isCustomCare() + support_report guard the other 13 report actions carry. -->

   NOT A TABLE — one card per skin, then one row per provider inside it, then a
   totals row. `getSkinsAssigned($user_id)` decides which skins appear, so an
   affiliate with two skins gets two cards.

   MONTH GRANULARITY ONLY. The filter is getDateCalendarioMonth(): an <optgroup>
   per year, one option per month, value "<start>|<end>", current month
   auto-selected. There is no free date range on this screen.

   HIDE COSTS IS ON BY DEFAULT. The checkbox is `checked` in the blade and
   toggles `.hidden-cost`, so the two commission columns are HIDDEN on load.
   Kept — an operator opening this screen sees seven columns, not nine.

   noNegative() clamps at zero, so a loss-making provider reads 0.00 rather
   than a negative commission. Applied to skin cost, % commissions and own
   commissions, exactly as the blade does.

   Known real-platform behaviour, implemented as evident intent per the build
   policy: the data endpoint guards with die("no user") and
   die("seleziona le date!") — untranslated plain text written into the AJAX
   target with a 200 status. The prototype validates and says so properly.
   <!-- SUGGESTION: replace the die() guards with ajaxError() JSON. -->

   Deliberately NOT added: sorting, pagination, per-provider drill-down, a
   date range, or a chart. The real screen has none of them. */

/* THE AFFILIATES, THEIR BRANDS AND THEIR PROVIDERS WERE ALL INVENTED — four
   partner names, a hand-written map of which brands each covered, and nine
   provider names. `hraRows` then produced a bet, a win, a bonus bet, a bonus
   win, a rake AND BOTH COMMISSION PERCENTAGES from a seeded PRNG. This report
   tells an affiliate what they earned.

   Real sources:
     users (level 1)             the affiliates themselves
     users.affiliate_id          which players each one brought in
     report_user_provider_daily  those players' volumes, per provider
     user_providers.percentage   the two real commission rates

   ATTRIBUTION IS `affiliate_id`, NEVER `affiliate_source`. 007 is emphatic
   about the difference: affiliate_id is a foreign key to users, and
   affiliate_source is a partner TAG ('track360'). Filtering one by the other
   returns nothing, which renders as an affiliate with no players rather than as
   a bug.

   RAKE HAS NO SOURCE. Upstream it is a poker/sport in-house figure; nothing in
   this schema records it, so the column renders "—" rather than 0.00 — a zero
   is a claim that no rake was taken.
   <!-- SUGGESTION: rake has no column anywhere in this schema. If in-house sport or poker rake is to be reported, it needs its own ledger classification (report_type_class) so it can be separated from ordinary payouts. --> */

/* getDateCalendarioMonth() — <optgroup> per year, value "<start>|<end>",
   current month selected. Two years back is what the real list shows. */
const hraMonths = () => {
  const out = [];
  const now = new Date(2026, 7, 8);            // fixed "today" — the prototype is deterministic
  for (let back = 0; back < 24; back++) {
    const d = new Date(now.getFullYear(), now.getMonth() - back, 1);
    const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
    const iso = (x) => `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}-${String(x.getDate()).padStart(2, "0")}`;
    const dm = (x) => `${String(x.getDate()).padStart(2, "0")}/${String(x.getMonth() + 1).padStart(2, "0")}`;
    out.push({
      value: `${iso(d)}|${iso(end)}`,
      year: d.getFullYear(),
      label: `${d.toLocaleString("en", { month: "long" })} (${dm(d)} - ${dm(end)})`,
    });
  }
  return out;
};
const HRA_MONTHS = hraMonths();

const hraNoNeg = (v) => (v < 0 ? 0 : v);
/* NULL RENDERS AS "—". Every money helper on this page went through here, and
   `Number(null) || 0` turned "nobody set a rate" and "no rake is recorded" into
   0.00 — a figure an affiliate would read as "you earned nothing" rather than
   "this build cannot tell you". */
const hraMoney = (v) => (v == null ? "—" : (Math.round((Number(v) || 0) * 100) / 100)
  .toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }));

/* One provider row per (skin, provider), from the affiliate's own players'
   volumes. `getAdminPerc` / `getAffiliatoPerc` upstream are per-(user,provider)
   lookups that return 0 when the row is absent — here they are the real
   `user_providers.percentage` rows for the SKIN ADMIN and for the AFFILIATE,
   and a MISSING row stays null rather than becoming 0. The two are different
   facts: 0% is a rate somebody set, absent is nobody having set one, and they
   produce the same commission figure by different meanings. */
const hraBuildRows = (volRows, adminPct, affPct) => {
  const by = {};
  (volRows || []).forEach(r => {
    const id = String(r.provider_id);
    const a = by[id] || (by[id] = {
      id, name: r.provider_name || ("Provider " + id),
      bet: 0, win: 0, bonus_bet: 0, bonus_win: 0,
    });
    const stake = Number(r.stake) || 0;
    const payout = Number(r.payout) || 0;
    if (r.funding === "bonus") { a.bonus_bet += stake; a.bonus_win += payout; }
    else { a.bet += stake; a.win += payout; }
  });
  return Object.keys(by).map(k => {
    const a = by[k];
    const profit = a.bet - a.win;
    const ap = adminPct[a.id];
    const fp = affPct[a.id];
    return {
      name: a.name,
      bet: a.bet, win: a.win, profit,
      bonus_bet: a.bonus_bet, bonus_win: a.bonus_win,
      /* NO SOURCE. Upstream this is in-house sport/poker rake; nothing in this
         schema records it, so it stays null and renders "—". */
      rake: null,
      skin_cost: ap == null ? null : hraNoNeg(ap / 100 * profit),
      perc_commissions: (ap == null || fp == null) ? null : hraNoNeg(ap - fp),
      own_commissions: fp == null ? null : hraNoNeg(fp / 100 * profit),
    };
  }).sort((x, y) => x.name.localeCompare(y.name));
};

const HostReportAffiliates = () => {
  const [filters, setFilters] = React.useState({ user_id: "", periodo_mese: HRA_MONTHS[0].value });
  const [applied, setApplied] = React.useState(null);
  const [hideCosts, setHideCosts] = React.useState(true);   // blade ships it CHECKED
  const [error, setError] = React.useState("");

  const search = (vals) => {
    const v = vals || filters;
    // The real endpoint's two die() guards, as real validation.
    if (!v.user_id) { setError("Select an affiliate. The real endpoint answers with the bare text “no user” and a 200 status."); setApplied(null); return; }
    if (!v.periodo_mese) { setError("Select a month. The real endpoint answers with the bare text “seleziona le date!” and a 200 status."); setApplied(null); return; }
    setError("");
    setApplied({ ...v });
  };

  /* Affiliates are level 1. `networkUsers` already excludes players and is
     RLS-scoped, so this is the set the caller may report on. */
  const affFeed = useHrsFetch(() => window.sb.list("networkUsers", { limit: 2000, filters: { level: 1 } }), []);
  const affiliates = React.useMemo(
    () => (affFeed.data || []).map(u => ({ id: Number(u.id), username: u.username, label: u.username })),
    [affFeed.data]);
  const aff = applied && affiliates.find(a => String(a.id) === String(applied.user_id));
  const period = applied ? applied.periodo_mese : "";
  const [pStart, pEnd] = period ? period.split("|") : ["", ""];

  /* The affiliate's PLAYERS — internal attribution, users.affiliate_id. */
  const playerFeed = useHrsFetch(
    () => (aff
      ? window.sb.list("players", { limit: 5000, filters: { affiliate: aff.id } })
      : Promise.resolve({ ok: true, data: [] })),
    [aff && aff.id]);
  const volFeed = useHrsFetch(
    () => (applied
      ? window.sb.list("reportUserProviderDaily", { limit: 5000, filters: { from: pStart, to: pEnd } })
      : Promise.resolve({ ok: true, data: [] })),
    [applied, pStart, pEnd]);
  const rateFeed = useHrsFetch(
    () => (applied ? window.sb.list("userProviders", { limit: 5000 }) : Promise.resolve({ ok: true, data: [] })),
    [applied]);
  const skinFeed = useHrsFetch(() => window.sb.list("skins", { limit: 200 }), []);
  const adminFeed = useHrsFetch(() => window.sb.list("networkUsers", { limit: 2000, filters: { level: 2 } }), []);

  const busy = playerFeed.loading || volFeed.loading || rateFeed.loading || affFeed.loading;
  const err = playerFeed.error || volFeed.error || rateFeed.error || affFeed.error;

  /* WHICH BRANDS — derived from where the affiliate's players actually are, not
     from a hand-written map. An affiliate with no players in a brand gets no
     card for it, which is the honest answer. */
  const skins = React.useMemo(() => {
    if (!aff) return [];
    const ids = {};
    (playerFeed.data || []).forEach(p => { if (p.skin_id != null) ids[Number(p.skin_id)] = true; });
    return (skinFeed.data || [])
      .filter(s => ids[Number(s.id)])
      .map(s => ({ id: Number(s.id), name: s.name }));
  }, [aff, playerFeed.data, skinFeed.data]);
  const periodLabel = applied
    ? (HRA_MONTHS.find(m => m.value === applied.periodo_mese) || {}).label + " " +
      (HRA_MONTHS.find(m => m.value === applied.periodo_mese) || {}).year
    : "";

  const cards = React.useMemo(() => {
    if (!aff) return [];
    const pctFor = (userId) => {
      const m = {};
      (rateFeed.data || []).forEach(r => {
        if (Number(r.user_id) === Number(userId)) m[String(r.provider_id)] = Number(r.percentage) || 0;
      });
      return m;
    };
    const affPct = pctFor(aff.id);
    return skins.map(s => {
      const playerIds = {};
      (playerFeed.data || []).forEach(p => { if (Number(p.skin_id) === s.id) playerIds[Number(p.id)] = true; });
      const vol = (volFeed.data || []).filter(v => playerIds[Number(v.user_id)]);
      /* The SKIN ADMIN's rates are the platform's cost side. Found by brand
         rather than assumed to be one account. */
      const admin = (adminFeed.data || []).find(u => Number(u.skin_id) === s.id);
      const rows = hraBuildRows(vol, admin ? pctFor(admin.id) : {}, affPct);
      const sum = (f) => rows.reduce((a, r) => a + (r[f] == null ? 0 : r[f]), 0);
      const anyNull = (f) => rows.some(r => r[f] == null);
      const tot = {
        bet: sum("bet"), win: sum("win"), profit: sum("profit"),
        bonus_bet: sum("bonus_bet"), bonus_win: sum("bonus_win"),
        /* A total over a column with unknown rows is itself unknown. Summing
           the known ones and printing it as the total understates it silently. */
        rake: null,
        skin_cost: anyNull("skin_cost") ? null : sum("skin_cost"),
        perc_commissions: anyNull("perc_commissions") ? null : sum("perc_commissions"),
        own_commissions: anyNull("own_commissions") ? null : sum("own_commissions"),
      };
      return { skin: s, rows, tot };
    });
  }, [aff, skins, playerFeed.data, volFeed.data, rateFeed.data, adminFeed.data]);

  const COLS = [
    { key: "name", label: "Provider" },
    { key: "bet", label: "Bet", align: "right", render: (r) => hraMoney(r.bet) },
    { key: "win", label: "Win", align: "right", render: (r) => hraMoney(r.win) },
    { key: "profit", label: "Profit", align: "right", render: (r) => hraMoney(r.profit),
      cellClass: (r) => r.profit < 0 ? "hrs-neg" : "hrs-pos" },
    { key: "bonus_bet", label: "Bonus bet", align: "right", render: (r) => hraMoney(r.bonus_bet) },
    { key: "bonus_win", label: "Bonus win", align: "right", render: (r) => hraMoney(r.bonus_win) },
    { key: "rake", label: "Rake", align: "right", render: (r) => hraMoney(r.rake) },
    { key: "skin_cost", label: "Skin cost", align: "right", render: (r) => hraMoney(r.skin_cost) },
    { key: "perc_commissions", label: "% commissions", align: "right", hidden: hideCosts,
      render: (r) => hraMoney(r.perc_commissions) },
    { key: "own_commissions", label: "Own commissions", align: "right", hidden: hideCosts,
      render: (r) => hraMoney(r.own_commissions) },
  ];

  const exportRows = cards.flatMap(c => c.rows.map(r => ({
    skin: c.skin.name, provider: r.name,
    bet: hraMoney(r.bet), win: hraMoney(r.win), profit: hraMoney(r.profit),
    bonus_bet: hraMoney(r.bonus_bet), bonus_win: hraMoney(r.bonus_win), rake: hraMoney(r.rake),
    skin_cost: hraMoney(r.skin_cost),
    perc_commissions: hraMoney(r.perc_commissions),
    own_commissions: hraMoney(r.own_commissions),
  })));

  return (
    <HrsShell
      title="Affiliates report"
      subtitle="Per-skin, per-provider turnover and commission for one affiliate, one month at a time."
      gate={["auth", "admin", "2fa", "g2fa"]}
      gateNote={<> <b>And nothing else.</b> Unlike the other thirteen report actions on <code>ReportsController</code>, this one carries no <code>isCustomCare()</code> / <code>support_report</code> check — any session the route middleware admits can read any affiliate's numbers.</>}
      explainer={{
        title: "What this report shows, in plain English",
        bullets: [
          "One card per skin the affiliate is assigned to, one row per provider inside it.",
          "Month granularity only — there is no free date range on this screen.",
          "“Hide costs” starts ON, so the two commission columns are hidden until you untick it. That is the real default.",
          "Every commission figure is clamped at zero: a loss-making provider reads 0.00, never a negative.",
        ],
      }}
      actions={applied && (
        <HrsExport count={exportRows.length} filename="affiliates-report.csv"
          onCsv={() => hrsCsv(exportRows, [
            { key: "skin", label: "Skin" }, { key: "provider", label: "Provider" },
            { key: "bet", label: "Bet" }, { key: "win", label: "Win" }, { key: "profit", label: "Profit" },
            { key: "bonus_bet", label: "Bonus bet" }, { key: "bonus_win", label: "Bonus win" },
            { key: "rake", label: "Rake" }, { key: "skin_cost", label: "Skin cost" },
            { key: "perc_commissions", label: "% commissions" }, { key: "own_commissions", label: "Own commissions" },
          ], "affiliates-report.csv")}
          note="The real button posts to ANY /reports/affiliates/excel. The export includes the two commission columns whether or not Hide costs is ticked." />
      )}
    >
      <HrsFilters
        fields={[
          { key: "user_id", label: "Affiliate", type: "select", grow: true,
            placeholder: "Select an affiliate",
            options: affiliates.map(a => ({ value: String(a.id), label: a.username })) },
          { key: "periodo_mese", label: "Month", type: "select",
            options: HRA_MONTHS.map(m => ({ value: m.value, label: `${m.year} — ${m.label}` })) },
        ]}
        values={filters}
        onChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onSearch={search}
        onReset={() => { setFilters({ user_id: "", periodo_mese: HRA_MONTHS[0].value }); setApplied(null); setError(""); }}
        resultLabel={applied ? `${skins.length} brand(s)` : "—"}
      >
        <div className="hra-costchk">
          <label>
            <input type="checkbox" checked={hideCosts} onChange={(e) => setHideCosts(e.target.checked)} />
            <span>Hide costs</span>
          </label>
        </div>
      </HrsFilters>

      {error && (
        <div className="hma-errs" role="alert">
          <div className="hma-errs__h"><Icon name="alert" size={13}/> Cannot run the report</div>
          <div>{error}</div>
        </div>
      )}

      {!applied && !error && (
        <div className="hra-empty">
          <Icon name="search" size={22} style={{ opacity: .4 }} />
          <div>Pick an affiliate and a month, then Search.</div>
          <div className="hma-hint">
            The real page renders an empty <code>#reports-affiliate-ajax</code> div on load and fills it
            from <code>getAffiliatesReport</code>. Nothing is shown until you search.
          </div>
        </div>
      )}

      {applied && busy && <HrsSkeleton rows={6} cols={9} />}
      {applied && !busy && err && <HrsError error={err} onRetry={() => { playerFeed.retry(); volFeed.retry(); rateFeed.retry(); }} />}
      {applied && !busy && !err && aff && cards.length === 0 && (
        <HrsEmpty>
          No brand has a player attributed to {aff.username} — attribution is
          <code> users.affiliate_id</code>, the internal referring account, not the partner tag
          in <code>affiliate_source</code>.
        </HrsEmpty>
      )}
      {applied && !busy && !err && aff && cards.map(({ skin, rows, tot }) => (
        <HrsSection key={skin.id} title={skin.name} sub={`${aff.label} · ${periodLabel}`}>
          <HrsTable
            rowKey="name"
            columns={COLS}
            rows={rows}
            totals={{
              name: "Total",
              bet: hraMoney(tot.bet), win: hraMoney(tot.win), profit: hraMoney(tot.profit),
              bonus_bet: hraMoney(tot.bonus_bet), bonus_win: hraMoney(tot.bonus_win),
              rake: hraMoney(tot.rake), skin_cost: hraMoney(tot.skin_cost),
              perc_commissions: hraMoney(tot.perc_commissions),
              own_commissions: hraMoney(tot.own_commissions),
            }}
          />
        </HrsSection>
      ))}

      {applied && aff && (
        <div className="hma-hint hra-foot">
          <b>Skin cost</b> = <code>getAdminPerc(admin, provider) / 100 &times; profit</code>.{" "}
          <b>% commissions</b> = <code>admin% &minus; getAffiliatoPerc(affiliate, provider, skin)</code>.{" "}
          Both lookups return <code>0</code> when the affiliate has no row in
          <code> user_providers</code> / <code>affiliate_providers</code>, and every result passes through
          <code> noNegative()</code>.
        </div>
      )}
    </HrsShell>
  );
};

window.HostReportAffiliates = HostReportAffiliates;
