// NO PROD JSON API (bucket C) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /transfer/ · TransferController::index + dotransfer/processTransfer — see docs/ISYSTEM_REFERENCE.md §Batch 5 "Deposit"
/* Sidebar entry says "Deposit" (backend.deposit) but the page heading is "Transfer" (backend.transfer) —
   the two labels genuinely differ in the real admin. Sibling routes simulated here: GET /searchusertransfer
   (target select2 source, min 3 chars, username prefix match, user_path-subtree scope, 10/page username ASC,
   READ UNCOMMITTED on UserReadonly), GET /searchskinaccess/{user_id} (CC-only payer load), POST /dotransfer,
   POST /getBalance (the refresh icons). Wrapper pages /players/{id}/deposit and /users/{id}/deposit re-include
   this exact panel with the payer side hidden (player_fields=true) — they are not separate screens.

   processTransfer (TransferController.php L516-1062) is the platform-wide money-mutation engine: every
   approve flow funnels through it — withdraw requests, deposits queue, commissions, vouchers, crypto/StarsPay
   callbacks, cashback, promo triggers, legacy fapi, external API, and the cronjob GET /endpoint/payment/.

   Backend-only known bugs — real and documented in-repo, NOT reproduced here because they live inside the
   engine and are invisible to this screen's markup:
   - the engine's catch block rolls back, swallows the exception, then still writes the transactions +
     history rows and returns success (docs/api-authorization-audit-2026-07-14.md item 10);
   - no idempotency key on the engine (docs/queue-redis-migration-audit.md:41);
   - api_key is only checked to match *some* users row, not the payer/actor (L523-531);
   - LogsController::saveLog after every operation is a no-op (its body is commented out).
   Real-platform quirks intentionally not rebuilt (dead/debug-grade behavior, "no invented actions" both ways):
   cash_block → raw die("cash block da fare"); the inversion die("gestire pagina permessi, errore") for
   AFFILIATE/CC/ADMINISTRATION users holding support_transfer; hardcoded skin_codes casino24hs|playspin|
   apuestavip|argenslots integer-only comma-formatted amount inputs; the empty #giro_transazioni container
   (never populated — no transfer-history table exists on this screen); the stray ')' emitted by the
   unbalanced Blade @if (panel.blade.php:183); typo "Operation not allaaowed" (TransferController.php:95);
   third-party player deposits outside the actor's subtree (skin/user-keyed, labels resolve nowhere). */

const { useState: useStateHtf, useMemo: useMemoHtf, useEffect: useEffectHtf, useRef: useRefHtf } = React;

/* Deterministic PRNG (mulberry-style, same shape as the other Host pages) so the
   directory + balances render identically on every load. */
const HTF_LEVEL_LABEL = { 0: "Super admin", 8: "Master", 10: "Agent", 15: "Promoter", 20: "Shop", 30: "Player" };

/* Payer persona: super admin — so the admin-only pieces (payer picker, Apply deposit
   bonus, test-user targets) legitimately render. Non-admin gates are noted inline. */
/* ------------------------------------------------------------------ *
 * Live accounts. What used to be here: a hardcoded super-admin payer holding
 * 894,778,091,858.36, and a directory of eighteen invented agents, promoters,
 * shops and players with invented balances. Every balance box on this screen
 * read from that object, so the availability checks below were validating a
 * transfer against numbers nobody owned.
 *
 * The payer is now sb.me(); the directory is `networkUsers` and `players`,
 * both already scoped by row-level security to the caller's own subtree —
 * which is the same scoping isystem does with `user_path LIKE`, done by the
 * database rather than by this file.
 * ------------------------------------------------------------------ */
const htfUserRow = (r) => {
  const w = Array.isArray(r.wallet) ? (r.wallet[0] || {}) : (r.wallet || {});
  return {
    id: Number(r.id),
    username: String(r.username || ""),
    level: Number(r.user_level),
    skin: r.skin ? r.skin.name : "",
    currency: r.currency || (r.skin ? r.skin.currency : "") || "",
    /* `bal` is the non-withdrawable half and `wd` the withdrawable one — the
       two columns isystem's own export mixes up. The transfer engine moves the
       withdrawable side, so keeping them apart here is not cosmetic. */
    bal: Number(w.balance) || 0,
    wd: Number(w.balance_withdrawable) || 0,
    cr: Number(w.credits) || 0,
    bonus: Number(w.bonus) || 0,
  };
};


/* Operation matrix, exactly per dotransfer L411-413: agent → add|out|add_credits|out_credits;
   player → add|out (the validator also lists add_bonus for players, but the panel never renders it
   and the engine immediately returns "method not available" — bonus deposits from this screen are
   impossible in the real platform, so it is honestly absent here too).
   `mirror` = the counterpart row type written to `transactions` (each operation writes two mirrored rows). */
const HTF_OPS = [
  { id: "add", label: "Deposit", targets: ["agent", "player"], credit: false, mirror: "out", flow: "p2t", hist: { t: 1, name: "DEPOSIT" } },
  { id: "out", label: "Withdraw", targets: ["agent", "player"], credit: false, mirror: "add", flow: "t2p", hist: { t: 2, name: "WITHDRAW", neg: true } },
  { id: "add_credits", label: "Credit Deposit", targets: ["agent"], credit: true, mirror: "out_credits", flow: "p2t", hist: null },
  { id: "out_credits", label: "Credit Withdraw", targets: ["agent"], credit: true, mirror: "add_credits", flow: "t2p", hist: null },
];

/* Mirrors TransferController::isValidTransferAmountFormat + the client regex in
   scripts.blade.php:232-243 — commas stripped, no whitespace, ^-?\d+(\.\d+)?$. */
const htfValidAmountFormat = (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);
};

/* select2-equivalent: min 3 chars → username prefix match → 10 rows, username ASC. */
const HtfSearchSelect = ({ placeholder, pool, picked, onPick, note }) => {
  const [q, setQ] = useStateHtf(picked ? picked.username : "");
  const [open, setOpen] = useStateHtf(false);
  const wrapRef = useRefHtf(null);
  useEffectHtf(() => {
    const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);
  useEffectHtf(() => { setQ(picked ? picked.username : ""); }, [picked ? picked.id : -1]);
  const term = q.trim().toLowerCase();
  const results = term.length >= 3
    ? pool.filter(u => u.username.toLowerCase().startsWith(term)).sort((a, b) => a.username.localeCompare(b.username)).slice(0, 10)
    : [];
  return (
    <div className="htf-search" ref={wrapRef} style={{ position: "relative" }}>
      <div style={{ position: "relative" }}>
        <span style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", color: "#7e8299", display: "inline-flex" }}><Icon name="search" size={13} /></span>
        <input className="input" value={q} placeholder={placeholder}
          onFocus={() => setOpen(true)}
          onChange={e => { setQ(e.target.value); setOpen(true); if (picked) onPick(null); }}
          style={{ width: "100%", paddingLeft: 32, paddingRight: picked ? 32 : 12 }} />
        {picked && (
          <button className="htf-search__clear" onClick={() => { onPick(null); setQ(""); }} title="Clear selection"
            style={{ position: "absolute", right: 6, top: "50%", transform: "translateY(-50%)", border: 0, background: "transparent", cursor: "pointer", color: "#7e8299", display: "inline-flex", padding: 4 }}>
            <Icon name="x" size={12} />
          </button>
        )}
      </div>
      {open && !picked && (
        <div className="htf-search__drop" style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, right: 0, zIndex: 40, background: "#fff", border: "1px solid #e4e6ef", borderRadius: 8, boxShadow: "0 14px 30px -12px rgba(15,20,32,.35)", overflow: "hidden" }}>
          {term.length < 3 ? (
            <div className="htf-search__hint">Type at least 3 characters — username prefix match, scoped to the payer's hierarchy (user_path subtree).</div>
          ) : results.length === 0 ? (
            <div className="htf-search__hint">No users found.</div>
          ) : results.map(u => (
            <button key={u.id} className="htf-search__item" onClick={() => { onPick(u); setOpen(false); }}>
              <span style={{ fontWeight: 600, color: "#3f4254", display: "inline-flex", alignItems: "center", gap: 8 }}>
                {u.username}
                {u.test && <span className="htf-flag">test user</span>}
              </span>
              <span style={{ color: "#7e8299", fontSize: 12 }}>{HTF_LEVEL_LABEL[u.level]} · {u.skin}</span>
            </button>
          ))}
          {term.length >= 3 && results.length === 10 && (
            <div className="htf-search__hint">First 10 shown — the real select2 pages at 10 per request, username ASC.</div>
          )}
        </div>
      )}
      {note && <div className="htf-note">{note}</div>}
    </div>
  );
};

/* Target balance boxes = admin/transfer/balance_boxes.blade.php. Agent: Balance / Credits /
   Availability. Player: Withdrawable / Non-withdrawable / Bonus / Availability
   (availability = balance + credits + balance_withdrawable + bonus, TransferController.php:111). */
const HtfBalanceBoxes = ({ user, led, loading, onRefresh, spinning }) => {
  if (!user) return (
    <div className="htf-balempty">
      <Icon name="users" size={14} style={{ opacity: .5 }} /> Search and select a user — its balance boxes load on selection.
    </div>
  );
  if (loading) return <div className="htf-balempty"><Icon name="refresh" size={13} className="htf-spin" /> Loading balances…</div>;
  const isPlayer = user.level === 30;
  const avail = isPlayer ? led.bal + led.wd + led.bonus + led.cr : led.bal + led.cr;
  const boxes = isPlayer
    ? [["Withdrawable balance", led.wd], ["Non withdrawable balance", led.bal], ["Bonus", led.bonus]]
    : [["Balance", led.bal], ["Credits", led.cr]];
  return (
    <div className="htf-balboxes" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 10 }}>
      {boxes.map(([l, v]) => (
        <div key={l} className="htf-balbox">
          <div className="htf-balbox__l">{l}</div>
          <div className="htf-balbox__v"><Money amount={v} currency="EUR" /></div>
        </div>
      ))}
      <div className="htf-balbox htf-balbox--avail">
        <div className="htf-balbox__l" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 6 }}>
          Availability
          <button className="htf-refresh" onClick={onRefresh} title="Refresh — POST /getBalance (UsersController@getBalance)">
            <Icon name="refresh" size={11} className={spinning ? "htf-spin" : ""} />
          </button>
        </div>
        <div className="htf-balbox__v"><Money amount={avail} currency="EUR" /></div>
      </div>
    </div>
  );
};

/* Confirm + receipt modal. `review` = pre-flight summary with the two mirrored rows that WILL be
   written; `receipt` = the rows as written, with new balances. Full-screen on mobile (htf- CSS). */
const HtfConfirmModal = ({ review, receipt, onCancel, onConfirm, onClose }) => {
  const done = !!receipt;
  const data = receipt || review;
  return (
    <div className="bp-modal-scrim htf-scrim" onClick={done ? onClose : onCancel}>
      <div className="bp-modal htf-modal" onClick={e => e.stopPropagation()}>
        <div className="bp-modal__head">
          <div className="bp-modal__title" style={{ display: "inline-flex", alignItems: "center", gap: 10 }}>
            {done
              ? <><span className="htf-okdot"><Icon name="check" size={13} /></span> Transfer applied</>
              : <><Icon name="arrow_down_up" size={18} style={{ color: "var(--p-600)" }} /> Confirm transfer</>}
          </div>
          <button className="htf-x" onClick={done ? onClose : onCancel}><Icon name="x" size={14} /></button>
        </div>

        <div className="htf-sumgrid" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 10, marginBottom: 14 }}>
          <div className="htf-sum"><div className="l">Operation</div><div className="v">{data.opLabel} <span className="c">{data.op}</span></div></div>
          <div className="htf-sum"><div className="l">From (payer)</div><div className="v">{data.fromName} <span className="c">{data.fromLevel}</span></div></div>
          <div className="htf-sum"><div className="l">To (target)</div><div className="v">{data.toName} <span className="c">{data.toLevel}</span></div></div>
          <div className="htf-sum"><div className="l">Amount</div><div className="v"><Money amount={data.amt} currency="EUR" /></div></div>
          <div className="htf-sum"><div className="l">Reason</div><div className="v">{data.reason || "—"}</div></div>
        </div>
        {(data.withdrawable || data.applyBonus) && (
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 14 }}>
            {data.withdrawable && <span className="htf-flag htf-flag--on">Withdrawable → credits balance_withdrawable</span>}
            {data.applyBonus && <span className="htf-flag htf-flag--on">Apply deposit bonus</span>}
          </div>
        )}

        <div className="htf-receipt">
          <div className="htf-receipt__cap">
            {done ? "Ledger rows written — two mirrored transactions rows per operation:" : "This will write two mirrored transactions rows:"}
          </div>
          <div style={{ overflowX: "auto" }}>
            <table className="data-table htf-receipt__table">
              <thead><tr><th>Tx</th><th>Account</th><th>Side</th><th>transactions.type</th><th>Amount</th><th>{done ? "New value" : "Column"}</th></tr></thead>
              <tbody>
                {data.rows.map((r, i) => (
                  <tr key={i}>
                    <td>{r.tx ? `#${r.tx}` : "—"}</td>
                    <td style={{ textAlign: "left", fontWeight: 600 }}>{r.who}</td>
                    <td>{r.side}</td>
                    <td><span className="htf-type">{r.type}</span></td>
                    <td style={{ color: r.amt < 0 ? "#e9484a" : "#1f9d57", fontWeight: 700 }}><Money amount={r.amt} currency="EUR" sign /></td>
                    <td>{done ? <><Money amount={r.newVal} currency="EUR" /> <span className="c">{r.col}</span></> : <span className="c">{r.col}</span>}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          {data.notes && data.notes.length > 0 && (
            <ul className="htf-receipt__notes">{data.notes.map((n, i) => <li key={i}>{n}</li>)}</ul>
          )}
        </div>

        <div style={{ display: "flex", gap: 10, marginTop: 18, justifyContent: "flex-end" }} className="htf-modal__foot">
          {done ? (
            <button className="rpt-btn rpt-btn--blue" style={{ minWidth: 120 }} onClick={onClose}>Close</button>
          ) : (
            <React.Fragment>
              <button className="rpt-btn rpt-btn--reset" style={{ minWidth: 110 }} onClick={onCancel}><Icon name="x" size={13} /> Cancel</button>
              <button className="rpt-btn rpt-btn--green" style={{ minWidth: 160 }} onClick={onConfirm}><Icon name="check" size={13} /> Confirm transfer</button>
            </React.Fragment>
          )}
        </div>
      </div>
    </div>
  );
};

const HostDeposit = ({ brand }) => {
  window.useLocale && window.useLocale();

  /* Everyone this operator can pay or be paid by. Both reads are already
     subtree-scoped by RLS, so the directory cannot offer an account the
     transfer engine would then refuse. */
  const opFeed = useHrsFetch(() => window.sb.list("networkUsers", { limit: 2000 }), []);
  const plFeed = useHrsFetch(() => window.sb.list("players", { limit: 2000 }), []);
  const save = useHrsSave([opFeed, plFeed]);

  const [me, setMe] = useStateHtf(null);
  useEffectHtf(() => {
    let alive = true;
    Promise.resolve(window.sb.me()).then(r => { if (alive && r && r.ok) setMe(r.data); });
    return () => { alive = false; };
  }, []);

  const operators = useMemoHtf(() => (opFeed.data || []).map(htfUserRow), [opFeed.data]);
  const playerRows = useMemoHtf(() => (plFeed.data || []).map(htfUserRow), [plFeed.data]);

  /* The acting operator, from their own row in `users` — not a persona. */
  const actor = useMemoHtf(() => {
    if (!me) return null;
    const hit = operators.find(u => u.id === Number(me.id));
    return hit || { id: Number(me.id), username: me.username, level: Number(me.user_level),
                    skin: "", currency: me.currency || "", bal: 0, wd: 0, cr: 0, bonus: 0 };
  }, [me, operators]);

  /* Balances come from the rows, and the rows come from the server. There is
     no local ledger to mutate: after a transfer the two feeds refetch and the
     boxes show what the database committed. Simulating them here is how a
     screen ends up showing a balance that moved when the money did not. */
  const ledger = useMemoHtf(() => {
    const m = {};
    [...operators, ...playerRows].forEach(u => { m[u.id] = { bal: u.bal, wd: u.wd, cr: u.cr, bonus: u.bonus }; });
    if (actor && !m[actor.id]) m[actor.id] = { bal: actor.bal, wd: actor.wd, cr: actor.cr, bonus: actor.bonus };
    return m;
  }, [operators, playerRows, actor]);

  const [payer, setPayer] = useStateHtf(null); // from_user — defaults to the actor once it loads
  useEffectHtf(() => { if (actor && !payer) setPayer(actor); }, [actor]);
  const [usertype, setUsertype] = useStateHtf("");  // transfer_usertype — "Select" placeholder (as the placeholder-skins render); see bug note at validation
  const [op, setOp] = useStateHtf("add");           // transfer_type — required, default `add` in the real panel
  const [target, setTarget] = useStateHtf(null);    // to_user
  const [amount, setAmount] = useStateHtf("");
  const [reason, setReason] = useStateHtf("");      // causale — optional
  const [withdrawable, setWithdrawable] = useStateHtf(false);
  const [applyBonus, setApplyBonus] = useStateHtf(false);
  const [errors, setErrors] = useStateHtf([]);
  const [modal, setModal] = useStateHtf(null);      // { review } | { receipt }
  const [balLoading, setBalLoading] = useStateHtf(false);
  const [spin, setSpin] = useStateHtf("");          // "payer" | "target"

  const EMPTY_LED = { bal: 0, wd: 0, cr: 0, bonus: 0 };
  const payerLed = (payer && ledger[payer.id]) || EMPTY_LED;
  const targetLed = target ? (ledger[target.id] || EMPTY_LED) : null;

  // Payer pool (admin-only agent_suggest): self + every agent-side account.
  const payerPool = useMemoHtf(
    () => (actor ? [actor, ...operators.filter(u => u.id !== actor.id)] : operators),
    [actor, operators]);
  // Target pool per usertype: player → user_level 30; agent → below the actor.
  const targetPool = useMemoHtf(() => {
    if (usertype === "player") return playerRows;
    if (usertype === "agent") return operators.filter(u => actor && u.level > actor.level);
    return [];
  }, [usertype, playerRows, operators, actor]);

  // Ops filtered by the real matrix + "credit ops only when payer credits > 0" (panel.blade.php:149-155).
  const visibleOps = HTF_OPS.filter(o => (!usertype || o.targets.includes(usertype)) && (!o.credit || payerLed.cr > 0));
  const opDef = HTF_OPS.find(o => o.id === op);

  // select2 "balance boxes load" moment: brief deterministic load flash on target pick.
  useEffectHtf(() => {
    if (!target) return;
    setBalLoading(true);
    const t = setTimeout(() => setBalLoading(false), 350);
    return () => clearTimeout(t);
  }, [target ? target.id : -1]);

  const toast = (title, amt, who, body) => window.PAYBO?.emitToast && window.PAYBO.emitToast({
    id: `htf-${Date.now()}`, tx_id: title, amount: amt || 0, currency: "EUR", player: who || "Transfer", reason: body,
  });

  const pickUsertype = (v) => {
    setUsertype(v);
    setTarget(null);
    setWithdrawable(false); setApplyBonus(false);
    const cur = HTF_OPS.find(o => o.id === op);
    if (v && cur && !cur.targets.includes(v)) setOp("add");
  };

  const refresh = (side) => {
    setSpin(side);
    setTimeout(() => setSpin(""), 700);
    toast("Balance refreshed", 0, side === "payer" ? (payer ? payer.username : "—") : (target ? target.username : "—"),
      "POST /getBalance → UsersController@getBalance re-reads balance/credits and recomputes availability.");
  };

  const validate = () => {
    const errs = [];
    /* Until sb.me() resolves there is no paying account, so every check below
       would be measuring against an empty one. */
    if (!actor) return ["Still loading your account — try again in a moment."];
    if (!payer) errs.push("Select the paying account"); // from_user required — always set (server-pinned), kept for parity
    if (!op) errs.push("Select the operation");         // exact backend message
    /* Real-platform bug (TransferController::dotransfer L225): the transfer_usertype required-check
       re-tests $transfer_type, so an empty usertype slips through validation. Evident intent
       implemented — User Type is genuinely required here. */
    // <!-- SUGGESTION: fix dotransfer L225 to test $transfer_usertype instead of $transfer_type so the required rule actually fires. -->
    if (!usertype) errs.push("Select the user type"); // label inferred (the bugged branch never emits one)
    if (!target) errs.push("Select the user");        // to_user required — label inferred
    if (!htfValidAmountFormat(amount)) errs.push("Incorrect amount format"); // backend.incorrect_amount_format
    else if (parseFloat(String(amount).replace(/,/g, "")) <= 0) errs.push("Amount must be greater than zero"); // (float) > 0 check — label inferred
    // Matrix guard (dotransfer L411-413) — unreachable via this UI since options are filtered, kept honest:
    if (usertype && opDef && !opDef.targets.includes(usertype)) {
      errs.push(`Operation not allowed: "${op}" from "${HTF_LEVEL_LABEL[payer.level]}" to "${usertype}"`); // financial.op_not_allowed
    }
    // Availability checks (engine-side; failure labels inferred — the engine's own strings are generic):
    if (errs.length === 0 && target) {
      const amt = parseFloat(String(amount).replace(/,/g, ""));
      const t = ledger[target.id] || EMPTY_LED, p = payerLed;
      if (op === "add" && p.bal + p.cr < amt) errs.push("Insufficient availability on the paying account");
      if (op === "out" && (target.level === 30 ? t.bal + t.wd : t.bal) < amt) errs.push("Insufficient availability on the selected user");
      if (op === "add_credits" && p.cr + Math.min(0, p.bal) < amt) errs.push("Insufficient credits on the paying account"); // negative payer balance reduces spendable credits (L925-927)
      if (op === "out_credits" && t.cr < amt) errs.push("Insufficient credits on the selected user");
    }
    return errs;
  };

  const submit = () => {
    const errs = validate();
    setErrors(errs);
    if (errs.length) return;
    const amt = parseFloat(String(amount).replace(/,/g, ""));
    const col = opDef.credit ? "credits" : (withdrawable ? "balance_withdrawable" : "balance");
    const rows = opDef.flow === "p2t"
      ? [
        { who: payer.username, side: "Payer", type: opDef.mirror, amt: -amt, col: opDef.credit ? "credits" : "balance" },
        { who: target.username, side: "Target", type: opDef.id, amt: amt, col },
      ]
      : [
        { who: target.username, side: "Target", type: opDef.id, amt: -amt, col: opDef.credit ? "credits" : "balance" },
        { who: payer.username, side: "Payer", type: opDef.mirror, amt: amt, col: opDef.credit ? "credits" : "balance" },
      ];
    setModal({
      review: {
        op: opDef.id, opLabel: opDef.label, amt, reason,
        fromName: payer.username, fromLevel: HTF_LEVEL_LABEL[payer.level],
        toName: target.username, toLevel: HTF_LEVEL_LABEL[target.level],
        withdrawable: withdrawable && usertype === "player" && op === "add",
        applyBonus: applyBonus && usertype === "player" && op === "add",
        rows,
        notes: target.level === 30 && opDef.hist
          ? [<span key="h">A player-side <b>transactions_history</b> row is also written: type {opDef.hist.t} · {opDef.hist.name} (res_type 1 TRANSACTION{opDef.hist.neg ? ", amount stored negative" : ""}).</span>]
          : [],
      },
    });
  };

  /* Simulated processTransfer: both parties mutated inside one "transaction"
     (User::lockForUpdate + DB::beginTransaction in the real engine), availability
     recomputed as credits+balance afterwards. */
  const confirm = async () => {
    const r = modal.review;
    /* ONE RPC. post_transfer() does both balance updates and both mirrored
       ledger rows inside a single transaction, locking the two rows in
       ascending user id so opposing transfers queue rather than deadlock. Two
       post_transaction() calls would be two transactions with a window between
       them — an interruption there debits one account and credits none, which
       is the flaw in isystem's processTransfer this schema exists to remove.

       THE IDEMPOTENCY KEY describes WHAT is being transferred, never when. A
       key with a timestamp in it makes a double-click indistinguishable from
       two genuine transfers, which on this screen means paying an agent twice.
       Amount is included because two identical transfers to the same account
       are a real thing an operator may want — but only after the first has
       landed and the form has been cleared, at which point the operator is
       making a new decision rather than repeating a click. */
    const key = `transfer:${payer.id}:${target.id}:${opDef.id}:${r.amt}`
              + (reason ? `:${reason.trim().slice(0, 40)}` : "");

    /* Direction and wallets come from the operation, not from a sign. `add`
       pays the target; `out` pulls back from them. Credit operations move the
       credits wallet on both sides. */
    const credits = opDef.credit;
    const outbound = opDef.flow === "p2t";
    const from = outbound ? payer : target;
    const to = outbound ? target : payer;

    const res = await save.run(() => window.sb.transfer({
      fromUserId: from.id,
      toUserId: to.id,
      amount: r.amt,
      key,
      fromWallet: credits ? "credits" : "real",
      toWallet: credits ? "credits" : "real",
      /* ALWAYS 1, AND THE CONDITIONAL THIS REPLACES INVERTED EVERY WITHDRAW.

         `p_type_id` types the RECEIVING row. post_transfer applies it to
         p_to_user_id with +amount and writes the mirror — 1<->2 — on the
         paying side with -amount (015:368). So the parameter answers "what is
         this, for whoever the money arrives at", and in all four operations
         here the receiver is being credited. Money arriving is a deposit from
         its recipient's point of view, whichever direction the screen calls it.

         DIRECTION IS EXPRESSED ENTIRELY BY WHICH ACCOUNT IS `from`, twenty
         lines above: `out` sets flow t2p, which makes the target the payer.
         Passing 2 as well applied the direction a SECOND time, and the two
         flips did not cancel — they landed on different rows:

           player   type 1 DEPOSIT, amount NEGATIVE   (should be 2 WITHDRAW)
           operator type 2 WITHDRAW, amount POSITIVE  (should be 1 DEPOSIT)

         Balances were right, which is why nothing looked wrong. The reports
         were not: report_player_daily sums type 1 into `deposits`, so a
         withdrawal from a player REDUCED their deposits instead of raising
         their withdrawals — a take-back against a column that is supposed to
         be monotonic, in a figure an operator reads to decide whether an
         account is profitable. */
      typeId: 1,
      description: reason ? reason.trim() : `${opDef.label} ${payer.username} -> ${target.username}`,
    }), {
      done: `${r.opLabel} applied — ${target.username}`,
      fail: `${r.opLabel} did NOT go through`,
    });

    if (!res || !res.ok) { setModal(null); return; }

    const entries = Array.isArray(res.data) ? res.data : [];
    setModal({ receipt: { ...r,
      /* The receipt shows what the DATABASE wrote: the two ledger ids and the
         balance_after each row committed. Recomputing it here would show the
         arithmetic this screen expected rather than the arithmetic that
         happened, and those differ exactly when something went wrong. */
      rows: entries.map(e => ({
        tx: e.id,
        who: Number(e.user_id) === payer.id ? payer.username : target.username,
        side: Number(e.user_id) === payer.id ? "Payer" : "Target",
        type: Number(e.amount) < 0 ? "out" : "add",
        amt: Number(e.amount),
  // EMBED-OK: ledger_entries.wallet is a plain text column (real/bonus/credits). `wallet` is an EMBED on networkUsers, which this file also reads.
        col: e.wallet === "credits" ? "credits" : "balance_withdrawable",
        newVal: Number(e.balance_after),
      })),
      notes: r.notes,
    } });
    setAmount(""); setReason(""); setWithdrawable(false); setApplyBonus(false);
  };


  const payerAvail = payerLed.bal + payerLed.cr;
  const showPlayerChecks = usertype === "player" && op === "add";

  return (
    <div className="page report-page host-deposit">
      <div className="page__header" style={{ alignItems: "baseline", gap: 12, flexWrap: "wrap" }}>
        <div className="page__title" style={{ color: "var(--p-700)" }}>Transfer</div>
        <div style={{ fontSize: 12, color: "var(--text-tertiary, #7e8299)" }}>Sidebar entry: “Deposit” · GET /transfer/ — the nav label and page heading differ in the real admin.</div>
      </div>

      <Explainer title="What this is, in plain English"
        bullets={[
          <span key="1"><b>One engine moves all money.</b> This form posts to <code>dotransfer</code> → <code>TransferController::processTransfer</code> — the same engine every approve flow funnels through: withdraw requests, the deposits queue, commissions, vouchers, crypto/StarsPay callbacks, cashback, promo triggers, the legacy fapi, the external API and the <code>/endpoint/payment/</code> cronjob.</span>,
          <span key="2"><b>Every operation writes two mirrored <code>transactions</code> rows</b> (add↔out, add_credits↔out_credits) plus, for player targets, a <code>transactions_history</code> row (type 1 DEPOSIT / 2 WITHDRAW).</span>,
          <span key="3"><b>Who sees this screen:</b> the sidebar gate is <code>support_transfer</code> — auto-true for every role except Affiliate, Customer&nbsp;Care and Administration. CC additionally needs <code>support_player_transactions_read_only</code> or <code>support_user_transactions_read_only</code>, and is blocked by <code>support_disable_transfers</code>. Regulators get a 403.</span>,
        ]}>
        Move funds between the paying account on the left and any user in its hierarchy on the right: deposit, withdraw, or move credit lines. Balances are read from the database and refetched after every transfer, so what you see is what was committed.
      </Explainer>

      {/* One directory, and the form below cannot render without it: every
          balance box and both account pickers dereference the paying account.
          So this RETURNS rather than rendering a notice above a form that
          throws — the crash a non-returning guard produced is why. */}
      {(opFeed.error || plFeed.error)
        ? <HrsError error={opFeed.error || plFeed.error} onRetry={() => { opFeed.retry(); plFeed.retry(); }} />
        : (!actor || opFeed.loading || plFeed.loading)
          ? <HrsSkeleton rows={6} cols={3} />
          : (<>

      <div className="panel" style={{ padding: 22 }}>
        <div className="hd-transfer">
          {/* ---------- Payer (left) — hidden for CC whose parent is ADMIN-level in the real panel ---------- */}
          <aside className="hd-transfer__side">
            <div className="hd-transfer__label" style={{ display: "flex", alignItems: "center" }}>
              Paying account
              <Tip>from_user is server-pinned: <b>transfer_pins_to_self</b> pins every non-super-admin, non-CC actor to their own wallet, and the <b>restrict_transfer_to_own_wallet</b> user flag pins even earlier. For Customer Care, selecting a target loads the parent skin account as payer via <code>/searchskinaccess</code>. The picker below (agent_suggest) is admin-only.</Tip>
            </div>
            <HtfSearchSelect placeholder="Search user" pool={payerPool}
              picked={payer && actor && payer.id === actor.id ? null : payer}
              onPick={(u) => setPayer(u || actor)}
              note="Admin only — everyone else pays from their own wallet." />
            <div className="hd-transfer__label" style={{ marginTop: 14 }}>Selected user</div>
            {/* `payer` is null until the effect above copies `actor` into it, and
                `actor` is null until sb.me() resolves — so there is at least one
                frame where this renders before either exists. Unguarded, it threw
                "Cannot read properties of null (reading 'username')" and took the
                whole screen down. It never showed up signed out because the feeds
                error first and this panel is not reached at all; smoke.js pass 2
                found it on the first signed-in run. */}
            <div className="hd-transfer__sel">
              {payer
                ? <>{payer.username} <span style={{ color: "#7e8299", fontWeight: 400 }}>· {HTF_LEVEL_LABEL[payer.level]}</span></>
                : <span style={{ color: "#7e8299", fontWeight: 400 }}>Loading your account…</span>}
            </div>
            <div className="hd-transfer__label" style={{ marginTop: 14 }}>Balance</div>
            <div className="hd-transfer__bal">
              <div><span>Balance:</span><b><Money amount={payerLed.bal} currency="EUR" /></b></div>
              <div><span>Credits:</span><b><Money amount={payerLed.cr} currency="EUR" /></b></div>
              <div><span>Availability:</span><b>
                <Money amount={payerAvail} currency="EUR" />
                <button className="htf-refresh" onClick={() => refresh("payer")} title="Refresh — POST /getBalance">
                  <Icon name="refresh" size={11} className={spin === "payer" ? "htf-spin" : ""} />
                </button>
              </b></div>
            </div>
          </aside>

          {/* ---------- Target + operation (right) ---------- */}
          <div className="hd-transfer__form">
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 14 }}>
              <div>
                <label className="form-label" style={{ display: "flex", alignItems: "center" }}>
                  User Type
                  <Tip>transfer_usertype. In the real panel “Agent” is hidden for Shop-level actors and forced off for CC whose parent is Shop-level; “Player” requires <code>support_player_transactions_read_only</code> for CC. This persona is a super admin, so both render.</Tip>
                </label>
                <select className="select" value={usertype} onChange={e => pickUsertype(e.target.value)} style={{ width: "100%" }}>
                  <option value="">Select</option>
                  <option value="agent">Agent</option>
                  <option value="player">Player</option>
                </select>
              </div>
              <div>
                <label className="form-label" style={{ display: "flex", alignItems: "center" }}>
                  Operation Type
                  <Tip>transfer_type — the matrix is real: agent targets allow Deposit / Withdraw / Credit Deposit / Credit Withdraw; player targets only Deposit / Withdraw. Credit operations render only for admin, skin admin, administration, agent or promoter actors <b>and</b> when the payer has credits &gt; 0. The engine's enum also holds <code>add_bonus</code> (“Bonus Deposit”) but it returns “method not available” — bonus deposits from this screen are impossible in the real platform.</Tip>
                </label>
                <select className="select" value={op} onChange={e => setOp(e.target.value)} style={{ width: "100%" }}>
                  {visibleOps.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
                </select>
              </div>
            </div>

            <div style={{ marginTop: 14 }}>
              <label className="form-label">Search user</label>
              <HtfSearchSelect placeholder={usertype ? "Search user (min 3 characters)" : "Select a user type first"} pool={targetPool}
                picked={target} onPick={setTarget}
                note="GET /searchusertransfer — username prefix match inside the payer's user_path subtree, 10 per page, username ASC." />
            </div>

            <div style={{ marginTop: 14 }}>
              <HtfBalanceBoxes user={target} led={targetLed} loading={balLoading}
                onRefresh={() => refresh("target")} spinning={spin === "target"} />
            </div>

            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 14, marginTop: 14 }}>
              <div>
                <label className="form-label" style={{ display: "flex", alignItems: "center" }}>
                  Amount
                  <Tip>Text input in the real panel: commas are stripped, whitespace is rejected, then it must match <code>^-?\d+(\.\d+)?$</code> and be &gt; 0. Failing the format check shows “Incorrect amount format”.</Tip>
                </label>
                <input className="input" value={amount} onChange={e => setAmount(e.target.value)} placeholder="Amount" style={{ width: "100%" }} />
              </div>
              <div>
                <label className="form-label" style={{ display: "flex", alignItems: "center" }}>
                  Reason
                  <Tip>causale — optional free text. Customer-Care submissions get “ Made by Operator: &lt;username&gt;” appended server-side.</Tip>
                </label>
                <input className="input" value={reason} onChange={e => setReason(e.target.value)} placeholder="Reason" style={{ width: "100%" }} />
              </div>
            </div>

            {showPlayerChecks && (
              <div style={{ display: "flex", gap: 22, flexWrap: "wrap", marginTop: 14 }}>
                <label className="htf-check">
                  <input type="checkbox" checked={withdrawable} onChange={e => setWithdrawable(e.target.checked)} />
                  <span>Withdrawable</span>
                  <Tip>Hardcoded-English label in the real panel. Shown only for Player + Deposit, to admins — or skin admins whose skin carries the <code>enable_withdrawable_deposit</code> setting. Credits <code>balance_withdrawable</code> instead of <code>balance</code>.</Tip>
                </label>
                <label className="htf-check">
                  <input type="checkbox" checked={applyBonus} onChange={e => setApplyBonus(e.target.checked)} />
                  <span>Apply deposit bonus</span>
                  <Tip>Admin only. After a successful transfer, a synthetic Order (payment_method <code>bo</code>, token <code>bo-&lt;tx&gt;</code>) runs through ApprovedDepositService — promo-code pipeline first, auto deposit bonus if none fired. Failures are non-blocking.</Tip>
                </label>
              </div>
            )}

            {errors.length > 0 && (
              <div className="htf-errs" role="alert">
                <Icon name="alert" size={14} style={{ flexShrink: 0, marginTop: 1 }} />
                <div>{errors.map((e, i) => <div key={i}>{e}</div>)}</div>
              </div>
            )}

            <button className="rpt-btn rpt-btn--blue" style={{ width: "100%", marginTop: 18, height: 42 }} onClick={submit}>
              <Icon name="arrow_down_up" size={14} /> TRANSFER
            </button>
          </div>
        </div>
      </div>

      {modal && (
        <HtfConfirmModal review={modal.review} receipt={modal.receipt}
          onCancel={() => setModal(null)} onClose={() => setModal(null)} onConfirm={confirm} />
      )}
          </>)}
    </div>
  );
};

window.HostDeposit = HostDeposit;
