// 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 /sport/bet/ · SportController::bet + getBetData — see docs/ISYSTEM_REFERENCE.md §Batch 5 "Bet from backoffice"
/* Bet from backoffice — the counter terminal. A shop/agent operator picks one of their players, opens the
   sportsbook INSIDE THAT PLAYER'S OWN SESSION and places bets that debit the player's real balance. There is
   no operator wallet in the loop: every ticket is the player's ticket, settles against the player's money and
   shows up on the Sport coupons screen under the player's name.

   Real surfaces represented here:
     page      GET  /sport/bet/                                → SportController::bet (view + gate only, L69)
     data      GET  /sport/bet/getBetData/{userid}/{sporttype}/ → SportController::getBetData (static, L184) —
               returns balance_html (currency + balance + balance_withdrawable, plus "Bonus: <total>"), the raw
               user_balance / user_balance_withdrawable, and the sportsbook iframe URL
     search    GET  /users2  (admin.users.search)               → UsersController::searchUsers2 (L5033) —
               username PREFIX match, user_types [30], scoped to Auth::user()->getChilds(true), 10 per page
     deposit   POST /dotransfer/                                → TransferController::dotransfer (L197) —
               {from_user, transfer_type:'add', transfer_usertype:'player', to_user, amount}
     transfer  GET  /transfer/?from=&type=player&to=            → the balance widget's transfer icon (HostDeposit.jsx)
     frame     FrontendGamesController::sport($request,$sporttype,1,$userid) → provider openIframe($player,$type);
               provider chosen by skins.sport_provider (default NovusController · igpixel · cmswager · mondogaming)

   Known-bug divergences (evident intent implemented, per CLAUDE.md policy — each is also flagged in-place):
   - SportController.php:188-190 tests/sets a local `$sport_type` while the parameter and everything downstream
     is `$sporttype`, so the "default to prematch" branch is dead. Implemented as intended here: anything that
     is not `prematch` | `live` normalises to `prematch` (hsbSportType).
     <!-- SUGGESTION: rename the local at SportController.php:188-190 to $sporttype (or delete the dead branch) so the documented prematch default actually applies. -->
   - SportController.php:198 silently runs `$row->update(['ip' => get_client_ip()])` on EVERY getBetData call —
     i.e. opening this screen, switching sport type, or hitting the balance refresh overwrites the PLAYER's
     stored `ip` with the OPERATOR's. That destroys the player's own last-login-IP audit trail (a column the
     Sport coupons list, Players list and fraud review all read) to satisfy a sportsbook-session need. Evident
     intent implemented: the operator's terminal IP is handed to the sportsbook session, the player's stored
     IP is left untouched — and the divergence is stated on screen instead of happening invisibly.
     <!-- SUGGESTION: stop writing users.ip from SportController::getBetData. Pass the operator IP straight to openIframe() (it already receives the session payload), and if the terminal IP must be persisted, give it its own column (e.g. users.last_bo_bet_ip) so the player's real last-login IP survives. -->
   - getBetData has NO ownership / skin / permission check beyond "target is user_level 30", so any authenticated
     back-office user can pull any player's balance AND a betting iframe in that player's session by ID. The
     select2 scoping to getChilds(true) is UI-only. Surfaced in the page's gate note rather than papered over.
     <!-- SUGGESTION: re-apply the screen's own gate inside getBetData — user_path descendant check (Str::startsWith), $disable_bet_section, and the isCustomCare()/support_sport_bet clause — so the endpoint is not a straight IDOR into any player's session. -->
   - The custom-deposit box only checks "not empty" client-side (alert "Insert amount"); the real format rules
     live in TransferController::isValidTransferAmountFormat and fire after the round trip. Applied here up
     front — same rules, no new ones: commas stripped, no whitespace, ^-?\d+(\.\d+)?$, amount > 0.

   Real-platform quirks deliberately NOT reproduced (dead or code-level, invisible as UI):
   - `$response["select_sport_type"]` is always '' (SportController.php:243-245) — leftover; the radio pair is
     static in the blade (which also duplicates the `checked` attribute on the prematch input).
   - FrontendGamesController::sport() is a non-static method called statically from the static getBetData
     (L254) — deprecated on PHP 7.4, fatal on PHP 8.
   - bet.blade.php: stray literal `>` after @section('footer_scripts') (L127); the sidebar <li> at
     sidebar.blade.php:174 is never closed; select2 + DataTables + bootstrap-datepicker + two copies of
     buttons.html5.min.js are loaded from CDNs and none of them is used by this page.
   - Untranslated strings in the real page: JS alert("Insert amount"), confirm('Are you sure you want to
     reload the player with X?'), and the server-built Italian link label "Trasferisci" (SportController.php:250).
     Written as operator-facing English here per the label policy.
     <!-- SUGGESTION: move the three hardcoded strings in bet.blade.php / SportController::getBetData into backend.* keys, and add the missing `bet_from_backoffice` key to public/default-lang/en/backend.php — today the nav label resolves nowhere outside the gitignored storage/lang. -->
     <!-- SUGGESTION: the fast-deposit denominations 5/10/20/50 are hardcoded (SportController.php:229-236) and merely suffixed with the player's currency — in ARS that makes the first two buttons unusable. Make them a per-skin/per-currency setting. -->
     <!-- SUGGESTION: align the sidebar gate with the controller gate. Today the sidebar's `!isCustomCare() || checkUserBoPerm(...,"support_sport_bet")` clause is dead (the outer condition already excludes CC) and `!isAdmin()` hides the entry from admins, yet bet() blocks neither — both roles reach the screen by typing the URL. -->

   Faithful absences — the real screen genuinely has none of these, so neither does this one: no table, no filters
   beyond Player + Sport type, no sortable columns, no pagination, no bulk actions, no export, no KPI strip
   (the balance widget IS the only figure on the page), and no create/edit form.

   Honesty pass: the balance widget's "Open in Transfer" link no longer just decorates an href — it performs
   the prototype's own cross-page navigation to the Deposit / Transfer screen (route key `host-deposit`) and
   carries the real deep-link query (from / type / to). What that screen does not do yet — consume the query
   to pre-fill payer and target — is stated in the link's Tip instead of being implied.

   Labels: `bet_from_backoffice` (page/nav title) resolves nowhere in the committed lang files — inferred.
   Everything else resolves: player→"Player", balance→"Balance", select_player→"Select player",
   sport_type→"Sport type", fast_deposit→"Fast deposit", custom_deposit→"Custom deposit",
   insert_amount→"Insert amount". */

const { useState: hsbUseState, useMemo: hsbUseMemo, useEffect: hsbUseEffect, useRef: hsbUseRef } = React;

/* ---------- deterministic PRNG (FNV-1a + mulberry32) — same convention as the sibling Host pages ---------- */
const hsbHash = (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 hsbRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
const hsbPad = (n) => String(n).padStart(2, "0");
const hsbClock = (ts) => { const d = new Date(ts); return `${hsbPad(d.getHours())}:${hsbPad(d.getMinutes())}:${hsbPad(d.getSeconds())}`; };
const hsbDT = (ts) => { const d = new Date(ts); return `${hsbPad(d.getDate())}/${hsbPad(d.getMonth() + 1)}/${d.getFullYear()} ${d.getHours()}:${hsbPad(d.getMinutes())}`; }; // d/m/Y G:i, same as HostSportCoupons
const hsbN = (n) => Number(n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });

/* Operator persona: a Shop-level cashier (user_level 20). That is the screen's real audience — the sidebar
   gate excludes Admin / Customer Care / Affiliate / Administration, leaving Shop, Agent, Promoter and Master.
   `sinoplata` is one of the shop usernames the Sport coupons page already shows as a Parent. */
const HSB_ACTOR = { id: 4500118, username: "sinoplata", level: 20, skin: "Casino24hs", skinId: 68, currency: "ARS", ip: "190.221.14.8" };
const HSB_LEVELS = { 0: "Super admin", 8: "Master", 10: "Agent", 15: "Promoter", 20: "Shop", 30: "Player" }; // same wording as HostDeposit.jsx

/* sporttype URL segment (the only two the route accepts). Prematch is rewritten to "sport" before it reaches
   the provider — FrontendGamesController.php:368-370. */
const HSB_SPORT_TYPES = [
  { id: "prematch", label: "Prematch", sent: "sport" },
  { id: "live", label: "Live", sent: "live" },
];
/* Real-platform bug (SportController.php:188-190): the empty-check reads `$sport_type` while the parameter is
   `$sporttype`, so the documented prematch default never applies. Evident intent implemented. */
const hsbSportType = (v) => (HSB_SPORT_TYPES.some(s => s.id === v) ? v : "prematch");

/* skins.sport_provider → provider controller (FrontendGamesController.php:358-374). Casino24hs leaves the
   column empty, so it falls through to the default. */
const HSB_PROVIDERS = [
  { code: "", name: "Novus", ctrl: "NovusController", note: "default — empty sport_provider" },
  { code: "igpixel", name: "IGPixel", ctrl: "IGPixelController" },
  { code: "cmswager", name: "CmsWager", ctrl: "CmsWagerController" },
  { code: "mondogaming", name: "MondoGaming", ctrl: "MondoGamingController" },
];
const HSB_SKIN = { name: "Casino24hs", id: 68, sportProvider: "", showSport: true, disableBetSection: false };

/* Hardcoded server-side at SportController.php:229-236 and suffixed with the player's currency. */
const HSB_FAST_AMOUNTS = [5, 10, 20, 50];

/* Same player universe as HostSportCoupons.jsx so an operator moving between the two screens sees the same people. */
const HSB_NAMES = ["martin147", "emaa4490", "carlos469", "martin6185", "abril142e", "Diego298", "Octavio5439", "Hector2979", "Ramon9295", "Martin7874", "lucas331", "vale889", "joacoo23", "florr910"];

const hsbBuildPlayers = () => {
  const rng = hsbRng(hsbHash("hsb-casino24hs"));
  const now = Date.now();
  /* User::defaultPlayer() — hasOne(Player,'parent_id') where user_level = 30 and default_cashier_player = true,
     auto-created as CA_<shopusername> by User::registerCAPlayer(). Pre-selected on load when it exists. */
  const list = [{ id: 4600001, username: `CA_${HSB_ACTOR.username}`, ca: true, bal: 18500, wd: 0, bonus: 0, ip: "181.94.17.204", lastAccess: now - 3 * 3600000 }];
  HSB_NAMES.forEach((u, i) => list.push({
    id: 4600000 + (i + 1) * 37 + 11,
    username: u, ca: false,
    bal: Math.round(rng() * 900000 * 100) / 100,          // users.balance — non-withdrawable
    wd: Math.round(rng() * 120000 * 100) / 100,           // users.balance_withdrawable
    bonus: rng() < 0.35 ? Math.round(rng() * 50000 * 100) / 100 : 0, // PlayersController::getPlayerTotalBonus
    ip: `181.${80 + Math.floor(rng() * 40)}.${Math.floor(rng() * 255)}.${Math.floor(rng() * 255)}`,
    lastAccess: now - Math.floor(rng() * 96) * 3600000,
  }));
  return list;
};
const HSB_PLAYERS = hsbBuildPlayers();

/* ---------- cross-page navigation to the Deposit / Transfer screen ----------
   The balance widget's transfer icon. Real target: route('admin.transfer')?from=&type=player&to=.
   The prototype has that screen (route key `host-deposit` → /deposit, src/pages/HostDeposit.jsx), so the
   icon navigates for real instead of toasting: push the registered path — query string included, exactly the
   deep-link payload the real link carries — and let app.jsx's popstate handler resolve it (activeForPath
   reads the pathname, so the params ride along harmlessly). HostDeposit does not pre-fill from them yet;
   the Tip beside the link says so rather than the link pretending otherwise. */
const hsbTransferPath = (playerId) =>
  `${(window.pathForActive && window.pathForActive("host-deposit")) || "/deposit"}?from=${HSB_ACTOR.id}&type=player&to=${playerId}`;
const hsbGoTransfer = (playerId) => {
  const path = hsbTransferPath(playerId);
  try {
    if (window.location.pathname + window.location.search !== path) window.history.pushState({ active: "host-deposit" }, "", path);
    const ev = typeof PopStateEvent === "function" ? new PopStateEvent("popstate") : new Event("popstate");
    window.dispatchEvent(ev);
  } catch (_e) {
    window.location.href = path; // last resort: a real full-page load of the same path
  }
};

/* Mirrors TransferController::isValidTransferAmountFormat — commas stripped, no whitespace, ^-?\d+(\.\d+)?$. */
const hsbValidAmount = (raw) => {
  if (raw === null || raw === undefined) return false;
  const s = String(raw).replace(/,/g, "");
  if (s === "" || /\s/.test(s)) return false;
  return /^-?\d+(\.\d+)?$/.test(s);
};

/* ==================================================================
   Player picker — select2 on admin.users.search (GET /users2).
   Same shape as HostDeposit.jsx's target search so the two screens read as one control; the rules differ
   because the endpoint differs: /users2 has no minimum-character rule, matches `username LIKE q%`, filters
   user_types [30] and pages 10 at a time inside Auth::user()->getChilds(true).
   ================================================================== */
const HsbPlayerSearch = ({ pool, picked, onPick }) => {
  const [q, setQ] = hsbUseState("");
  const [open, setOpen] = hsbUseState(false);
  const wrapRef = hsbUseRef(null);
  hsbUseEffect(() => {
    const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);
  const term = q.trim().toLowerCase();
  const matches = pool.filter(u => u.username.toLowerCase().startsWith(term)); // LIKE q% — prefix, not contains
  const results = matches.slice(0, 10);                                        // 10 per select2 page
  return (
    <div className="hsb-search" ref={wrapRef} style={{ position: "relative" }}>
      <div style={{ position: "relative" }}>
        <span className="hsb-search__ic" style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", display: "inline-flex" }}><Icon name="search" size={13} /></span>
        <input className="input hsb-search__in" value={open ? q : (picked ? picked.username : "")}
          placeholder="Select player" /* backend.select_player */
          onFocus={() => { setQ(""); setOpen(true); }}
          onChange={e => { setQ(e.target.value); setOpen(true); }}
          style={{ width: "100%", paddingLeft: 32, paddingRight: 30 }} />
        <span className="hsb-search__caret" style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", display: "inline-flex", pointerEvents: "none" }}><Icon name="chevron_down" size={13} /></span>
      </div>
      {open && (
        <div className="hsb-search__drop">
          {results.length === 0 ? (
            <div className="hsb-search__hint">No player found. The list only holds your own descendants (<code>getChilds(true)</code>) with <code>user_level = 30</code>.</div>
          ) : results.map(u => (
            <button key={u.id} className="hsb-search__item" onClick={() => { onPick(u); setOpen(false); setQ(""); }}>
              <span className="hsb-search__name">
                {u.username}
                {u.ca && <span className="hsb-flag">default cashier player</span>}
              </span>
              <span className="hsb-search__meta">#{u.id}</span>
            </button>
          ))}
          {matches.length > 10 && (
            <div className="hsb-search__hint">First 10 shown — select2 pages this feed 10 rows per request, username prefix match.</div>
          )}
        </div>
      )}
    </div>
  );
};

/* ==================================================================
   Balance widget — getBetData L216-227. The real page renders one pre-built balance_html string:
   "<currency> (balance + balance_withdrawable)" plus "Bonus: <getPlayerTotalBonus(id)>".
   The same response also carries the raw user_balance / user_balance_withdrawable (L202-203); they are shown
   here as the split under the combined figure, which is the only thing that makes the combined number
   auditable at a counter. Box styling matches HostDeposit.jsx's balance boxes.
   ================================================================== */
const HsbBalanceBoxes = ({ player, led, loading, spinning, onRefresh }) => {
  if (!player) return (
    <div className="hsb-empty"><Icon name="user" size={14} style={{ opacity: .5 }} /> Pick a player — the balance loads with the sportsbook session.</div>
  );
  if (loading) return <div className="hsb-empty"><Icon name="refresh" size={13} className="hsb-spin" /> Loading balance…</div>;
  return (
    <div className="hsb-balboxes">
      <div className="hsb-balbox hsb-balbox--main">
        <div className="hsb-balbox__l">
          Balance
          <button className="hsb-refresh" onClick={onRefresh} title="Refresh — re-calls GET /sport/bet/getBetData/{userid}/{sporttype}/">
            <Icon name="refresh" size={11} className={spinning ? "hsb-spin" : ""} />
          </button>
        </div>
        <div className="hsb-balbox__v"><Money amount={led.bal + led.wd} currency={HSB_ACTOR.currency} /></div>
        <div className="hsb-balbox__s">
          Non-withdrawable <b>{hsbN(led.bal)}</b> · Withdrawable <b>{hsbN(led.wd)}</b>
          <Tip size={11}>The real widget prints only the combined figure (<code>balance</code> + <code>balance_withdrawable</code>). Both parts ship in the same response as <code>user_balance</code> and <code>user_balance_withdrawable</code> — split out here so the total is checkable at the counter.</Tip>
        </div>
      </div>
      <div className="hsb-balbox">
        <div className="hsb-balbox__l">Bonus</div>
        <div className="hsb-balbox__v"><Money amount={led.bonus} currency={HSB_ACTOR.currency} /></div>
        <div className="hsb-balbox__s">PlayersController::getPlayerTotalBonus</div>
      </div>
    </div>
  );
};

/* ==================================================================
   Deposit confirmation. Real page: a browser confirm() for fast deposit
   ('Are you sure you want to reload the player with X?') and none at all for the custom box.
   Full-screen on mobile (hsb- CSS).
   ================================================================== */
const HsbConfirmModal = ({ deal, onCancel, onConfirm }) => (
  <div className="bp-modal-scrim hsb-scrim" onClick={onCancel}>
    <div className="bp-modal hsb-modal" onClick={e => e.stopPropagation()}>
      <div className="bp-modal__head">
        <div className="bp-modal__title" style={{ display: "inline-flex", alignItems: "center", gap: 10 }}>
          <Icon name="arrow_down_up" size={18} style={{ color: "var(--p-600)" }} /> {deal.kind === "fast" ? "Fast deposit" : "Custom deposit"}
        </div>
        <button className="hsb-x" onClick={onCancel}><Icon name="x" size={14} /></button>
      </div>

      <div className="hsb-sumgrid">
        <div className="hsb-sum"><div className="l">Player</div><div className="v">{deal.player.username} <span className="c">#{deal.player.id}</span></div></div>
        <div className="hsb-sum"><div className="l">Amount</div><div className="v"><Money amount={deal.amount} currency={HSB_ACTOR.currency} /></div></div>
        <div className="hsb-sum"><div className="l">From</div><div className="v">{HSB_ACTOR.username} <span className="c">{HSB_LEVELS[HSB_ACTOR.level]}</span></div></div>
        <div className="hsb-sum"><div className="l">Operation</div><div className="v">Deposit <span className="c">transfer_type add</span></div></div>
      </div>

      <ul className="hsb-notes">
        <li>Posts <code>POST /dotransfer</code> with <code>{`{from_user: ${HSB_ACTOR.id}, transfer_type: 'add', transfer_usertype: 'player', to_user: ${deal.player.id}, amount: ${deal.amount}}`}</code>.</li>
        <li><code>from_user</code> is advisory — <code>dotransfer</code> re-pins it to the authenticated user for every non-admin actor (TransferController.php:206-218), so the money always leaves <b>your</b> wallet.</li>
        <li>The engine writes two mirrored <code>transactions</code> rows (your <code>out</code> ↔ the player's <code>add</code>) plus a player-side <code>transactions_history</code> row, type <b>1 · DEPOSIT</b>. It credits <code>balance</code> — non-withdrawable.</li>
      </ul>

      <div className="hsb-modal__foot">
        <button className="btn btn--secondary hsb-fbtn" onClick={onCancel}><Icon name="x" size={13} /> Cancel</button>
        <button className="hrs-btn hrs-btn--export hsb-fbtn" onClick={onConfirm}><Icon name="check" size={13} /> Reload player</button>
      </div>
    </div>
  </div>
);

/* ==================================================================
   Sportsbook frame. The real page drops an <iframe id="sportIframe"> here, height-fitted by JS to
   window.innerHeight − header − bottom bar, reloaded by setBetPlayer() on every player / sport-type change.
   A prototype cannot open a real provider session, so the panel states exactly what production loads and
   hands over — including the two failure messages — instead of faking a sportsbook.
   ================================================================== */
const HsbSportFrame = ({ player, sport, provider, loading, loadedAt }) => {
  const type = HSB_SPORT_TYPES.find(s => s.id === sport);
  if (!player) return (
    <div className="hsb-frame hsb-frame--empty">
      <div className="hsb-frame__mid">
        <div className="hsb-frame__ico"><Icon name="user" size={20} /></div>
        <div className="hsb-frame__t">No player selected</div>
        <div className="hsb-frame__p">The sportsbook only opens inside a player's session. Pick a player on the left and the frame loads for them.</div>
      </div>
    </div>
  );
  return (
    <div className="hsb-frame">
      <div className="hsb-frame__bar">
        <span className="hsb-frame__chip"><Icon name="globe" size={11} /> {provider.name}</span>
        <span className="hsb-frame__chip">{type.label}</span>
        <span className="hsb-frame__url">/sport/bet/getBetData/{player.id}/{sport}/</span>
        <span className="hsb-frame__when">{loading ? "reloading…" : `frame loaded ${hsbClock(loadedAt)}`}</span>
      </div>
      <div className={`hsb-frame__body${loading ? " is-loading" : ""}`}>
        <div className="hsb-frame__mid">
          <div className="hsb-frame__ico"><Icon name={loading ? "refresh" : "activity"} size={20} className={loading ? "hsb-spin" : ""} /></div>
          <div className="hsb-frame__t">{loading ? "Reloading sportsbook…" : `${provider.name} sportsbook — ${type.label}`}</div>
          <div className="hsb-frame__p">
            In production this area is <code>&lt;iframe id="sportIframe"&gt;</code>, sized to the viewport and reloaded by
            <code> setBetPlayer()</code> on every player or sport-type change. It runs <b>{player.username}</b>'s own session:
            bets placed in it are the player's bets, priced in {HSB_ACTOR.currency}, debited from the balance on the left and
            settled onto the player's coupons.
          </div>
          <div className="hsb-frame__fail">
            If the provider call fails the real page renders <code>Can't load sport</code> (SportController.php:256-262) or
            <code> Sports under maintenance</code> (FrontendGamesController.php:399) in this same area — nothing is charged.
          </div>
        </div>
      </div>
    </div>
  );
};

/* ==================================================================
   Page
   ================================================================== */
const HostSportBet = () => {
  const [ledger, setLedger] = hsbUseState(() => {
    const m = {};
    HSB_PLAYERS.forEach(p => { m[p.id] = { bal: p.bal, wd: p.wd, bonus: p.bonus }; });
    return m;
  });
  // Default selection = the operator's defaultPlayer (CA_<shop>) when one exists, else nothing (placeholder "Select player").
  const [player, setPlayer] = hsbUseState(() => HSB_PLAYERS.find(p => p.ca) || null);
  const [sport, setSport] = hsbUseState(hsbSportType("")); // normalises to prematch — see the $sport_type note above
  const [custom, setCustom] = hsbUseState("");
  const [err, setErr] = hsbUseState("");
  const [deal, setDeal] = hsbUseState(null);
  const [loading, setLoading] = hsbUseState(false);
  const [spin, setSpin] = hsbUseState(false);
  const [loadedAt, setLoadedAt] = hsbUseState(() => Date.now());

  const led = player ? ledger[player.id] : null;
  const provider = HSB_PROVIDERS.find(p => p.code === HSB_SKIN.sportProvider) || HSB_PROVIDERS[0];
  const pool = hsbUseMemo(() => HSB_PLAYERS, []);

  const toast = (title, detail) => window.PAYBO?.emitToast && window.PAYBO.emitToast({
    id: `hsb-${Date.now()}`, tx_id: title, amount: 0, currency: "HOST", player: player ? player.username : "Bet from backoffice", reason: detail,
  });

  /* One getBetData call per player / sport-type change: it returns the balance AND the iframe URL, which is why
     the frame reloads whenever either changes (setBetPlayer). */
  hsbUseEffect(() => {
    if (!player) return;
    setLoading(true);
    const t = setTimeout(() => { setLoading(false); setLoadedAt(Date.now()); }, 420);
    return () => clearTimeout(t);
  }, [player ? player.id : -1, sport]);

  const refreshBalance = () => {
    if (!player) return;
    setSpin(true);
    setTimeout(() => setSpin(false), 700);
    toast("Balance refreshed", `getBetData re-read balance + balance_withdrawable + total bonus for ${player.username}. On the real platform this same call also rewrites that player's stored IP — see the session panel.`);
  };

  const applyDeposit = () => {
    const p = deal.player, amt = deal.amount;
    const cur = ledger[p.id];
    setLedger({ ...ledger, [p.id]: { ...cur, bal: cur.bal + amt } }); // transfer_type add credits `balance` (non-withdrawable)
    setDeal(null);
    setCustom(""); setErr("");
    toast("Player reloaded", `${hsbN(amt)} ${HSB_ACTOR.currency} deposited to ${p.username} — two mirrored transactions rows + a type 1 DEPOSIT history row. New balance ${hsbN(cur.bal + amt + cur.wd)} ${HSB_ACTOR.currency}.`);
  };

  const askCustom = () => {
    /* Real page: only a non-empty check here (alert "Insert amount"); the format rules below are
       TransferController's own and currently fire after the round trip. */
    if (!String(custom).trim()) { setErr("Insert amount"); return; } // backend.insert_amount
    if (!hsbValidAmount(custom)) { setErr("Incorrect amount format"); return; } // backend.incorrect_amount_format
    const amt = parseFloat(String(custom).replace(/,/g, ""));
    if (!(amt > 0)) { setErr("Amount must be greater than zero"); return; } // label inferred — the engine's own string is generic
    setErr("");
    setDeal({ kind: "custom", player, amount: amt });
  };

  return (
    <HrsShell
      title={<>Bet from backoffice <span className="hsb-inferred">label inferred</span></>}
      subtitle={<>Place a player's sportsbook bets from the counter · signed in as <b>{HSB_ACTOR.username}</b> ({HSB_LEVELS[HSB_ACTOR.level]}) · {HSB_SKIN.name}</>}
      gate={<>Real-platform access needs the skin setting <code>show_sport</code> and <b>no</b> <code>disable_bet_section</code> on the skin; Customer Care additionally needs <code>support_sport_bet</code>. Net audience: Shop, Agent, Promoter, Master. </>}
      gateNote={<>Two honesty notes. <b>The gates disagree</b>: the sidebar hides the entry from full admins and (through a dead clause) from every Customer Care user, yet <code>SportController::bet()</code> blocks neither — both reach the page by typing <code>/sport/bet/</code>. <b>The data endpoint has no gate at all</b>: <code>getBetData</code> checks only that the target is <code>user_level 30</code> — no <code>user_path</code> descent, no skin check, no permission re-check — so any authenticated back-office user can pull any player's balance and a betting iframe in that player's session by ID. The player list below is scoped to your own descendants, but that scoping is UI-only.</>}
      explainer={{
        title: "What this is, in plain English",
        bullets: [
          <span key="1"><b>These are the player's real bets.</b> The frame on the right is the sportsbook opened <i>inside the selected player's session</i> — their id, username, currency and skin. Every ticket placed here debits that player's real balance and settles onto their coupons, exactly as if they had bet from their phone.</span>,
          <span key="2"><b>Two controls, one call.</b> Player and Sport type are the whole form. Changing either re-calls <code>GET /sport/bet/getBetData/{"{userid}/{sporttype}/"}</code>, which returns the balance widget and the iframe URL together — so the frame reloads whenever the balance does.</span>,
          <span key="3"><b>Deposits go through the normal transfer engine.</b> Fast and Custom deposit both post <code>/dotransfer</code> as <code>transfer_type: add</code> — the same engine behind the Deposit screen. The money leaves your wallet and credits the player's non-withdrawable balance.</span>,
          <span key="4"><b>One divergence, on purpose.</b> On the real platform this page silently overwrites the player's stored IP with the operator's on every call. Here the terminal IP is handed to the sportsbook session and the player's own IP is left intact — see the session panel.</span>,
        ],
      }}>

      <div className="hsb">
        <div className="hsb-risk">
          <span className="hsb-risk__ic"><Icon name="alert" size={15} /></span>
          <div>
            <b>Real money, in the player's name.</b> Bets placed in this frame are attributed to {player ? <b>{player.username}</b> : "the selected player"}, not to you.
            They cannot be un-placed from here — cancellation is the Sport coupons screen's job, and only inside the skin's cancel window.
          </div>
        </div>

        <div className="hsb-grid" style={{ display: "grid", gridTemplateColumns: "360px 1fr", gap: 20, alignItems: "start" }}>

          {/* ---------------- left: player, balance, deposits ---------------- */}
          <aside className="hsb-side" style={{ display: "grid", gap: 14 }}>

            <section className="hsb-card">
              <div className="hsb-card__h">
                Player
                <Tip>select2 on <code>admin.users.search</code> (<code>GET /users2</code> → <code>UsersController::searchUsers2</code>): <code>username LIKE q%</code> (prefix, not contains), <code>user_types [30]</code>, 10 rows per request, restricted to <code>Auth::user()-&gt;getChilds(true)</code> — your own subtree.</Tip>
              </div>
              <HsbPlayerSearch pool={pool} picked={player} onPick={setPlayer} />
              {player && (
                <div className="hsb-picked">
                  <div className="hsb-picked__row"><span>Player ID</span><b>#{player.id}</b></div>
                  <div className="hsb-picked__row"><span>Currency</span><b>{HSB_ACTOR.currency}</b></div>
                  <div className="hsb-picked__row"><span>Last access</span><b>{hsbDT(player.lastAccess)}</b></div>
                  {player.ca && (
                    <div className="hsb-picked__ca">
                      <Icon name="info" size={12} /> Pre-selected: this shop's <b>default cashier player</b> (<code>default_cashier_player</code>, created as <code>CA_{HSB_ACTOR.username}</code>). When a shop has one, the real page opens on it.
                    </div>
                  )}
                </div>
              )}
            </section>

            <section className="hsb-card">
              <div className="hsb-card__h">
                Balance
                <Tip>The widget and both icons render only for non-Customer-Care operators, or Customer Care holding <code>support_player_transactions_read_only</code> (SportController.php:207-213, 226). This persona is a Shop, so they render.</Tip>
              </div>
              <HsbBalanceBoxes player={player} led={led} loading={loading} spinning={spin} onRefresh={refreshBalance} />
              {player && (
                <div className="hsb-tlinkrow">
                  <a className="hsb-tlink" href={hsbTransferPath(player.id)}
                    onClick={(e) => { e.preventDefault(); hsbGoTransfer(player.id); }}>
                    <Icon name="arrow_down_up" size={12} /> Open in Transfer
                  </a>
                  <Tip size={11}>The balance widget's transfer icon — it really opens this build's Deposit / Transfer screen, carrying <code>?from={HSB_ACTOR.id}&amp;type=player&amp;to={player.id}</code> in the URL, the same payload the real link uses (<code>route('admin.transfer')</code>). Honest caveat: that screen does not read the query yet, so the payer and target still have to be picked there.</Tip>
                </div>
              )}
            </section>

            <section className="hsb-card">
              <div className="hsb-card__h">
                Fast deposit
                <Tip>Four fixed denominations, hardcoded at SportController.php:229-236 and suffixed with the player's currency — they are not per-skin or per-currency. Each button posts <code>/dotransfer</code> after a confirmation.</Tip>
              </div>
              <div className="hsb-fast">
                {HSB_FAST_AMOUNTS.map(a => (
                  <button key={a} className="hsb-fastbtn" disabled={!player}
                    onClick={() => setDeal({ kind: "fast", player, amount: a })}>
                    <b>{a}</b> <span>{HSB_ACTOR.currency}</span>
                  </button>
                ))}
              </div>
            </section>

            <section className="hsb-card">
              <div className="hsb-card__h">
                Custom deposit
                <Tip>Free amount → the same <code>/dotransfer</code> POST. The real link label is the untranslated Italian “Trasferisci”; written in English here per the label policy.</Tip>
              </div>
              <div className="hsb-custom">
                <input className="input" value={custom} disabled={!player}
                  placeholder="Insert amount" /* backend.insert_amount */
                  onChange={e => { setCustom(e.target.value); if (err) setErr(""); }}
                  onKeyDown={e => { if (e.key === "Enter" && player) askCustom(); }} />
                <button className="hrs-btn hrs-btn--filters hsb-cbtn" disabled={!player} onClick={askCustom}>
                  <Icon name="arrow_down_up" size={13} /> Transfer
                </button>
              </div>
              {err && <div className="hsb-err" role="alert"><Icon name="alert" size={13} /> {err}</div>}
              <div className="hsb-hint">Amount rules are <code>TransferController</code>'s own: commas stripped, no whitespace, <code>^-?\d+(\.\d+)?$</code>, greater than zero. The real page checks only that the box is not empty and lets the server reject the rest.</div>
            </section>
          </aside>

          {/* ---------------- right: sport type + session + frame ---------------- */}
          <div className="hsb-main" style={{ display: "grid", gap: 14 }}>

            <section className="hsb-card">
              <div className="hsb-card__h">
                Sport type
                <Tip>The <code>sporttype</code> URL segment — the only two values the route accepts. Prematch is rewritten to <code>sport</code> before it reaches the provider (FrontendGamesController.php:368-370).</Tip>
              </div>
              <div className="hsb-seg" role="radiogroup" aria-label="Sport type">
                {HSB_SPORT_TYPES.map(s => (
                  <button key={s.id} role="radio" aria-checked={sport === s.id}
                    className={`hsb-seg__b${sport === s.id ? " is-on" : ""}`}
                    onClick={() => setSport(hsbSportType(s.id))}>
                    {s.label}
                    <span className="hsb-seg__c">{s.id}</span>
                  </button>
                ))}
              </div>

              <div className="hsb-hand">
                <div className="hsb-hand__h"><Icon name="shield" size={12} /> Session hand-off</div>
                <div className="hsb-hand__grid">
                  <span className="hsb-k">Provider</span>
                  <span className="hsb-v">
                    {provider.name} <code>{provider.ctrl}</code>
                    <Tip size={11}>Chosen by <code>skins.sport_provider</code>: empty → <code>NovusController</code>, <code>igpixel</code> → IGPixel, <code>cmswager</code> → CmsWager, <code>mondogaming</code> → MondoGaming. {HSB_SKIN.name} leaves the column empty.</Tip>
                  </span>
                  <span className="hsb-k">Opened as</span>
                  <span className="hsb-v">{player ? <>{player.username} <code>#{player.id}</code> · {HSB_ACTOR.currency} · {HSB_SKIN.name}</> : "—"}</span>
                  <span className="hsb-k">Type sent</span>
                  <span className="hsb-v"><code>{HSB_SPORT_TYPES.find(s => s.id === sport).sent}</code></span>
                  <span className="hsb-k">Player's stored IP</span>
                  <span className="hsb-v">{player ? <code>{player.ip}</code> : "—"} <span className="hsb-ok">unchanged</span></span>
                  <span className="hsb-k">Terminal IP</span>
                  <span className="hsb-v"><code>{HSB_ACTOR.ip}</code> <span className="hsb-muted">handed to the sportsbook session</span></span>
                </div>

                {/* Known-bug divergence — see the file header. The real getBetData runs
                    $row->update(['ip' => get_client_ip()]) on every call, silently replacing the player's own
                    last-login IP with the operator's. Not reproduced; stated instead. */}
                <div className="hsb-ipnote">
                  <span className="hsb-ipnote__ic"><Icon name="lock" size={13} /></span>
                  <div>
                    <b>Divergence, on purpose.</b> On the real platform every <code>getBetData</code> call — opening this page,
                    switching sport type, refreshing the balance — overwrites <code>users.ip</code> for this player with the
                    operator's address, wiping the player's own last-login IP that the Sport coupons list, the Players list and
                    fraud review all read. Here the terminal IP goes to the sportsbook session and the player's stored IP is
                    left alone. Nothing about the bet changes; the audit trail survives.
                  </div>
                </div>
              </div>
            </section>

            <HsbSportFrame player={player} sport={sport} provider={provider} loading={loading} loadedAt={loadedAt} />
          </div>
        </div>
      </div>

      {deal && <HsbConfirmModal deal={deal} onCancel={() => setDeal(null)} onConfirm={applyDeposit} />}
    </HrsShell>
  );
};

window.HostSportBet = HostSportBet;
