// Represents: GET /sportprofiles/ · SportProfilesController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Sport profiles"
/* Commissions ▾ → Sport profiles. A sport profile is the rate card that turns a shop/agent's sport
   volume into commission. Commission profiles point at one of these through
   `commissions_profiles.sport_profile_id`, and the Commissions report reads the same rows back
   (SportProfile.perc_first/second_range feed showPercType(); the 30-rung ladder feeds the
   "Percentage steps" tab of the turnover modal — docs/ISYSTEM_REFERENCE.md §Batch 2 "Commissions").

   Screen actions: SportProfilesController::index (L13, aborts 404 unless
   isadmin() || (isSkinAdmin() && checkSkinSett(skin_id,"enable_commissions_management"))),
   ::getSportprofilesTable (L42, the DataTables feed, same gate), ::sportprofileForm (L156,
   GET /sportprofiles/form/?id=), ::saveSportprofile (L189, POST /sportprofiles/saveSportprofile?id=)
   and ::delete (L424, GET /sportprofiles/delete/{id}/, isadmin() only). Routes routes/admin.php
   L1400-1414 (+ the delete at L1634, in the shared delete-routes block) — all unnamed, inside
   Route::name('admin.')->middleware(['auth','admin','2fa','g2fa']). Views:
   admin/sportprofiles/index.blade.php + modals/sportprofile.blade.php (generaModalGestione) +
   forms/sportprofile.blade.php; JS driver public/js/pages/sportprofiles/ajax.js.

   THE THREE TYPOLOGIES (types(), controller L373-384 — the select only ever offers these three):
     1  "Turnover + Monthly Bonus"        (misto a scaletta) — the 30-rung per-event ladder
     3  "Fixed turnover + monthly bonus"  (misto fisso)      — one flat turnover %
     10 "Net profit"                      (utile)            — GGR only, no turnover leg
   Every typology also stores the GGR band ladder, so 1 and 3 pay a turnover leg PLUS a GGR leg,
   combined by the `formula` (ggr_commission_calc_method 1 or 2). Typology 10 has no turnover leg,
   which is why the form hides the formula for it. Dead save-branches exist for tipologia 4/5/15
   (controller L261-286) but the select never offers them, so they are not built here.

   Known real-platform defects, handled per the repo's known-bug policy (CLAUDE.md):
   - EDIT WINDOW IS CLIENT-SIDE ONLY. forms/sportprofile.blade.php L489-499 hides the Save button
     whenever you are editing and the browser clock says (new Date).getDate() > 2 — every role,
     super admin included, no skin-flag exemption. saveSportprofile has NO date guard at all, so the
     same POST sent by hand succeeds on any day of the month. The lock is kept here (it is real
     behaviour operators depend on) but it is presented as a disabled control with the reason
     spelled out, because the blade's own explanation is dead: it calls .show() on #cant_save_label,
     an element that exists nowhere in the view, so on the real screen Save silently vanishes.
   - The separate warning alert (commissions.unable_edit) uses a DIFFERENT date rule —
     getDayOfCurrentPeriod() > 2, days since the first Monday of the month (utils.php L2377-2399) —
     and is suppressed for exempt users (isadmin() || (isSkinAdmin() && skin flag
     disable_change_comm_profile_limits)). So a super admin sees no warning and still loses the Save
     button: the two rules disagree on both the date and the audience. Both are surfaced honestly.
   - Users Assigned is declared sortable but the server switch has no case for it (controller
     L86-117), so clicking it silently reorders by sport_profiles.id ASC. Evident intent
     implemented: it sorts by the count.
   - On create the new id is discarded (`SportProfile::create($datip)->id;`, controller L361), so the
     success payload echoes the empty request id. Evident intent implemented: the new row gets its id.
   - Typology 10's visible `utile_perc` input and typology 3's `mistofisso_utile` row are decoys —
     JS never un-hides them and the saved value comes from `utile_perc3`, the third GGR band's
     percentage. Rendering a control that cannot affect the saved record would be a lie, so this form
     shows the single field that really is saved and says where it lands.
   Each divergence carries a <!-- SUGGESTION: --> line at its implementation site.

   Faithful to the real screen, deliberately NOT added: no KPI strip and no totals (the screen has
   none), no export (none), no bulk actions (n/a), no status column (`stato` is always written as 1,
   controller L289, and stati() is never rendered here), no created/updated columns (addedTime /
   updateTime are unix-stamped server side but never displayed). The row payload also carries a
   hidden `nome_completo` (JS-escaped name, controller L141) that only builds the edit link's title.

   The one visualization here (the ladder rail) is drawn with plain divs rather than a shared ui.jsx
   chart: none of the existing exports (Sparkline / BarChart / HorizontalBar) is an editable,
   click-to-select control, and this agent may not extend ui.jsx. */

const { useState: hspUseState, useMemo: hspUseMemo, useRef: hspUseRef } = React;

/* Deterministic PRNG (FNV-1a + mulberry32) — same convention as the sibling Host pages, so the
   mock list and every generated rate card render identically on every load. */
const hspHash = (s) => { let h = 2166136261 >>> 0; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; };
const hspRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
/* `skins` — leftJoin('skins','skins.id','sport_profiles.skin_id') for the Skin column, and
   SkinsController::getSkinsList() for both the filter and the form select. Ids/names are aligned
   with the sibling Host settings pages so the same operator universe reads across screens. */
const HSP_SKINS = [
  { id: 47, name: "win24hs" },
  { id: 52, name: "apostando365" },
  { id: 55, name: "apuestadepana" },
  { id: 58, name: "PlaySpin" },
  { id: 62, name: "Jokerenvivo" },
  { id: 66, name: "Juegojoker" },
  { id: 68, name: "Tucasino" },
  { id: 70, name: "Jugaygana" },
];
const hspSkinName = (id) => { const s = HSP_SKINS.find(x => x.id === Number(id)); return s ? s.name : `#${id}`; };

/* types() — controller L373-384. `short` is the internal Italian name the rest of the codebase and
   the blade's own div ids use (misto / mistofisso / utile); it is shown as supporting text, never
   as the label. */
const HSP_TYPES = [
  { value: 1, label: "Turnover + Monthly Bonus", short: "misto a scaletta", cls: "t1",
    blurb: "Turnover commission read off a 30-rung ladder — one percentage per number of events on the coupon — plus the GGR bands." },
  { value: 3, label: "Fixed turnover + monthly bonus", short: "misto fisso", cls: "t3",
    blurb: "One flat turnover percentage for every coupon, whatever its event count, plus the GGR bands." },
  { value: 10, label: "Net profit", short: "utile", cls: "t10",
    blurb: "No turnover leg at all — the profile pays on net profit only, through the GGR bands." },
];
const hspType = (t) => HSP_TYPES.find(x => x.value === Number(t)) || null;
const hspTypeLabel = (t) => { const m = hspType(t); return m ? m.label : `#${t}`; };

/* SportProfile::getFormula() — how the turnover leg and the GGR leg are combined
   (ggr_commission_calc_method, SportProfile.php L20-21 / L96-114). Saved for typology 1 and 3 only;
   the blade hides the select for typology 10, which has no turnover leg to net off. */
const HSP_FORMULAS = [
  { value: 1, label: "( Network Profit - TURNOVER Commission ) * Commission %" },
  { value: 2, label: "( Network Profit * Commission % ) - TURNOVER Commission" },
];

/* calc_period. backend.weekly / backend.monthly resolve nowhere in the committed default lang
   (only backend.extras.weekly/monthly exist), so these are written out. */
const HSP_PERIODS = [{ value: "m", label: "Monthly" }, { value: "w", label: "Weekly" }]; /* label inferred */

/* scaglionisport() (controller L386-421) builds 30 rungs, each one a single event count — from == to
   on every rung, which is exactly why the required-check below it is dead. */
const HSP_RUNGS = 30;
const HSP_RUNG_IDX = Array.from({ length: HSP_RUNGS }, (_, i) => i + 1);

const HSP_PAGE_SIZES = [5, 10, 25, 50]; // ajax.js lengthMenu [5,10,25,50], pageLength 50

/* ---------------- numeric helpers ----------------
   Every percentage field on the real form is a plain text input; saveSportprofile normalises it with
   str_replace(',', '.') before validating, so "12,5" and "12.5" are the same value. Same here. */
const hspNum = (v) => { const n = parseFloat(String(v == null ? "" : v).replace(",", ".")); return isNaN(n) ? 0 : n; };
const hspHas = (v) => String(v == null ? "" : v).trim() !== "";
const hspFix = (v) => hspHas(v) ? hspNum(v).toFixed(2) : "";
const hspPct = (v) => `${hspNum(v).toFixed(2)}%`;
/* perc_first_range / perc_second_range are stored as one slash-delimited string, split again with
   explode("/") when the row is re-opened (forms blade L160-186). Shown live under the bands. */
const hspRangeStr = (from, to, pct) => `${hspFix(from) || "0.00"}/${hspFix(to) || "0.00"}/${hspFix(pct) || "0.00"}`;

/* ---------------- the two date rules ----------------
   (a) The Save-button rule the blade actually applies: plain calendar day of month > 2.
   (b) getDayOfCurrentPeriod() (utils.php L2377-2399): days counted from the first Monday of the
       commission month — the same anchor hrsPeriodOptions("commission") uses. Reconstructed here. */
const hspFirstMonday = (y, m) => { const first = new Date(y, m, 1); return new Date(y, m, 1 + ((8 - first.getDay()) % 7)); };
const hspDayOfPeriod = (d) => {
  let anchor = hspFirstMonday(d.getFullYear(), d.getMonth());
  if (d < anchor) anchor = hspFirstMonday(d.getFullYear(), d.getMonth() - 1);
  return Math.floor((new Date(d.getFullYear(), d.getMonth(), d.getDate()) - anchor) / 86400000) + 1;
};
const hspSaveBlocked = (d) => d.getDate() > 2;

/* ---------------- mock `sport_profiles` rows ----------------
   Rate cards are generated from the row's own seed so the ladder shapes are stable and plausible:
   a low opening rung, a climb, then a plateau. `stopEarly` leaves the tail rungs unset — a real and
   common state, because the ladder's required-check never fires (see HspLadder). */
const hspLadderFor = (key, stopEarly) => {
  const rnd = hspRng(hspHash(`ladder|${key}`));
  const start = 1 + Math.round(rnd() * 3);
  const peak = 12 + Math.round(rnd() * 10);
  const plateau = 8 + Math.floor(rnd() * 8);
  const stop = stopEarly ? plateau + 1 + Math.floor(rnd() * 4) : HSP_RUNGS;
  return HSP_RUNG_IDX.map(i => {
    if (i > stop) return "";
    const t = Math.min(1, (i - 1) / (plateau - 1));
    return (start + (peak - start) * t).toFixed(2);
  });
};
const hspBandsFor = (key) => {
  const rnd = hspRng(hspHash(`bands|${key}`));
  const b1 = 1000 * (2 + Math.floor(rnd() * 4));            // 2k … 5k
  const b2 = b1 + 1000 * (10 + Math.floor(rnd() * 15));     // +10k … +24k
  const p1 = 20 + Math.round(rnd() * 10);
  const p2 = p1 + 5 + Math.round(rnd() * 5);
  const p3 = p2 + 5 + Math.round(rnd() * 5);
  return { from1: "0.00", to1: b1.toFixed(2), p1: p1.toFixed(2), to2: b2.toFixed(2), p2: p2.toFixed(2), p3: p3.toFixed(2) };
};

const HSP_SEED = [
  { id: 12, name: "Sport escalera 40% + bonus", tip: 1, skin: 68, stopEarly: false },
  { id: 11, name: "Agentes sport utile 25%", tip: 10, skin: 68 },
  { id: 10, name: "Sport fijo 15% + bonus mensual", tip: 3, skin: 70 },
  { id: 9, name: "Promotores sport escalera", tip: 1, skin: 66, stopEarly: true },
  { id: 8, name: "Sport GGR only (Jokerenvivo)", tip: 10, skin: 62 },
  { id: 7, name: "Default sport win24hs", tip: 1, skin: 47, stopEarly: false },
  { id: 6, name: "Sport fijo apostando365", tip: 3, skin: 52 },
  { id: 5, name: "Sport escalera PlaySpin", tip: 1, skin: 58, stopEarly: true },
  { id: 3, name: "Sport neto apuestadepana", tip: 10, skin: 55 },
];
const hspBuildProfiles = () => HSP_SEED.map(s => {
  const rnd = hspRng(hspHash(`sportprofile|${s.id}|${s.name}`));
  const b = hspBandsFor(`${s.id}|${s.name}`);
  return {
    id: s.id,
    name: s.name,
    tipologia: s.tip,
    skin_id: s.skin,
    /* JS forces "m" and hides the select for typology 1 and 3 (blade L407-408, L423-424); only
       typology 10 rows can legitimately carry "w". */
    calc_period: s.tip === 10 ? (rnd() > 0.6 ? "w" : "m") : "m",
    misto: s.tip === 1 ? hspLadderFor(`${s.id}|${s.name}`, !!s.stopEarly) : HSP_RUNG_IDX.map(() => ""),
    mistofisso_perc: s.tip === 3 ? (3 + Math.round(rnd() * 60) / 10).toFixed(2) : "",
    from1: b.from1, to1: b.to1, p1: b.p1, to2: b.to2, p2: b.p2, p3: b.p3,
    formula: s.tip === 10 ? "" : String(rnd() > 0.5 ? 2 : 1),
    coupon_min: String(rnd() > 0.55 ? 1 + Math.floor(rnd() * 4) : 0),
    played_min: (rnd() > 0.5 ? (50 * (1 + Math.floor(rnd() * 8))) : 0).toFixed(2),
    /* SportProfile::find($id)->users()->count() per row — hasManyThrough(User, CommissionsProfile,
       'sport_profile_id', 'profilo_provvigionale'), i.e. every user whose commission profile points
       at this sport profile. Recomputed per row: the N+1 the controller does at L142. */
    users_cnt: Math.floor(rnd() * 26),
  };
});

/* ================================================================== *
 * Chrome
 * ================================================================== */
const HspTypeChip = ({ t, sub }) => {
  const m = hspType(t);
  if (!m) return <span className="hsp-tchip">#{t}</span>;
  return (
    <span className={`hsp-tchip hsp-tchip--${m.cls}`} title={m.blurb}>
      {m.label}
      {sub && <em>{m.short}</em>}
    </span>
  );
};

/* The real modal is generaModalGestione() → admin/utils/modal.blade.php (modal
   gestioneSportprofileModal, body AJAX-loaded from /sportprofiles/form). Full-screen on mobile per
   the brief §11; the footer is sticky so Save stays reachable in a form this tall. */
const HspModal = ({ title, sub, onClose, children, footer, wide }) => (
  <div className="bp-modal-scrim hsp-scrim" onClick={onClose}>
    <div className={`bp-modal hsp-modal${wide ? " hsp-modal--wide" : ""}`} onClick={e => e.stopPropagation()}>
      <div className="hsp-modal__head">
        <div className="hsp-modal__titles">
          <div className="hsp-modal__title">{title}</div>
          {sub && <div className="hsp-modal__sub">{sub}</div>}
        </div>
        <button className="hsp-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="hsp-modal__body">{children}</div>
      {footer && <div className="hsp-modal__foot">{footer}</div>}
    </div>
  </div>
);

const HspSection = ({ title, note, hint, children, tone }) => (
  <section className={`hsp-sec${tone ? ` hsp-sec--${tone}` : ""}`}>
    <div className="hsp-sec__head">
      <div className="hsp-sec__title">{title}</div>
      {note && <div className="hsp-sec__note">{note}</div>}
    </div>
    {children}
    {hint && <div className="hsp-hint">{hint}</div>}
  </section>
);

const HspField = ({ label, req, err, hint, htmlFor, children, width }) => (
  <div className="hsp-field" style={width ? { maxWidth: width } : undefined}>
    <label className="hsp-label" htmlFor={htmlFor}>{label}{req && <span className="hsp-req">*</span>}</label>
    {children}
    {err && <div className="hsp-fielderr">{err}</div>}
    {hint && <div className="hsp-hint">{hint}</div>}
  </div>
);

/* ================================================================== *
 * The 30-rung ladder (typology 1 only) — misto_1 … misto_30
 * ------------------------------------------------------------------
 * The real form prints 30 stacked text inputs labelled "1 eventi" …
 * "30 eventi" (hardcoded Italian, blade L103-122) with no summary and no
 * way to see the shape of the curve you are editing. Same 30 fields here,
 * presented as: a full-width rail that draws the ladder and doubles as a
 * jump target, a wrapping rung grid for typing, and a derived read-out
 * that names the one thing the wall of inputs hides — where the ladder
 * stops paying. Nothing new is stored; every control writes one of the
 * same misto_N values.
 * ================================================================== */
const HspLadder = ({ value, onChange, disabled }) => {
  const [sel, setSel] = hspUseState(-1); // nothing highlighted until a rung is clicked or focused
  const refs = hspUseRef([]);
  const nums = value.map(hspNum);
  const peak = Math.max(1, ...nums);

  const setRung = (i, v) => { const next = value.slice(); next[i] = v; onChange(next); };
  const blurRung = (i) => { const next = value.slice(); next[i] = hspFix(next[i]); onChange(next); };
  /* Editing affordance over the same 30 fields — no new data, no new field: it writes this rung's
     percentage into every rung below it, which is how a plateau is built by hand today. */
  const carry = (i) => {
    const v = hspFix(value[i]);
    const next = value.slice();
    for (let j = i; j < HSP_RUNGS; j++) next[j] = v;
    onChange(next);
  };
  const focusRung = (i) => { setSel(i); const el = refs.current[i]; if (el) el.focus(); };

  /* Derived reading of the ladder — all of it computed from the 30 values. */
  let lastSet = 0, setCount = 0, dip = 0;
  nums.forEach((n, i) => {
    if (hspHas(value[i]) && n > 0) { lastSet = i + 1; setCount++; }
    if (i > 0 && n > 0 && nums[i - 1] > 0 && n < nums[i - 1] && !dip) dip = i + 1;
  });
  let flatFrom = 0;
  if (lastSet === HSP_RUNGS) { flatFrom = HSP_RUNGS; while (flatFrom > 1 && nums[flatFrom - 2] === nums[HSP_RUNGS - 1]) flatFrom--; }

  return (
    <div className={`hsp-lad${disabled ? " hsp-lad--ro" : ""}`}>
      <div className="hsp-ladsum">
        <div className="hsp-ladsum__stat"><span>Rungs set</span><b>{setCount} / {HSP_RUNGS}</b></div>
        <div className="hsp-ladsum__stat"><span>Peak</span><b>{hspPct(peak)}</b></div>
        <div className="hsp-ladsum__stat">
          <span>{flatFrom ? "Flat from" : "Pays up to"}</span>
          <b>{flatFrom ? `${flatFrom} event${flatFrom === 1 ? "" : "s"}` : (lastSet ? `${lastSet} event${lastSet === 1 ? "" : "s"}` : "—")}</b>
        </div>
        {lastSet === 0 && (
          <div className="hsp-warn">
            <Icon name="alert" size={13} />
            <div>No rung carries a percentage, so this profile pays <b>no turnover commission at all</b> — and nothing
              on save will say so.</div>
          </div>
        )}
        {lastSet > 0 && lastSet < HSP_RUNGS && (
          <div className="hsp-warn">
            <Icon name="alert" size={13} />
            <div>Rungs {lastSet + 1}–{HSP_RUNGS} are unset, so a coupon with that many events earns <b>no turnover
              commission</b>.</div>
          </div>
        )}
        {dip > 0 && lastSet === HSP_RUNGS && (
          <div className="hsp-warn hsp-warn--soft">
            <Icon name="info" size={13} />
            <div>The ladder steps down at rung {dip} — check that is intended.</div>
          </div>
        )}
      </div>

      {/* Rail: one column per rung, height ∝ percentage. Click jumps to that rung's input. */}
      <div className="hsp-rail" role="group" aria-label="Ladder shape">
        {HSP_RUNG_IDX.map((n, i) => {
          const v = nums[i];
          const on = hspHas(value[i]) && v > 0;
          return (
            <button key={n} type="button"
              className={`hsp-rail__rung${on ? "" : " hsp-rail__rung--zero"}${sel === i ? " hsp-rail__rung--sel" : ""}`}
              title={`${n} event${n === 1 ? "" : "s"} — ${on ? hspPct(v) : "not set (pays 0%)"}`}
              onClick={() => focusRung(i)}>
              <span className="hsp-rail__bar" style={{ height: `${on ? Math.max(4, (v / peak) * 100) : 0}%` }} />
            </button>
          );
        })}
      </div>
      <div className="hsp-railax">
        {[1, 5, 10, 15, 20, 25, 30].map(n => <span key={n} style={{ left: `${((n - 0.5) / HSP_RUNGS) * 100}%` }}>{n}</span>)}
      </div>

      {/* Grid: the 30 inputs themselves. */}
      <div className="hsp-rungs">
        {HSP_RUNG_IDX.map((n, i) => {
          const on = hspHas(value[i]) && nums[i] > 0;
          return (
            <div key={n} className={`hsp-rung${sel === i ? " hsp-rung--sel" : ""}${on ? "" : " hsp-rung--zero"}`}>
              {/* "1 eventi" … "30 eventi" in the blade — leftover Italian, written out in English here. */}
              <label className="hsp-rung__lab" htmlFor={`hsp-misto-${n}`}>{n} <em>ev</em></label>
              <div className="hsp-rung__row">
                <input id={`hsp-misto-${n}`} className="hsp-rung__in" inputMode="decimal" disabled={disabled}
                  ref={el => { refs.current[i] = el; }}
                  value={value[i]} placeholder="—"
                  onFocus={() => setSel(i)}
                  onChange={e => setRung(i, e.target.value)}
                  onBlur={() => blurRung(i)} />
                <span className="hsp-rung__pc">%</span>
              </div>
              {!disabled && (
                <button type="button" className="hsp-rung__carry" tabIndex={-1}
                  title={`Copy ${hspHas(value[i]) ? hspPct(nums[i]) : "this value"} into rungs ${n + 1}–${HSP_RUNGS}`}
                  disabled={n === HSP_RUNGS} onClick={() => carry(i)}>
                  <Icon name="arrow_down" size={11} />
                </button>
              )}
            </div>
          );
        })}
      </div>

      <div className="hsp-hint">
        Stored one column per rung, <code>misto_1</code> … <code>misto_30</code> on <code>sport_profiles</code> — the
        legacy shape the Commissions report's "Percentage steps" tab reads back. Every rung is optional: the
        controller's required-check computes <code>$obbligatorio = from == to ? 0 : 1</code> (L233-237) while
        <code> scaglionisport()</code> gives every rung the same from and to, so the flag is always 0 and nothing is
        ever enforced. An unset rung is not "inherit the one below" — it simply pays nothing.
        {/* <!-- SUGGESTION: the misto_N required-check in saveSportprofile is structurally dead (from == to on every rung produced by scaglionisport(), so $obbligatorio is always 0). Either drop it, or replace it with the check operators actually need — warn when the ladder stops before rung 30, because those coupons then earn 0% silently. --> */}
      </div>
    </div>
  );
};

/* ================================================================== *
 * GGR band ladder — validated for every typology (blade L211-327,
 * controller L294-332). Three bands; bands 2 and 3 mirror the previous
 * band's "to" as a readonly "from", and band 3 is open-ended.
 * ================================================================== */
const HspBands = ({ v, on, errs, disabled, thirdNote }) => (
  <div className="hsp-bands">
    <div className="hsp-bands__head">
      <span>Band</span><span>From</span><span>To</span><span>Commission %</span>
    </div>

    <div className={`hsp-band${errs.finoaF1 ? " hsp-band--err" : ""}`}>
      <span className="hsp-band__n">1</span>
      <input className="hsp-in" inputMode="decimal" disabled={disabled} value={v.from1} onChange={e => on("from1", e.target.value)} onBlur={() => on("from1", hspFix(v.from1))} />
      <input className="hsp-in" inputMode="decimal" disabled={disabled} value={v.to1} onChange={e => on("to1", e.target.value)} onBlur={() => on("to1", hspFix(v.to1))} />
      <div className="hsp-band__pc"><input className="hsp-in" inputMode="decimal" disabled={disabled} value={v.p1} onChange={e => on("p1", e.target.value)} onBlur={() => on("p1", hspFix(v.p1))} /><span>%</span></div>
    </div>

    <div className={`hsp-band${errs.aF2 ? " hsp-band--err" : ""}`}>
      <span className="hsp-band__n">2</span>
      {/* daF2 is a readonly input the blade keeps in sync with finoaF1 from JS. */}
      <input className="hsp-in hsp-in--ro" value={hspFix(v.to1) || "0.00"} readOnly tabIndex={-1} title="Mirrors band 1's To" />
      <input className="hsp-in" inputMode="decimal" disabled={disabled} value={v.to2} onChange={e => on("to2", e.target.value)} onBlur={() => on("to2", hspFix(v.to2))} />
      <div className="hsp-band__pc"><input className="hsp-in" inputMode="decimal" disabled={disabled} value={v.p2} onChange={e => on("p2", e.target.value)} onBlur={() => on("p2", hspFix(v.p2))} /><span>%</span></div>
    </div>

    <div className={`hsp-band hsp-band--last${errs.p3 ? " hsp-band--err" : ""}`}>
      <span className="hsp-band__n">3</span>
      <input className="hsp-in hsp-in--ro" value={hspFix(v.to2) || "0.00"} readOnly tabIndex={-1} title="Mirrors band 2's To" />
      <input className="hsp-in hsp-in--ro" value="∞" readOnly tabIndex={-1} title="Open-ended — the blade renders this control disabled" />
      <div className="hsp-band__pc"><input className="hsp-in" inputMode="decimal" disabled={disabled} value={v.p3} onChange={e => on("p3", e.target.value)} onBlur={() => on("p3", hspFix(v.p3))} /><span>%</span></div>
    </div>

    <div className="hsp-stored">
      <span>Stored as</span>
      <code>perc_first_range = "{hspRangeStr(v.from1, v.to1, v.p1)}"</code>
      <code>perc_second_range = "{hspRangeStr(v.to1, v.to2, v.p2)}"</code>
    </div>
    {thirdNote && <div className="hsp-thirdnote"><Icon name="info" size={13} /><div>{thirdNote}</div></div>}
  </div>
);

/* ================================================================== *
 * Create / Edit — GET /sportprofiles/form/?id= then POST
 * /sportprofiles/saveSportprofile?id=. Neither endpoint has ANY
 * permission check (controller L156, L189).
 * ================================================================== */
const HspFormModal = ({ profile, onClose, onSave, now }) => {
  const isNew = !profile;
  const blank = {
    name: "", skin_id: "", tipologia: "", calc_period: "m",
    misto: HSP_RUNG_IDX.map(() => ""), mistofisso_perc: "",
    from1: "0.00", to1: "", p1: "", to2: "", p2: "", p3: "",
    formula: "1", coupon_min: "0", played_min: "0.00",
  };
  const [f, setF] = hspUseState(() => {
    if (!profile) return blank;
    return {
      name: profile.name,
      skin_id: String(profile.skin_id),
      tipologia: String(profile.tipologia),
      calc_period: profile.calc_period || "m",
      misto: profile.misto.slice(),
      mistofisso_perc: profile.mistofisso_perc,
      from1: profile.from1, to1: profile.to1, p1: profile.p1,
      to2: profile.to2, p2: profile.p2, p3: profile.p3,
      formula: profile.formula || "1",
      coupon_min: profile.coupon_min,
      played_min: profile.played_min,
    };
  });
  const [errs, setErrs] = hspUseState({});
  const [banner, setBanner] = hspUseState("");

  const set = (k, v) => { setF(x => ({ ...x, [k]: v })); if (errs[k]) setErrs(e => { const n = { ...e }; delete n[k]; return n; }); setBanner(""); };
  const tip = Number(f.tipologia) || 0;

  /* THE LOCK. Blade L489-499: on edit only, and only from the browser's own clock. It applies to
     every role — there is no exemption path in that JS — while the warning alert below it uses a
     different rule and does exempt super admins. Neither is checked server-side. */
  const locked = !isNew && hspSaveBlocked(now);
  const periodDay = hspDayOfPeriod(now);
  const EXEMPT = true; /* the prototype's viewer is a super admin: isadmin() ⇒ exempt from the alert */
  const showAlert = !EXEMPT && periodDay > 2;

  /* Where the third GGR band's percentage really lands, per typology. */
  const thirdCol = tip === 1 ? "misto_utile" : tip === 3 ? "mistofisso_utile" : tip === 10 ? "utile_perc" : null;

  const save = () => {
    /* Mirrors the inline validation in saveSportprofile L204-344 (no FormRequest): manual $errors[]
       accumulation answered as ajaxError($msg, ["campierrati" => [fields]]), which the shared modal
       turns into red field outlines. */
    const e = {};
    if (!f.name.trim()) e.name = "Insert name";                       /* backend.insert_name */
    if (!f.skin_id) e.skin_id = "Select skin";                        /* backend.select_skin */
    if (!f.tipologia) e.tipologia = "Select typology";                /* backend.select_typology */
    if (!f.calc_period) e.calc_period = "Insert period";              /* backend.insert_period — label inferred */
    if (tip === 3 && (!hspHas(f.mistofisso_perc) || hspNum(f.mistofisso_perc) === 0)) e.mistofisso_perc = "Fill in all fields";
    /* utile_perc3 — required on every typology, and 0 is a legal value, so this only checks presence. */
    if (!hspHas(f.p3)) e.p3 = "Fill in all fields";                   /* backend.fill_in_all_fields */
    if (!hspHas(f.to1) || hspFix(f.to1) === "0.00") e.finoaF1 = "Fill in all fields";
    if (!hspHas(f.to2)) e.aF2 = "Fill in all fields";
    else if (hspNum(f.to2) <= hspNum(f.to1)) e.aF2 = "Impossible interval"; /* backend.impossible_interval */
    setErrs(e);
    const first = ["name", "skin_id", "tipologia", "calc_period", "mistofisso_perc", "finoaF1", "aF2", "p3"].map(k => e[k]).filter(Boolean)[0];
    if (first) { setBanner(first); return; }
    setBanner("");
    onSave({
      id: isNew ? null : profile.id,
      ...f,
      skin_id: Number(f.skin_id),
      tipologia: tip,
      /* JS forces "m" whenever the period select is hidden (typology 1 and 3). */
      calc_period: tip === 10 ? f.calc_period : "m",
      /* ggr_commission_calc_method is only written for typology 1 and 3. */
      formula: tip === 10 ? "" : f.formula,
      misto: tip === 1 ? f.misto.slice() : HSP_RUNG_IDX.map(() => ""),
      mistofisso_perc: tip === 3 ? hspFix(f.mistofisso_perc) : "",
    });
    onClose();
  };

  return (
    <HspModal
      wide
      title={isNew ? "New sport profile" /* label inferred — the real "new" title is passed in from the toolbar button */ : `Edit ${profile.name}`}
      sub={isNew
        ? <>POST <code>/sportprofiles/saveSportprofile</code> · <code>stato</code> is written as <b>1</b> server-side, always</>
        : <>ID {profile.id} · <code>sport_profiles.id = {profile.id}</code> · {profile.users_cnt} user{profile.users_cnt === 1 ? "" : "s"} assigned</>}
      onClose={onClose}
      footer={<>
        <button className="btn btn--secondary" onClick={onClose}>Close</button>
        {locked
          ? (
            /* KNOWN BUG — DIVERGENCE: the real blade REMOVES the button and calls .show() on
               #cant_save_label to explain why — an element that does not exist in the view, so the
               screen just loses its Save with no message at all. The lock is kept (it is real), the
               missing explanation is restored. */
            // <!-- SUGGESTION: enforce the edit window on the server (saveSportprofile has no date guard, so the block is trivially bypassed by replaying the POST) and align the two rules — the button-hider uses the calendar day of month with no exemptions, while the warning alert uses getDayOfCurrentPeriod() and exempts super admins / skins with disable_change_comm_profile_limits. Until then, render the reason: #cant_save_label is referenced in JS but exists in no view. -->
            <button className="btn btn--primary" disabled title="Editing is closed for this period">
              <Icon name="lock" size={13} /> Save locked
            </button>
          )
          : <button className="btn btn--primary" onClick={save}><Icon name="check" size={13} /> Save</button>}
      </>}>

      {banner && <div className="hsp-err hsp-err--banner"><Icon name="alert" size={13} /> {banner}</div>}

      {locked && (
        <div className="hsp-lock">
          <div className="hsp-lock__head"><Icon name="lock" size={14} /> Editing is closed — day {now.getDate()} of the month</div>
          <div className="hsp-lock__body">
            The form hides Save on every edit once the <b>browser's</b> calendar day is past the 2nd, for every role,
            super admin included (<code>forms/sportprofile.blade.php</code> L489-499). Creating a new profile is not
            affected. Two things about this are worth knowing:
            <ul>
              <li><b>The server does not enforce it.</b> <code>saveSportprofile</code> has no date guard, so the same
                POST replayed by hand is accepted on any day — the window is a courtesy, not a control.</li>
              <li><b>The screen normally says nothing.</b> The blade explains the block through
                <code> #cant_save_label</code>, which exists in no view, so the button silently disappears. This panel
                is that missing explanation.</li>
            </ul>
            For reference this is day <b>{periodDay}</b> of the commission period (counted from the first Monday of the
            month) — the rule behind the separate warning alert, which uses a different date <i>and</i> exempts
            super admins.
          </div>
        </div>
      )}

      {showAlert && (
        <div className="hsp-err hsp-err--banner">
          <Icon name="alert" size={13} /> Commissions profiles only editable on 2 first days of current period
          {/* commissions.unable_edit */}
        </div>
      )}

      <HspSection title="Profile">
        <div className="hsp-grid2">
          <HspField label="Name" req err={errs.name} htmlFor="hsp-name">
            <input id="hsp-name" className={`hsp-in hsp-in--full${errs.name ? " hsp-invalid" : ""}`} autoFocus={isNew}
              value={f.name} onChange={e => set("name", e.target.value)} />
          </HspField>
          {/* Super admin gets the full skins list; a skin admin gets a hidden input pinned to their own
              skin (blade L56-72) — client-side scoping only, see the gate note on the page header. */}
          <HspField label="Skin" req err={errs.skin_id} htmlFor="hsp-skin">
            <select id="hsp-skin" className={`hsp-in hsp-in--full${errs.skin_id ? " hsp-invalid" : ""}`}
              value={f.skin_id} onChange={e => set("skin_id", e.target.value)}>
              <option value="">Select skin</option>
              {HSP_SKINS.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </select>
          </HspField>
        </div>

        <HspField label="Typology" req err={errs.tipologia} htmlFor="hsp-tip"
          hint={tip ? hspType(tip).blurb : "Choosing a typology decides which rate card below applies."}>
          <select id="hsp-tip" className={`hsp-in hsp-in--full${errs.tipologia ? " hsp-invalid" : ""}`}
            value={f.tipologia} onChange={e => set("tipologia", e.target.value)}>
            <option value="">Select typology</option>
            {HSP_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
          </select>
        </HspField>

        {/* The period select is rendered only for typology 10; for 1 and 3 the JS both hides it and
            forces "m", so showing a control that cannot change anything would be a decoy. */}
        {tip === 10 ? (
          <HspField label="Period" req err={errs.calc_period} htmlFor="hsp-period" width={280}
            hint={<>Which run pays this profile — <code>calc_period</code> must match the commission run's type.</>}>
            <select id="hsp-period" className={`hsp-in hsp-in--full${errs.calc_period ? " hsp-invalid" : ""}`}
              value={f.calc_period} onChange={e => set("calc_period", e.target.value)}>
              {HSP_PERIODS.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
            </select>
          </HspField>
        ) : tip ? (
          <div className="hsp-hint hsp-hint--pin">
            <Icon name="lock" size={12} />
            <div>Period is fixed to <b>Monthly</b> for this typology — the form hides the select and forces
              <code> calc_period = "m"</code> (blade L407-408, L423-424). Only <b>Net profit</b> profiles can be paid
              weekly.</div>
          </div>
        ) : null}
      </HspSection>

      {tip === 1 && (
        <HspSection title="Turnover ladder" note={<>one percentage per number of events on the coupon</>}>
          <HspLadder value={f.misto} onChange={v => set("misto", v)} disabled={locked} />
        </HspSection>
      )}

      {tip === 3 && (
        <HspSection title="Fixed turnover" note={<>one percentage for every coupon</>}>
          <HspField label="Turnover commission" req err={errs.mistofisso_perc} htmlFor="hsp-fisso" width={280}
            hint={<>Saved to <code>mistofisso_perc</code>. Required and must not be 0 — the one field this typology
              validates strictly.</>}>
            <div className="hsp-band__pc hsp-band__pc--solo">
              <input id="hsp-fisso" className={`hsp-in${errs.mistofisso_perc ? " hsp-invalid" : ""}`} inputMode="decimal"
                disabled={locked} value={f.mistofisso_perc}
                onChange={e => set("mistofisso_perc", e.target.value)} onBlur={() => set("mistofisso_perc", hspFix(f.mistofisso_perc))} />
              <span>%</span>
            </div>
          </HspField>
        </HspSection>
      )}

      {tip === 10 && (
        <HspSection title="Turnover ladder" tone="muted">
          <div className="hsp-hint hsp-hint--pin">
            <Icon name="info" size={12} />
            <div><b>Net profit</b> profiles have no turnover leg — nothing is paid per coupon. The whole rate card is
              the GGR bands below.</div>
          </div>
        </HspSection>
      )}

      {/* Always rendered, like the blade's own block (L211-327) — the GGR bands are validated on
          every typology, including before one is chosen. */}
      <HspSection title="Commissions on GGR" note={<>validated for every typology</>}>
          <HspBands v={f} on={set} errs={errs} disabled={locked}
            thirdNote={thirdCol && (
              /* The single most confusing wiring on this form: the open-ended band's percentage is
                 ALSO the profile's net-profit percentage, under a different column name per
                 typology. The blade's own inputs for those columns are hidden decoys, so this note
                 replaces them rather than rendering controls that cannot affect the record. */
              // <!-- SUGGESTION: give the third band's percentage one field name instead of copying utile_perc3 into misto_utile / mistofisso_utile / utile_perc, and delete the two dead inputs (typology 3's display:none mistofisso_utile row and typology 10's never-un-hidden utile_perc box) — today the form ships controls whose value is never read. -->
              <>Band 3's percentage is saved <b>twice</b>: as the open-ended GGR band <i>and</i> as this profile's
                net-profit commission, in <code>{thirdCol}</code>. The form has no separate control for it — the
                blade's own {tip === 10 ? <><code>utile_perc</code> input is never un-hidden</> : <><code>{thirdCol}</code> row is <code>display:none</code></>}, and the value comes from here either way.</>
            )} />
          {errs.finoaF1 && <div className="hsp-fielderr">Band 1 · To: {errs.finoaF1}</div>}
          {errs.aF2 && <div className="hsp-fielderr">Band 2 · To: {errs.aF2}</div>}
          {errs.p3 && <div className="hsp-fielderr">Band 3 · %: {errs.p3}</div>}
      </HspSection>

      {tip !== 10 && (
        <HspSection title="Formula" note={<>how the two legs are combined</>}>
          <HspField label="Calculation method" htmlFor="hsp-formula"
            hint={<>Saved to <code>ggr_commission_calc_method</code>. Not validated — a profile saved without touching
              it keeps whatever the select happened to show. Hidden for <b>Net profit</b>, which has no turnover
              commission to net off.</>}>
            <select id="hsp-formula" className="hsp-in hsp-in--full" disabled={locked}
              value={f.formula} onChange={e => set("formula", e.target.value)}>
              {HSP_FORMULAS.map(x => <option key={x.value} value={x.value}>{x.label}</option>)}
            </select>
          </HspField>
        </HspSection>
      )}

      <HspSection title="Thresholds" note={<>optional — both default to 0</>}>
          <div className="hsp-grid2">
            <HspField label="Minimum number of coupons" htmlFor="hsp-coupons"
              hint={<>Saved to <code>coupon_min</code>.</>}>
              <input id="hsp-coupons" className="hsp-in hsp-in--full" inputMode="numeric" disabled={locked}
                value={f.coupon_min} onChange={e => set("coupon_min", e.target.value)} />
            </HspField>
            <HspField label="Minimum bet" htmlFor="hsp-minbet" hint={<>Saved to <code>played_min</code>.</>}>
              <input id="hsp-minbet" className="hsp-in hsp-in--full" inputMode="decimal" disabled={locked}
                value={f.played_min} onChange={e => set("played_min", e.target.value)} onBlur={() => set("played_min", hspFix(f.played_min))} />
            </HspField>
          </div>
      </HspSection>

      {!isNew && (
        /* Read-only count the real edit modal prints at the bottom of the form (blade L328-337). */
        <div className="hsp-assigned">
          <Icon name="users" size={14} />
          <b>{profile.users_cnt}</b> user{profile.users_cnt === 1 ? "" : "s"} assigned
          <span>through <code>commissions_profiles.sport_profile_id</code> → <code>users.profilo_provvigionale</code></span>
        </div>
      )}
    </HspModal>
  );
};

/* ================================================================== *
 * Delete — GET /sportprofiles/delete/{id}/ behind a JS confirm.
 * The controller answers 'Permission error' to anyone who is not a super
 * admin (L426-427), but ajax.js renders the trash icon for every user who
 * can see the table — a skin admin can click it and gets no visible
 * feedback at all.
 * ================================================================== */
const HspDeleteDialog = ({ profile, onClose, onConfirm }) => (
  <HspModal title="Delete sport profile" onClose={onClose}
    footer={<>
      <button className="btn btn--secondary" onClick={onClose}>Cancel</button>
      <button className="btn btn--danger" onClick={() => { onConfirm(profile); onClose(); }}>
        <Icon name="trash" size={13} /> Delete
      </button>
    </>}>
    <div className="hsp-dlgq">Are you sure you want to delete this record?</div>
    <div className="hsp-dlgsub"><b>{profile.name}</b> · ID {profile.id} · {hspTypeLabel(profile.tipologia)} · {hspSkinName(profile.skin_id)}</div>
    {profile.users_cnt > 0 && (
      <div className="hsp-warn">
        <Icon name="alert" size={13} />
        <div>
          {profile.users_cnt} user{profile.users_cnt === 1 ? " is" : "s are"} on a commission profile pointing at this
          row. Deleting it leaves <code>commissions_profiles.sport_profile_id</code> aimed at a record that no longer
          exists — nothing on the platform checks first.
          {/* <!-- SUGGESTION: have SportProfilesController::delete refuse (or reassign) when the hasManyThrough user count is greater than 0, the same count the table already renders — right now the row goes and the referencing commissions_profiles rows are left dangling. --> */}
        </div>
      </div>
    )}
    <div className="hsp-hint">
      Sent as a plain <code>GET /sportprofiles/delete/{profile.id}/</code> — the platform's generic delete convention
      for this batch of screens. The endpoint is <b>super admin only</b>; a skin admin who clicks the same icon gets a
      JSON <code>'Permission error'</code> the UI never displays.
      {/* <!-- SUGGESTION: hide the trash icon unless isadmin(), or surface the 'Permission error' the endpoint returns — as shipped, a skin admin's delete looks like it did nothing. --> */}
    </div>
  </HspModal>
);

/* ================================================================== */
const SportProfiles = () => {
  window.useLocale && window.useLocale();

  const now = hspUseMemo(() => new Date(), []);
  const [rows, setRows] = hspUseState(hspBuildProfiles);
  /* Filters are DataTables column searches applied on the Search button, not on keyup. */
  const [draft, setDraft] = hspUseState({ id: "", name: "", skin: "", tipologia: "" });
  const [applied, setApplied] = hspUseState({ id: "", name: "", skin: "", tipologia: "" });
  const [sort, setSort] = hspUseState({ key: "id", dir: "desc" }); // ajax.js order [[0,"desc"]]
  const [page, setPage] = hspUseState(0);
  const [pageSize, setPageSize] = hspUseState(50);
  const [form, setForm] = hspUseState(null); // null | { profile: row|null }
  const [del, setDel] = hspUseState(null);   // null | row

  const editLocked = hspSaveBlocked(now);
  const periodDay = hspDayOfPeriod(now);

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "tag", placeholder: "Exact ID", width: 140,
      tip: <>Exact match on <code>sport_profiles.id</code> (controller L71-73) — not a contains search.</> },
    { key: "name", label: "Name", type: "text", icon: "search", placeholder: "Profile name", grow: true,
      tip: <>Server-side <code>LIKE %…%</code> on <code>sport_profiles.name</code> (L75-77).</> },
    /* Rendered only for super admins (index.blade.php L72-82). A non-admin never sees it because the
       controller hard-scopes their query to their own skin (L120-122) — the filter would be a no-op. */
    { key: "skin", label: "Skin", type: "select", icon: "flag", placeholder: "- All skins -", width: 210,
      options: HSP_SKINS.map(s => ({ value: String(s.id), label: s.name })),
      tip: <>Super admin only. Skin admins do not get this control: their list is already forced to
        <code> skin_id = </code>their own skin server-side.</> },
    { key: "tipologia", label: "Typology", type: "select", icon: "sliders", placeholder: "- All typologies -", width: 250,
      options: HSP_TYPES.map(t => ({ value: String(t.value), label: t.label })),
      tip: <>Options come from <code>types()</code> — only 1, 3 and 10 are ever offered, although the save path still
        carries dead branches for 4, 5 and 15.</> },
  ];

  const filtered = hspUseMemo(() => {
    const idq = String(applied.id || "").trim();
    const nq = String(applied.name || "").trim().toLowerCase();
    return rows.filter(r => {
      if (idq && String(r.id) !== idq) return false;
      if (nq && r.name.toLowerCase().indexOf(nq) === -1) return false;
      if (applied.skin && String(r.skin_id) !== String(applied.skin)) return false;
      if (applied.tipologia && String(r.tipologia) !== String(applied.tipologia)) return false;
      return true;
    });
  }, [rows, applied]);

  const sorted = hspUseMemo(() => {
    const dir = sort.dir === "asc" ? 1 : -1;
    const cmp = {
      id: (a, b) => a.id - b.id,
      name: (a, b) => a.name.localeCompare(b.name),
      tipologia: (a, b) => a.tipologia - b.tipologia,
      skin_id: (a, b) => hspSkinName(a.skin_id).localeCompare(hspSkinName(b.skin_id)),
      /* KNOWN BUG — DIVERGENCE: users_cnt is registered with a name so DataTables asks the server to
         order by it, but the controller's order switch (L86-117) has no case for it, so the request
         silently falls through to sport_profiles.id ASC — the column looks sortable and is not.
         Evident intent implemented: it sorts by the count. */
      // <!-- SUGGESTION: either add a users_cnt case to SportProfilesController's order switch (it needs the hasManyThrough count as a subquery, which would also kill the per-row N+1 at L142) or declare the column orderable:false so the header stops pretending. -->
      users_cnt: (a, b) => a.users_cnt - b.users_cnt,
    }[sort.key] || ((a, b) => a.id - b.id);
    return filtered.slice().sort((a, b) => cmp(a, b) * dir);
  }, [filtered, sort]);

  const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
  const safePage = Math.min(page, pageCount - 1);
  const paged = sorted.slice(safePage * pageSize, safePage * pageSize + pageSize);

  const onSearch = (v) => { setApplied({ id: v.id || "", name: v.name || "", skin: v.skin || "", tipologia: v.tipologia || "" }); setPage(0); };
  const onReset = () => { const z = { id: "", name: "", skin: "", tipologia: "" }; setDraft(z); setApplied(z); setPage(0); };

  const onSave = (p) => {
    if (p.id == null) {
      /* KNOWN BUG — DIVERGENCE: the real create discards the new id (`SportProfile::create($datip)->id;`
         with the result unassigned, controller L361), so the success payload echoes the empty request
         id and the caller cannot address the row it just made. Evident intent implemented. */
      // <!-- SUGGESTION: assign the result of SportProfile::create() and return its id in the save response's params / record_data — every other create path in this batch has the same unassigned-expression bug. -->
      const id = rows.reduce((m, r) => Math.max(m, r.id), 0) + 1;
      setRows(rs => [...rs, { ...p, id, users_cnt: 0 }]);
      hrsToast(`Sport profile "${p.name}" created`, `id ${id} · ${hspTypeLabel(p.tipologia)} · ${hspSkinName(p.skin_id)} · stato = 1 (always written server-side).`);
    } else {
      setRows(rs => rs.map(r => r.id === p.id ? { ...r, ...p, users_cnt: r.users_cnt } : r));
      hrsToast(`Sport profile "${p.name}" saved`, `${hspTypeLabel(p.tipologia)} · ${hspSkinName(p.skin_id)} · the edit window is not checked server-side.`);
    }
  };

  const onDelete = (p) => {
    setRows(rs => rs.filter(r => r.id !== p.id));
    hrsToast(`Sport profile "${p.name}" deleted`, `GET /sportprofiles/delete/${p.id}/ — accepted because this session is a super admin; any other role gets 'Permission error'.`);
  };

  const acts = (r) => (
    <div className="hsp-acts">
      {/* title="Elimina" in the real markup — leftover Italian. */}
      <button className="hsp-act hsp-act--danger" title="Delete" onClick={(e) => { e.stopPropagation(); setDel(r); }}>
        <Icon name="trash" size={13} />
      </button>
      <button className="hsp-act hsp-act--edit" title="Edit" onClick={(e) => { e.stopPropagation(); setForm({ profile: r }); }}>
        <Icon name="edit" size={13} />
      </button>
    </div>
  );

  const columns = [
    { key: "id", label: "ID", sortable: true, firstDir: "desc", width: 90, render: r => <span className="hsp-id">{r.id}</span> },
    /* The Name cell is the edit link — javascript:gestioneSportprofile(id,'Edit <name>') (controller
       L137). The row also carries a hidden nome_completo that only feeds that title. */
    { key: "name", label: "Name", sortable: true, firstDir: "asc",
      render: r => (
        <button className="hsp-namelink" title="Edit" onClick={() => setForm({ profile: r })}>
          <span>{r.name}</span>
          <Icon name="chevron_right" size={13} />
        </button>
      ) },
    { key: "tipologia", label: "Typology", sortable: true, firstDir: "asc", width: 300,
      render: r => <HspTypeChip t={r.tipologia} sub /> },
    { key: "skin_id", label: "Skin", sortable: true, firstDir: "asc", width: 170,
      render: r => <span className="hsp-skin">{hspSkinName(r.skin_id)}</span> },
    { key: "users_cnt", label: "Users Assigned", sortable: true, firstDir: "desc", align: "center", width: 160,
      render: r => <span className={`hsp-users${r.users_cnt ? "" : " hsp-users--zero"}`}>{hrsInt(r.users_cnt)}</span> },
    { key: "_acts", label: "Actions", align: "center", width: 120, render: acts }, // backend.actions
  ];

  return (
    <HrsShell
      title="Sport profiles"
      subtitle="Rate cards that turn a shop or agent's sport volume into commission"
      gate={<>Real-platform access: <code>isadmin()</code> <b>or</b> a skin admin whose skin carries
        <code> enable_commissions_management</code> — <code>SportProfilesController::index</code> and
        <code> getSportprofilesTable</code> abort <code>404</code> for anyone else. No <code>checkUserBoPerm</code>
        permission is involved. </>}
      gateNote={<>Permission asymmetry, honestly: <code>sportprofileForm</code> and <code>saveSportprofile</code> carry
        <b> no role or skin check at all</b>, so any authenticated, 2FA'd back-office user who calls those URLs
        directly can create or edit a profile — and, because the skin scoping is a hidden input rather than a server
        rule, aim it at a skin that is not theirs. <code>delete</code> does re-check <code>isadmin()</code>, but its
        trash icon is rendered for everyone who can see the table.</>}
      explainer={{ bullets: [
        <>A <b>sport profile</b> is the sport half of a commission contract. Commission profiles point at one through
          <code> commissions_profiles.sport_profile_id</code>; the monthly and weekly commission runs then read the
          percentages stored here, and the Commissions report shows them back as the turnover modal's "Percentage
          steps" and range breakdown.</>,
        <>Three typologies ship: <b>Turnover + Monthly Bonus</b> pays a turnover leg off a 30-rung ladder — a
          percentage per number of events on the coupon; <b>Fixed turnover + monthly bonus</b> pays one flat turnover
          percentage; <b>Net profit</b> pays no turnover at all. All three also carry the GGR bands, and the
          <i> formula</i> decides how the two legs are combined for the first two.</>,
        <><b>Editing is time-boxed — but only in the browser.</b> Past the 2nd of the month the form removes Save from
          every edit, for every role. <code>saveSportprofile</code> has no matching date check, so the restriction
          holds only for people using the screen as intended.</>,
        <>Deleting a profile is not blocked by the users assigned to it: the count in the last column comes from
          commission profiles pointing here, and nothing verifies it before the row is removed.</>,
      ] }}
      actions={
        <button className="hrs-btn hrs-btn--filters" onClick={() => setForm({ profile: null })}>
          <Icon name="plus" size={14} /> New sport profile{/* label inferred */}
        </button>
      }>

      {/* The edit window, stated up front rather than discovered when Save is missing. Derived from
          the browser clock exactly as the real JS is. */}
      <div className={`hsp-window${editLocked ? " hsp-window--shut" : " hsp-window--open"}`}>
        <Icon name={editLocked ? "lock" : "check"} size={14} />
        <div>
          <b>Edit window {editLocked ? "closed" : "open"}</b> — day {now.getDate()} of the month
          {editLocked
            ? <> · the form drops <b>Save</b> on every edit past the 2nd, for every role including super admin. Creating
              a new profile still works, and the server accepts edits regardless: <code>saveSportprofile</code> has no
              date guard.</>
            : <> · edits save normally until the end of the 2nd.</>}
        </div>
        <span className="hsp-window__day" title="getDayOfCurrentPeriod() — days since the first Monday of the month (utils.php L2377-2399), the rule behind the separate warning alert">
          period day {periodDay}
        </span>
      </div>

      <HrsFilters
        fields={FIELDS} values={draft}
        onChange={(k, v) => setDraft(d => ({ ...d, [k]: v }))}
        onSearch={onSearch} onReset={onReset}
        resultLabel={`${hrsInt(sorted.length)} of ${hrsInt(rows.length)}`} />

      <HrsTable
        columns={columns} rows={paged} rowKey="id"
        sort={sort} onSort={(s) => { setSort(s); setPage(0); }}
        empty={(applied.id || applied.name || applied.skin || applied.tipologia)
          ? "No sport profile matches these filters."
          : "No sport profiles yet — create one with New sport profile."}
        renderCard={r => (
          <>
            <div className="hrs-card__top">
              <b>{r.name}</b>
              <span className="hsp-id">ID {r.id}</span>
            </div>
            <div className="hsp-card__chips">
              <HspTypeChip t={r.tipologia} />
              <span className="hsp-skin">{hspSkinName(r.skin_id)}</span>
              <span className={`hsp-users${r.users_cnt ? "" : " hsp-users--zero"}`}>{hrsInt(r.users_cnt)} assigned</span>
            </div>
            <div className="hsp-card__acts">
              <button className="btn btn--secondary btn--sm" onClick={() => setForm({ profile: r })}><Icon name="edit" size={12} /> Edit</button>
              <button className="btn btn--ghost btn--sm hsp-card__del" onClick={() => setDel(r)}><Icon name="trash" size={12} /> Delete</button>
            </div>
          </>
        )} />

      <HrsPager page={safePage} pageSize={pageSize} total={sorted.length} sizes={HSP_PAGE_SIZES}
        onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(0); }} />

      {form && <HspFormModal profile={form.profile} now={now} onClose={() => setForm(null)} onSave={onSave} />}
      {del && <HspDeleteDialog profile={del} onClose={() => setDel(null)} onConfirm={onDelete} />}
    </HrsShell>
  );
};

/* Loads after src/pages/HostCommissions.jsx, deliberately replacing its legacy SportProfiles global
   (app.jsx:470 renders <SportProfiles/> for route key "comm-sport-profiles"). */
window.SportProfiles = SportProfiles;
