// Represents: GET /withdrawmethods/ · WithdrawMethodsController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Withdrawal methods"
/* ====================================================================
   WITHDRAWAL METHODS — Settings ▾ rebuild (Batch 3), Hrs* shell
   ====================================================================
   Real screen: `GET /withdrawmethods/` (unnamed route, routes/admin.php
   L1382-1384) → WithdrawMethodsController::index (L88-96) + ::getWithdrawmethods
   Table (L109-189, DataTables JSON) + ::withdrawmethodForm (L206-223, modal HTML)
   + ::saveWithdrawmethod (L233-304) + ::delete (L307-318, GET).
   Blade: admin/withdrawmethods/index.blade.php, modals/withdrawmethod.blade.php,
   forms/withdrawmethod.blade.php; JS driver public/js/pages/withdrawmethods/ajax.js.

   TWO-TABLE MODEL (identical to Deposit methods):
   - `withdraw_methods` — the SUPER-ADMIN GLOBAL CATALOG this screen edits:
     id, name, method_code (indexed), description (mediumtext), img,
     addedTime/updateTime (epoch ints). Model fillable: name, method_code,
     description, img.
   - `skin_withdraw_methods` — PER-SKIN enablement + money rules, keyed
     (skin_id, withdraw_id): min_with, max_with, limit_day/week/month,
     bo_status, limited (+ fee_pct, currency, auto_approve_under added later
     and written only by the modern Payments admin). Edited on a DIFFERENT
     screen — `/skins/{id}/withdrawmethods/` (SkinsController::showSkin
     WithdrawMethods L2185-2202, saved through saveEditSkin case
     "withdrawmethods" L3591-3628 → updateSkinWithdraws L2344-2365).
     Enablement semantics: ROW EXISTS = method visible to the frontend;
     bo_status=1 additionally shows it on the BO shop withdraw page
     (getWithdrawMethods with is_bo=1); limited=1 opts the method into the
     day/week/month caps enforced by PaymentLimitService (cache-backed).
   This page therefore renders the catalog as the editable surface and the
   per-skin rows READ-ONLY inside each row's expander — it never duplicates
   the skin tab's editor (that lives in HostSkins.jsx).

   THE `voucher` METHOD_CODE: VouchersController::saveNewVoucher (L340-358)
   and the legacy FrontendApiController (L925) call
   WithdrawMethodsController::getInfoSkinMethod($skin_id, "voucher").
   Voucher creation hard-fails with "Method not enabled" unless a
   skin_withdraw_methods row exists for the catalog row whose method_code is
   exactly `voucher`, and that row's min_with/max_with bound the voucher
   amount the operator can issue. Renaming the row is harmless; changing or
   deleting its CODE silently breaks voucher creation platform-wide. The
   screen states this on the row, in the expander and in the delete dialog —
   the per-skin ranges shown for `voucher` are the same numbers
   HostVouchers.jsx enforces (Play365Vivo EUR 10–1000, AcarayBets ARS
   1 000–500 000), so the two screens agree. The row chip, the expander note
   and the mobile card flag all NAVIGATE to that screen (`host-vouchers` →
   /vouchers) via hsw2GoToVouchers — it exists in this prototype, so the
   dependency is a working link rather than prose about one.

   Logo field honesty: the "Choose file" button opens a real file picker with
   the form's own accept list (.png/.jpg/.jpeg) and shows the file that was
   actually chosen; it used to invent a filename and toast "Logo attached"
   with no file involved. Nothing is uploaded — the field hint says so and
   names the real multipart endpoint.

   Faithful absences (nothing added — brief §3):
   - NO KPI strip and NO totals: the real index renders neither.
   - NO export: the screen has none (no DataTables Buttons configured).
   - NO bulk actions, NO status column: the in-controller enums stati()
     (0=Disabled / 1=Active, L98-107) and tipologieWithdrawmethods()
     (default / slick, L192-204) are dead code with no callers anywhere, and
     `withdraw_methods` has no status/type column — so none is rendered.
     // <!-- SUGGESTION: delete stati(), tipologieWithdrawmethods() and
     //      getWithdrawmethodsList() — three dead methods, plus the unused
     //      BulletProof / ImageUploaderException imports and the $max_images
     //      = 6 the form view never reads. -->
   - NO date filter: the index loads datepicker assets (and passes the
     copy-pasted option `withdrawmethod: "it"`, ajax.js:120) but renders no
     date input. Not invented here.
   - NO rich-text description: index.blade.php L135-136 loads TinyMCE from
     cdnjs and never initialises it — the field is a plain textarea, so it
     stays a plain textarea here.
     // <!-- SUGGESTION: drop the TinyMCE + datepicker CDN bundles from
     //      withdrawmethods/index.blade.php — the page loads ~300KB of
     //      third-party JS it never calls. -->
   - NO Reset button: ajax.js:108-115 wires a `#kt_reset` handler to a button
     the blade never renders. Only the shared kit's active-filter pills /
     "Clear all" (presentation, not a server workflow) are offered.
     // <!-- SUGGESTION: render the Reset button the JS already handles, or
     //      remove the dead handler. -->

   Divergences implemented as evident intent (build policy: implement the
   intent, record the divergence):
   1. PER-SKIN SAVE WIPES COLUMNS. updateSkinWithdraws (SkinsController
      L2344-2365) DELETES every skin_withdraw_methods row for the skin and
      re-INSERTS only min_with/max_with/limit_day/limit_week/limit_month/
      bo_status/limited — so fee_pct, currency and auto_approve_under
      (written by AdminPaymentsController L230-248 / PaymentMethodService
      L286-312) are silently destroyed on every legacy skin-tab save. The
      expander here models the intent — those three survive — and marks them
      as the at-risk group so the operator can see what the legacy save eats.
      // <!-- SUGGESTION: make updateSkinWithdraws upsert on
      //      (skin_id, withdraw_id) instead of delete-all + re-insert; today
      //      one save on the legacy Skins → Withdrawal methods tab resets
      //      every fee, currency and auto-approve threshold configured in
      //      the Payments admin, with no warning and no audit trail. -->
   2. CREATE DISCARDS THE NEW ID. saveWithdrawmethod L294 evaluates
      `WithdrawMethod::create($datip)->id;` and throws the result away, so
      the success JSON returns the request's empty `id` (the table reload
      masks it). Here a created row gets its real id and is flagged NEW.
      // <!-- SUGGESTION: return the insert id in params/record_data so the
      //      caller can deep-link the new row instead of reloading blind. -->
   3. DELETE ORPHANS THE SKIN ROWS. delete() (L307-318) hard-deletes the
      catalog row with no FK/cascade and no cleanup, leaving dangling
      skin_withdraw_methods rows, and never flushes the PaymentLimitService
      caches keyed by method_code. The dialog here states exactly what will
      break and the prototype removes the dependent skin rows with the
      catalog row.
      // <!-- SUGGESTION: cascade-delete (or block on) skin_withdraw_methods
      //      references, flush the payment_withdraw_* caches per method_code,
      //      refuse to delete the row whose method_code is `voucher`, and make
      //      the endpoint a POST/DELETE — it is a plain GET behind a JS
      //      confirm today, so a prefetch or a shared link can delete a
      //      payment method. -->
   4. NO UNIQUENESS ON method_code / name. Nothing at any layer stops two
      catalog rows sharing a code, and getWithdrawMethods (L54-85) keys its
      result map by method_code without dedup — the last row wins and the
      other becomes unreachable. The form warns inline (non-blocking, exactly
      like the server, which would accept it); the mock catalog ships the
      real-world shape of this defect: a legacy "Crypto" row and the live
      "crypto" row both carrying method_code `crypto`.
      // <!-- SUGGESTION: add a unique index on withdraw_methods.method_code
      //      and a `unique` validation rule in saveWithdrawmethod. -->
   5. PAGER TOTALS. getWithdrawmethodsTable reports iTotalDisplayRecords =
      the FILTERED count for both totals (L160, L180-187) and hardcodes
      sEcho=0, so DataTables' "of N entries" lies while a filter is on. The
      pager here reports the filtered set against the real catalog size.
      // <!-- SUGGESTION: send the unfiltered count as iTotalRecords and echo
      //      back the real sEcho; also return a Response instead of a bare
      //      json_encode echo. -->
   6. IMAGE VALIDATION MESSAGE. The `image` rule runs inside a try/catch
      (L265-279), so a bad upload surfaces the generic "The given data was
      invalid." attached to the img field. A field-specific message is used.
      // <!-- SUGGESTION: validate img outside the catch and return the
      //      field's own message. -->
   7. `img_remove` IS NEVER READ. The form posts a hidden img_remove flag the
      controller ignores, so clearing a logo in the real UI does nothing.
      Removing the logo here actually clears it.
      // <!-- SUGGESTION: honour img_remove in saveWithdrawmethod (and delete
      //      the orphaned file from storage). -->
   8. HARDCODED ITALIAN. Modal fallback title "Nuovo withdrawmethod"
      (modals/withdrawmethod.blade.php:8), delete confirm "Elimina metodo di
      prelievo", edit-title prefix "Modifica" (ajax.js:42-45). English used.
   9. LOGO IS FAKE-REQUIRED. The view marks it with obbligatorio() but the
      backend only validates it when a file is actually uploaded — shown as
      optional here.
      // <!-- SUGGESTION: drop the obbligatorio() marker or make img truly
      //      required; today the asterisk lies. -->
   Not implementable in a prototype, recorded only:
   - ajax.js:39 builds the row actions from `full['ID']`, i.e. from the
     ENGLISH label of __('backend.id'). Any runtime lang override of that key
     silently kills the Actions column.
     // <!-- SUGGESTION: key the row payload on a stable field name, not on a
     //      translated column label. -->
   - Uploads land on the Laravel disk (storage/app/public/withdraw/img) while
     display URLs come from config('media.WITHDRAW_IMG_WEB_PATH') =
     MEDIA_DOMAIN . "/withdraw/img/" — serving depends on the media domain
     mapping to that storage path (UNCLEAR from the repo).

   Permission honesty (surfaced in the title Tip): there is NO
   checkUserBoPerm on this screen. The sidebar entry sits inside
   @if (isadmin()) (sidebar.blade.php:522, group gate L504) = super admin,
   user_level 0 — but server-side ONLY delete() re-checks isadmin()
   (L308). index / table / form / save are protected by nothing beyond the
   shared admin route middleware, so any authenticated 2FA'd backoffice user
   who knows the URL can list, create and edit payment methods.
     // <!-- SUGGESTION: add the isadmin() check to index,
     //      getWithdrawmethodsTable, withdrawmethodForm and
     //      saveWithdrawmethod — hiding the link is not authorisation. -->

   Labels: every key this screen uses resolves in public/default-lang/en/
   backend.php (withdrawal_methods:60, new_method:135, id:82, name:83,
   code:136, description:137, logo:138, actions:495, save:227, close:425,
   minimum_withdrawal:393, maximum_withdrawal:394, active:386, active_bo:387,
   limited:388). The three per-skin limit keys (backend.limit_day /
   limit_week / limit_month) resolve NOWHERE in committed lang files —
   those are marked "label inferred" at their use sites, as are the three
   Payments-admin columns, which the legacy skin tab never labels at all.

   Mock data: deterministic (seeded PRNG) so the catalog and its per-skin
   rows render identically on every load. Method codes are the real ones the
   reference documents (bank / pix / crypto / voucher) plus the codes the
   sibling prototypes already use on withdrawal_requests (wire-argentina,
   online, cripten); the two 123Hub/VamosPago codes are mocked, matching the
   convention set in HostWithdraws.jsx.
   ==================================================================== */

const { useState: useStateHsw, useMemo: useMemoHsw } = React;

const hswToast = (m, isErr) => window.PAYBO?.emitToast && window.PAYBO.emitToast({
  id: `hsw-${Date.now()}`, tx_id: m, amount: 0, currency: isErr ? "ERR" : "HOST",
  player: "Withdrawal methods", reason: isErr ? "Fix this before continuing." : "Prototype state only \u2014 not persisted.",
});

/* Cross-page navigation to the Vouchers screen (`host-vouchers` → /vouchers),
   which this prototype really has — same pushState + PopStateEvent convention
   the rest of the app uses (see HostCmsGameTaxonomy / HostDashboard). The
   dependency is real on the platform too: VouchersController::saveNewVoucher
   resolves the catalog row whose method_code is `voucher` and uses its per-skin
   min_with/max_with as the voucher amount bounds, so the operator wants to get
   there from this row. Returns false and stays put if the route is unknown —
   it never claims to have navigated. */
const hsw2GoToVouchers = () => {
  try {
    const path = window.pathForActive && window.pathForActive("host-vouchers");
    if (!path) return false;
    if (window.location.pathname !== path) window.history.pushState({ active: "host-vouchers" }, "", path);
    window.dispatchEvent(new PopStateEvent("popstate"));
    return true;
  } catch (_e) { return false; }
};

/* decimal(10,2) / decimal(18,2) columns — always two decimals, like the DB. */
const hswAmt = (n) => Number(n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const hswPad2 = (n) => String(n).padStart(2, "0");
/* addedTime / updateTime are epoch ints on this table. */
const hswFmtTs = (ts) => { const d = new Date(ts); return `${hswPad2(d.getDate())}/${hswPad2(d.getMonth() + 1)}/${d.getFullYear()} ${hswPad2(d.getHours())}:${hswPad2(d.getMinutes())}`; };
/* altnome($name) — the filename slug saveWithdrawmethod builds before uniqid(). */
const hswSlug = (s) => String(s).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");

const HSW_VOUCHER_CODE = "voucher";

/* HSW_SKINS / HSW_SKIN_BY lived here: seven invented skins with a currency and
   a scale factor, used to make the mock amounts read plausibly per tenant. Both
   became unreferenced when the per-skin expander was wired to
   skinPaymentMethods, and stayed in the file — which is how a screen ends up
   with a list of tenants that resembles the real one and answers to nothing.
   The skins now come from the `skin:skins!skin_id(id,name,currency)` embed.

   hswRng and hswTs went the same way: defined, never called, left over from the
   generators they used to feed. */

/* ---- withdraw_methods (global catalog) ----
   ids follow the real ones seen on the platform. Rows 14 + 15 deliberately
   share method_code `crypto`: nothing prevents it (divergence 4) and it is
   why the per-skin tab lists both a dormant "Crypto" and a live "crypto". */
/* ---------- the catalogue --------------------------------------------------
   Was eight invented withdrawal methods with fabricated descriptions and
   hand-written timestamps. Now `payment_methods` filtered to flow =
   'withdrawal' — the SAME table the deposit screen reads, split by flow.

   isystem keeps deposit_methods and withdraw_methods as two near-identical
   tables, and they have already drifted: the withdrawal one has no description
   column, so its own screen renders a field that cannot exist. One table with a
   `flow` discriminator cannot drift from itself. */
const hswRow = (m) => ({
  id: m.id,
  name: m.name,
  code: m.code,
  desc: m.description,
  img: m.logo_url,
  added: m.created_at ? Date.parse(m.created_at) : null,
  updated: m.created_at ? Date.parse(m.created_at) : null,
});

/* Which skins carry a skin_withdraw_methods row for each catalog id.
   Row existence = "Active" (front office) — there is no status column. */
/* HSW_ENABLED removed with the generator it fed. */

/* Base amounts before the skin's currency scale (min, max, day, week, month). */
/* HSW_BASE removed with the generator it fed. */

/* fee_pct / currency / auto_approve_under exist on skin_withdraw_methods but
   are written ONLY by the modern Payments admin — and destroyed by the legacy
   skin-tab save (divergence 1). Seeded here so the loss is visible. */
/* HSW_PAY_ADMIN removed with the generator it fed. */

/* Per-skin enablement, keyed by method id — was a seeded RNG over a
   hand-written enablement map. Now skin_payment_methods, grouped in the
   component. */

/* Filters — exactly the real pair (index.blade.php L66-72, controller L131-137).
   Applied on Search, like the real #kt_search handler. */
const HSW_FIELDS = [
  { key: "id", label: "ID", type: "number", icon: "tag", placeholder: "Exact id…", width: 190,
    tip: <>Exact match on <code>withdraw_methods.id</code> (controller L131-133) — not a range and not a partial match.</> },
  { key: "name", label: "Name", type: "text", icon: "search", placeholder: "Any part of the name…", grow: true,
    tip: <>Matches anywhere in the name — the server applies <code>name LIKE '%value%'</code> (controller L135-137). There is no filter on <code>method_code</code>, even though it is the indexed join key.</> },
];
const HSW_EMPTY_F = { id: "", name: "" };

/* ------------------------------------------------------------------ */
/* Row bits                                                            */
/* ------------------------------------------------------------------ */

const HswCodeChip = ({ code, dup }) => (
  <span className={`hsw-code${code === HSW_VOUCHER_CODE ? " hsw-code--voucher" : ""}${dup ? " hsw-code--dup" : ""}`}>{code}</span>
);

const HswFlag = ({ on, yes = "Yes", no = "No" }) => (
  <span className={`hsw-flag${on ? " hsw-flag--on" : ""}`}>{on ? yes : no}</span>
);

/* Read-only mirror of `/skins/{id}/withdrawmethods/` for one catalog row. */
const HswDetail = ({ m, rows, dupOf }) => (
  <div className="hsw-detail">
    <div className="hsw-detail__title">
      Per-skin enablement — <code>skin_withdraw_methods</code>
      <Tip>
        This catalog screen edits the global row only. Min/max, the three limits and the two switches are per-skin
        and are edited on <code>{"/skins/{id}/withdrawmethods/"}</code> (Skins → Withdrawal methods), a different
        controller — <code>SkinsController::showSkinWithdrawMethods</code>. Shown read-only here so the catalog row
        can be judged in context.
      </Tip>
    </div>
    <div className="hsw-detail__sub">
      A row existing at all is what the real platform calls <b>Active</b> — there is no status column.
      <b> Active on BO</b> (<code>bo_status</code>) additionally shows the method on the backoffice shop withdraw page;
      <b> Limited</b> opts it into the day/week/month caps enforced by <code>PaymentLimitService</code>.
    </div>

    {dupOf != null && (
      <div className="hsw-note hsw-note--warn">
        <Icon name="alert" size={13} />
        <span>
          Code <code>{m.code}</code> is also used by catalog row <b>#{dupOf}</b>. <code>getWithdrawMethods()</code> keys its
          result map by <code>method_code</code> without deduplication (L54-85), so only one of the two rows is ever
          reachable by the withdraw forms — the later one wins.
        </span>
      </div>
    )}

    {m.code === HSW_VOUCHER_CODE && (
      <div className="hsw-note hsw-note--voucher">
        <Icon name="receipt" size={13} />
        <span>
          <b>The Voucher screen depends on this row.</b> <code>VouchersController::saveNewVoucher</code> calls
          <code> getInfoSkinMethod($skin_id, "voucher")</code> and aborts with <i>“Method not enabled”</i> when the skin has
          no row below; when it does, <code>min_with</code>/<code>max_with</code> are the exact bounds the create-voucher
          form enforces{rows.length > 0 ? <> — today {rows.map((r, i) => <React.Fragment key={r.skin}>{i > 0 && ", "}<b>{r.skin} {hswAmt(r.min)}–{hswAmt(r.max)} {r.cur}</b></React.Fragment>)}</> : null}.
          {/* Real navigation — the Vouchers screen exists in this prototype at /vouchers. */}
          <span style={{ display: "block", marginTop: 8 }}>
            <button className="hsw-btn hsw-btn--ghost hsw-btn--sm" onClick={hsw2GoToVouchers}>
              <Icon name="receipt" size={12} /> Open Vouchers
            </button>
          </span>
        </span>
      </div>
    )}

    {rows.length === 0 ? (
      <div className="hsw-detail__empty">
        Not enabled on any skin. With no <code>skin_withdraw_methods</code> row the method is invisible to the player
        frontend and to the BO withdraw form — the catalog row exists but nothing can use it.
      </div>
    ) : (
      <div className="hsw-subwrap">
        <table className="hsw-sub">
          <thead>
            <tr>
              <th>Skin</th>
              <th className="hsw-r">Minimum withdrawal</th>
              <th className="hsw-r">Maximum withdrawal</th>
              <th className="hsw-r">Limit day{/* label inferred — backend.limit_day resolves in no committed lang file */}</th>
              <th className="hsw-r">Limit week{/* label inferred — backend.limit_week */}</th>
              <th className="hsw-r">Limit month{/* label inferred — backend.limit_month */}</th>
              <th className="hsw-c">Active on BO</th>
              <th className="hsw-c">Limited</th>
              <th className="hsw-r hsw-risk">Fee %{/* label inferred — no lang key; the legacy skin tab never renders it */}</th>
              <th className="hsw-c hsw-risk">Currency{/* label inferred */}</th>
              <th className="hsw-r hsw-risk">Auto-approve under{/* label inferred */}</th>
            </tr>
          </thead>
          <tbody>
            {rows.map(r => (
              <tr key={r.skin}>
                <td className="hsw-sub__skin">{r.skin}</td>
                <td className="hsw-r">{hswAmt(r.min)} <span className="hsw-cur">{r.cur}</span></td>
                <td className="hsw-r">{hswAmt(r.max)} <span className="hsw-cur">{r.cur}</span></td>
                <td className={`hsw-r${r.limited ? "" : " hsw-muted"}`}>{hswAmt(r.day)}</td>
                <td className={`hsw-r${r.limited ? "" : " hsw-muted"}`}>{hswAmt(r.week)}</td>
                <td className={`hsw-r${r.limited ? "" : " hsw-muted"}`}>{hswAmt(r.month)}</td>
                <td className="hsw-c"><HswFlag on={r.bo} /></td>
                <td className="hsw-c"><HswFlag on={r.limited} /></td>
                <td className="hsw-r hsw-risk">{r.fee ? `${r.fee.toFixed(2)}%` : <span className="hsw-muted">—</span>}</td>
                <td className="hsw-c hsw-risk">{r.feeCur || <span className="hsw-muted">—</span>}</td>
                <td className="hsw-r hsw-risk">{r.auto ? hswAmt(r.auto) : <span className="hsw-muted">—</span>}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    )}

    {rows.length > 0 && (
      <div className="hsw-note hsw-note--risk">
        <Icon name="alert" size={13} />
        <span>
          The three shaded columns are written only by the modern Payments admin. Saving the legacy
          Skins → Withdrawal methods tab runs <code>updateSkinWithdraws</code>, which deletes every row for the skin and
          re-inserts only the seven fields that form posts — <b>wiping fee %, currency and auto-approve for every method
          on that skin</b>. This prototype keeps them (see divergence 1 in the file header).
        </span>
      </div>
    )}

    {rows.some(r => !r.bo) && (
      <div className="hsw-note">
        <Icon name="info" size={13} />
        <span>
          <b>{rows.filter(r => !r.bo).map(r => r.skin).join(", ")}</b>: the row exists (so the player frontend offers the
          method) but <code>bo_status = 0</code>, so <code>getWithdrawMethods(…, is_bo = 1)</code> hides it from the
          backoffice shop withdraw page.
        </span>
      </div>
    )}
  </div>
);

/* ------------------------------------------------------------------ */
/* Modals — full-screen on mobile (brief §11)                          */
/* ------------------------------------------------------------------ */

const HswModal = ({ title, sub, onClose, children, footer, wide }) => (
  <div className="bp-modal-scrim hsw-scrim" onClick={onClose}>
    <div className={`bp-modal hsw-modal${wide ? " hsw-modal--wide" : ""}`} onClick={e => e.stopPropagation()}>
      <div className="hsw-modal__head">
        <div>
          <div className="hsw-modal__title">{title}</div>
          {sub && <div className="hsw-modal__sub">{sub}</div>}
        </div>
        <button className="hsw-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="hsw-modal__body">{children}</div>
      {footer && <div className="hsw-modal__foot">{footer}</div>}
    </div>
  </div>
);

/* Create / edit — the four real fields of forms/withdrawmethod.blade.php.
   Titles are English (real ones are the Italian "Nuovo withdrawmethod" /
   "Modifica …" — divergence 8). */
const HswFormModal = ({ row, catalog, onClose, onSave }) => {
  const isNew = row.id == null;
  const [name, setName] = useStateHsw(row.name || "");
  const [code, setCode] = useStateHsw(row.code || "");
  const [desc, setDesc] = useStateHsw(row.desc || "");
  const [img, setImg] = useStateHsw(row.img || "");
  const [pick, setPick] = useStateHsw("");   // the file the operator really chose
  const [err, setErr] = useStateHsw({});
  const fileRef = React.useRef(null);

  const trimmed = code.trim().toLowerCase();
  const clash = catalog.find(m => m.id !== row.id && m.code.toLowerCase() === trimmed && trimmed !== "");
  const dropsVoucher = row.code === HSW_VOUCHER_CODE && trimmed !== HSW_VOUCHER_CODE;

  const submit = () => {
    /* Inline validation mirrors saveWithdrawmethod L250-279 — same three
       required fields, same messages (backend.insert_name / insert_code /
       insert_description). img is validated only when a file is attached. */
    const e = {};
    if (!name.trim()) e.name = "Insert name";
    if (!code.trim()) e.code = "Insert code";
    if (!desc.trim()) e.desc = "Insert description";
    setErr(e);
    if (Object.keys(e).length) { hswToast("Correct the following errors", true); return; }
    onSave({ ...row, name: name.trim(), code: trimmed, desc: desc.trim(), img });
  };

  return (
    <HswModal
      title={isNew ? "New method" : `Edit ${row.name}`}
      sub={isNew ? "Creates a row in the global withdraw_methods catalog. Per-skin limits are set afterwards on each skin's Withdrawal methods tab." : `withdraw_methods #${row.id} · created ${hswFmtTs(row.added)}`}
      onClose={onClose}
      footer={<>
        <button className="hsw-btn hsw-btn--ghost" onClick={onClose}>Close</button>
        <button className="hsw-btn hsw-btn--primary" onClick={submit}><Icon name="check" size={13} /> Save</button>
      </>}
    >
      <div className="hsw-field">
        <label className="hsw-label">Name <span className="hsw-req">*</span></label>
        <input className={`hsw-input${err.name ? " hsw-input--err" : ""}`} value={name} onChange={e => setName(e.target.value)} placeholder="Transferencia Bancaria" />
        {err.name && <div className="hsw-err">{err.name}</div>}
        <div className="hsw-hint">Shown to players on the withdraw form and in the Withdraw requests Method filter. Free text — no uniqueness rule at any layer.</div>
      </div>

      <div className="hsw-field">
        <label className="hsw-label">
          Code <span className="hsw-req">*</span>
          <Tip>
            <code>withdraw_methods.method_code</code> — the indexed join key every payment flow uses:
            <code> getWithdrawMethods()</code> keys its map by it, <code>PaymentLimitService</code> caches by it, and
            <code> getInfoSkinMethod($skin_id, "voucher")</code> looks the voucher method up by it. Changing it on a live
            row silently re-points (or breaks) every consumer.
          </Tip>
        </label>
        <input className={`hsw-input hsw-input--mono${err.code ? " hsw-input--err" : ""}`} value={code} onChange={e => setCode(e.target.value)} placeholder="bank" />
        {err.code && <div className="hsw-err">{err.code}</div>}
        {clash && (
          /* Non-blocking, exactly like the server: there is NO unique rule and
             no unique index, so this save would succeed (divergence 4). */
          <div className="hsw-warnline">
            <Icon name="alert" size={12} />
            <span>Code <code>{trimmed}</code> is already used by <b>#{clash.id} {clash.name}</b>. The backend accepts this — but only one of the two rows will ever be reachable, because the lookup map is keyed by code.</span>
          </div>
        )}
        {dropsVoucher && (
          <div className="hsw-warnline hsw-warnline--hard">
            <Icon name="alert" size={12} />
            <span>This row currently carries <code>voucher</code>. Changing its code makes every voucher creation fail with <i>“Method not enabled”</i>, on every skin, immediately.</span>
          </div>
        )}
      </div>

      <div className="hsw-field">
        <label className="hsw-label">Description <span className="hsw-req">*</span></label>
        {/* Plain textarea: index.blade.php loads TinyMCE and never initialises it. */}
        <textarea className={`hsw-textarea${err.desc ? " hsw-input--err" : ""}`} rows={4} value={desc} onChange={e => setDesc(e.target.value)} placeholder="What this method does and what the player must supply…" />
        {err.desc && <div className="hsw-err">{err.desc}</div>}
      </div>

      <div className="hsw-field">
        <label className="hsw-label">
          Logo <span className="hsw-opt">optional</span>
          <Tip>
            Marked required in the real form with <code>obbligatorio()</code>, but the backend validates
            <code> img</code> only when a file is actually attached — so it is optional (divergence 9). Accepted:
            .png .jpg .jpeg. Stored as <code>{hswSlug(name || "name") + "_<uniqid>.png"}</code> on the Laravel disk.
          </Tip>
        </label>
        <div className="hsw-logo">
          <div className="hsw-logo__thumb">{img ? <Icon name="receipt" size={18} /> : <Icon name="upload" size={16} />}</div>
          <div className="hsw-logo__body">
            <div className="hsw-logo__name">{img || <span className="hsw-muted">No logo uploaded</span>}</div>
            <div className="hsw-logo__acts">
              {/* A real file picker, with the real form's accept list. It used
                  to fabricate a filename and toast "Logo attached" without any
                  file having been chosen — now the name shown comes from the
                  file the operator actually picked, and the hint below says
                  plainly that nothing is uploaded anywhere. */}
              <input
                ref={fileRef} type="file" accept=".png,.jpg,.jpeg" style={{ display: "none" }}
                onChange={(e) => {
                  const file = e.target.files && e.target.files[0];
                  if (!file) return;
                  const ext = (file.name.split(".").pop() || "png").toLowerCase();
                  setPick(file.name);
                  /* saveWithdrawmethod stores altnome($name) . '_' . uniqid() . '.' . ext */
                  setImg(hswSlug(name || "method") + "_" + Math.random().toString(16).slice(2, 8) + "." + ext);
                  e.target.value = "";
                }}
              />
              <button className="hsw-mini" onClick={() => fileRef.current && fileRef.current.click()}>
                <Icon name="upload" size={11} /> Choose file
              </button>
              {/* The real form posts a hidden img_remove the controller never
                  reads, so removal is a no-op there (divergence 7). */}
              {img && <button className="hsw-mini hsw-mini--danger" onClick={() => { setImg(""); setPick(""); }}><Icon name="trash" size={11} /> Remove</button>}
            </div>
            {pick && (
              <div className="hsw-hint">
                Selected <b>{pick}</b> → this row would store it as <code>{img}</code>.
              </div>
            )}
            <div className="hsw-hint">
              <b>Nothing is uploaded from this prototype.</b> On the platform the file rides along with
              <code> POST /withdrawmethods/saveWithdrawmethod/</code> as multipart <code>img</code> and lands on the Laravel disk
              (<code>storage/app/public/withdraw/img</code>); here only the filename is recorded on the row.
              A rejected image would return a field-specific message here; the real controller validates inside a try/catch
              and surfaces the generic “The given data was invalid.” instead (divergence 6).
            </div>
          </div>
        </div>
      </div>
    </HswModal>
  );
};

/* Delete — real flow is a JS confirm titled "Elimina metodo di prelievo" hitting
   GET /withdrawmethods/delete/{id}/. The consequences are spelled out here. */
const HswDeleteModal = ({ row, skinRows, onClose, onDelete }) => {
  const uses = skinRows[row.id] || [];
  const isVoucher = row.code === HSW_VOUCHER_CODE;
  return (
    <HswModal
      title={`Delete ${row.name}?`}
      sub={`withdraw_methods #${row.id} · code ${row.code}`}
      onClose={onClose}
      footer={<>
        <button className="hsw-btn hsw-btn--ghost" onClick={onClose}>Close</button>
        <button className="hsw-btn hsw-btn--danger" onClick={() => onDelete(row)}><Icon name="trash" size={13} /> Delete method</button>
      </>}
    >
      {isVoucher && (
        <div className="hsw-danger">
          <Icon name="alert" size={15} />
          <div>
            <b>This is the <code>voucher</code> method.</b> Deleting it makes <code>getInfoSkinMethod($skin_id, "voucher")</code>
            return nothing, so every voucher creation — backoffice and player frontend alike — fails with
            <i> “Method not enabled”</i>, on every skin, from the moment you confirm. There is no guard for this in the
            backend today.
          </div>
        </div>
      )}
      <div className="hsw-killlist">
        <div className="hsw-killlist__t">What this removes</div>
        <ul>
          <li>The catalog row <code>withdraw_methods #{row.id}</code>.</li>
          <li>
            {uses.length === 0
              ? <>No <code>skin_withdraw_methods</code> rows — this method is not enabled on any skin.</>
              : <><b>{uses.length}</b> per-skin configuration{uses.length > 1 ? "s" : ""} ({uses.map(u => u.skin).join(", ")}) with their min/max, limits and Payments-admin fee settings.</>}
          </li>
          <li>Any pending withdrawal request already carrying <code>payment_method = {row.code}</code> keeps the raw string — the Withdraw requests list will show a method it can no longer resolve.</li>
        </ul>
      </div>
      <div className="hsw-note">
        <Icon name="info" size={13} />
        <span>
          The real endpoint is a <b>GET</b> behind a JS confirm, hard-deletes the row, leaves the per-skin rows orphaned
          (no FK, no cleanup) and never flushes the <code>PaymentLimitService</code> caches keyed by
          <code> method_code</code>. This prototype removes the dependent rows with the method (divergence 3).
        </span>
      </div>
    </HswModal>
  );
};

/* ------------------------------------------------------------------ */
/* Page                                                                */
/* ------------------------------------------------------------------ */

const SetWithdrawalMethods = () => {
  const feed = useHrsFetch(() => window.sb.list("paymentMethods",
    { limit: 200, filters: { flow: "withdrawal" } }), []);
  const catalog = useMemoHsw(() => (feed.data || []).map(hswRow), [feed.data]);

  /* `flow` is passed here too. Without it the call returned deposit rows as
     well, which only looked harmless because everything is grouped by
     method_id — a deposit method's skin rows would have appeared under a
     withdrawal method sharing an id. */
  const skinFeed = useHrsFetch(() => window.sb.list("skinPaymentMethods",
    { limit: 500, filters: { flow: "withdrawal" } }), []);
  const skinRows = useMemoHsw(() => {
    const out = {};
    (skinFeed.data || []).forEach(r => {
      (out[r.method_id] = out[r.method_id] || []).push({
        skin_id: r.skin_id,
        /* Was `r.method && r.method.name` — the embedded PAYMENT METHOD, so
           every row of the per-skin expander printed the method's own name in
           the Skin column. Every row of every method read the same. */
        skin: (r.skin && r.skin.name) || String(r.skin_id),
        min: r.min_amount, max: r.max_amount,
        day: r.limit_day, week: r.limit_week, month: r.limit_month,
        fee: r.fee_pct,
        /* HswDetail reads cur / bo / limited / auto / feeCur. This mapping
           emitted currency / enabled / agents, so five fields were permanently
           undefined: Active-on-BO and Limited rendered false for every skin,
           Currency and Auto-approve rendered blank, and `limited` being
           undefined greyed out all three limit cells and made the "not on BO"
           note at the foot list every skin on the platform. */
        cur: r.currency,
        feeCur: r.currency,
        /* This schema has one `enabled` boolean where isystem had `enabled`
           AND `bo_status`. The screen's distinction between "on the frontend"
           and "on the back office" collapses to that one column, so both read
           from it rather than one of them reading undefined.
           <!-- SUGGESTION: if the two states are genuinely different,
                skin_payment_methods needs a bo_status column; today the
                back-office view cannot be turned off independently. --> */
        bo: !!r.enabled,
        limited: !!r.limited,
        auto: r.auto_approve_under,
        agents: !!r.agents_enabled, enabled: !!r.enabled,
      });
    });
    return out;
  }, [skinFeed.data]);
  const [draft, setDraft] = useStateHsw(HSW_EMPTY_F);
  const [applied, setApplied] = useStateHsw(HSW_EMPTY_F);
  /* ajax.js:20 sets order [[0,"desc"]] → id DESC (the server's own fallback is
     id ASC, L121, and never wins because the client always sends an order). */
  const [sort, setSort] = useStateHsw({ key: "id", dir: "desc" });
  const [page, setPage] = useStateHsw(0);
  const [pageSize, setPageSize] = useStateHsw(50);   // pageLength: 50 (ajax.js:17)
  const [form, setForm] = useStateHsw(null);
  const [del, setDel] = useStateHsw(null);
  const [fresh, setFresh] = useStateHsw(null);       // id of a row created this session

  /* Codes used more than once — the defect divergence 4 describes. */
  const dupBy = useMemoHsw(() => {
    const seen = {}, out = {};
    catalog.forEach(m => { const c = m.code.toLowerCase(); if (seen[c] != null) { out[m.id] = seen[c]; out[seen[c]] = m.id; } else seen[c] = m.id; });
    return out;
  }, [catalog]);

  const filtered = useMemoHsw(() => {
    const idq = String(applied.id || "").trim();
    const nq = String(applied.name || "").trim().toLowerCase();
    let rows = catalog.filter(m =>
      (!idq || String(m.id) === idq) &&            // exact, controller L131-133
      (!nq || m.name.toLowerCase().includes(nq))   // LIKE %…%, controller L135-137
    );
    const dir = sort.dir === "asc" ? 1 : -1;
    rows = rows.slice().sort((a, b) => sort.key === "name"
      ? dir * a.name.localeCompare(b.name, "en", { sensitivity: "base" })
      : dir * (a.id - b.id));
    return rows;
  }, [catalog, applied, sort]);

  const pageRows = filtered.slice(page * pageSize, page * pageSize + pageSize);

  /* Create, edit and delete are writes, and they are wired. `flow` is sent on
     create only: this screen IS the withdrawal list, and a method that changes
     flow leaves it while staying enabled on every skin that had it. */
  const hswSave = useHrsSave([feed]);
  const save = (next) => {
    const payload = {
      name: next.name, code: next.code, description: next.desc || null,
      logo_url: next.img || null,
    };
    /* A THUNK, not a promise — `run` refuses while a save is in flight, and a
       promise built here would already have been sent by then. */
    const call = () => (next.id == null
      ? window.sb.create("paymentMethods", { ...payload, flow: "withdrawal" })
      : window.sb.update("paymentMethods", next.id, payload));
    hswSave.run(call, {
      done: next.id == null ? `${next.name} created` : `${next.name} updated`,
      fail: next.id == null ? `${next.name} was not created` : `${next.name} was not updated`,
    }).then(res => { if (res && res.ok) setForm(null); });
  };

  const remove = (row) => {
    /* THE COUNT IS SAID OUT LOUD, not used as a veto. isystem hard-deletes and
       strands the per-skin rows; here the delete is soft, so they keep pointing
       at a row that still exists and the enablement is recoverable. The deposit
       screen refuses while a method is in use — this one cannot, because the
       real screen's own delete dialog is the only place an operator sees the
       count at all. */
    const enabledOn = (skinRows[row.id] || []).length;
    hswSave.run(() => window.sb.remove("paymentMethods", row.id), {
      done: enabledOn
        ? `${row.name} deleted — ${enabledOn} per-skin enablement row(s) still reference it and are not stranded, because the delete is soft`
        : `${row.name} deleted`,
      fail: `${row.name} was not deleted`,
    }).then(res => { if (res && res.ok) setDel(null); });
  };

  const columns = [
    { key: "id", label: "ID", sortable: true, firstDir: "desc", width: 92,
      render: r => <span className="hsw-id">{r.id}{fresh === r.id && <span className="hsw-new">NEW</span>}</span> },
    { key: "name", label: "Name", sortable: true, firstDir: "asc",
      /* The Name cell is the edit link on the real screen (gestioneWithdrawmethod). */
      render: r => (
        <div className="hsw-namecell">
          <button className="hsw-namebtn" onClick={() => setForm(r)} title={`Edit ${r.name}`}>
            {r.name}<Icon name="chevron_right" size={12} />
          </button>
          <div className="hsw-namemeta">
            {/* method_code is not one of the three columns the real DataTables payload sends
                (ID / Name / Actions). It is surfaced inline because it is this catalog's whole
                purpose — the join key every payment flow resolves — and the operator otherwise
                has to open each row to see it. Same table, same edit form: no new data.
                <!-- SUGGESTION: add method_code as a real column (and a filter) to
                     getWithdrawmethodsTable — the catalog is unreadable without it. --> */}
            <HswCodeChip code={r.code} dup={dupBy[r.id] != null} />
            {r.code === HSW_VOUCHER_CODE && (
              /* Navigates for real to /vouchers (host-vouchers) — the screen
                 exists in this prototype, so the dependency is a link, not prose. */
              <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
                <button
                  className="hsw-dep"
                  style={{ fontFamily: "inherit", cursor: "pointer" }}
                  title="Open the Vouchers screen — it resolves this method_code"
                  onClick={(e) => { e.stopPropagation(); hsw2GoToVouchers(); }}
                >
                  <Icon name="receipt" size={10} /> Vouchers <Icon name="external" size={9} />
                </button>
                <Tip>The Voucher screen resolves this exact code. Its per-skin <code>min_with</code>/<code>max_with</code> are the amount bounds of every voucher issued; without an enabled row, voucher creation fails with “Method not enabled”. This chip opens that screen.</Tip>
              </span>
            )}
            {dupBy[r.id] != null && (
              <span className="hsw-dep hsw-dep--warn">
                <Icon name="alert" size={10} /> duplicate code
                <Tip>Row #{dupBy[r.id]} carries the same <code>method_code</code>. Nothing enforces uniqueness, and the lookup map keyed by code keeps only one of them.</Tip>
              </span>
            )}
            <span className="hsw-skins">{(skinRows[r.id] || []).length} skin{(skinRows[r.id] || []).length === 1 ? "" : "s"}</span>
          </div>
        </div>
      ) },
    { key: "acts", label: "Actions", align: "center", width: 120,
      render: r => (
        <div className="hsw-acts">
          {/* Server-side only delete() re-checks isadmin(); edit is reachable by any
              authenticated BO user (see the permission note in the title Tip). */}
          <button className="hsw-act hsw-act--danger" title="Delete" onClick={(e) => { e.stopPropagation(); setDel(r); }}><Icon name="trash" size={13} /></button>
          <button className="hsw-act hsw-act--edit" title="Edit" onClick={(e) => { e.stopPropagation(); setForm(r); }}><Icon name="edit" size={13} /></button>
        </div>
      ) },
  ];

  return (
    <HrsShell
      title="Withdrawal methods"
      subtitle="Global catalog · withdraw_methods"
      gate="isadmin()"
      gateNote={<>
        {" "}That is the <b>sidebar</b> gate only (super admin, <code>user_level 0</code>). Server-side just
        <code> delete()</code> re-checks it — listing, the modal form and <code>saveWithdrawmethod</code> are protected by
        nothing beyond the shared admin middleware, so any authenticated 2FA'd backoffice user who knows the URL can
        create and edit payment methods.
      </>}
      explainer={{
        title: "What this screen is, in plain English",
        bullets: [
          <>This is the <b>global catalog</b>. A row here is just a name, a code, a description and a logo — it moves no money and enables nothing on its own.</>,
          <><b><code>method_code</code> is the join key.</b> The withdraw forms, <code>PaymentLimitService</code> and the Withdraw requests filter all resolve methods by code, never by id or name. Renaming a row is cosmetic; changing its code is not.</>,
          <><b>Money rules are per skin.</b> Minimum / maximum, the day-week-month caps and the two switches live in <code>skin_withdraw_methods</code> and are edited on each skin's Withdrawal methods tab. Expand a row to see where it is enabled and on what terms.</>,
          <><b>The <code>voucher</code> code powers the Voucher screen.</b> Its per-skin min/max are the bounds on every voucher an operator can issue — delete or rename that code and voucher creation stops platform-wide.</>,
        ],
      }}
      actions={<button className="hsw-btn hsw-btn--primary" onClick={() => setForm({ id: null, name: "", code: "", desc: "", img: "" })}><Icon name="plus" size={13} /> New method</button>}
    >
      <HrsFilters
        fields={HSW_FIELDS}
        values={draft}
        onChange={(k, v) => setDraft(d => ({ ...d, [k]: v }))}
        onSearch={(v) => { setApplied(v); setPage(0); }}
        resultLabel={<>{filtered.length} <span style={{ fontSize: 11, fontWeight: 500, color: "var(--text-tertiary)" }}>of {catalog.length}</span></>}
      />

      <HrsAsync state={feed} skeletonRows={7} skeletonCols={5}
                empty="No withdrawal methods configured yet. Payment methods are your business data — the schema seeds none.">
        {() => (<>
      <HrsTable
        columns={columns}
        rows={pageRows}
        sort={sort}
        onSort={(s) => { setSort(s); setPage(0); }}
        rowKey="id"
        rowDetail={(r) => <HswDetail m={r} rows={skinRows[r.id] || []} dupOf={dupBy[r.id]} />}
        empty="No method matches the ID / Name filter."
        renderCard={(r) => (
          <>
            <div className="hrs-card__top">
              <b>{r.name}</b>
              <HswCodeChip code={r.code} dup={dupBy[r.id] != null} />
            </div>
            <div className="hrs-card__grid">
              <span>ID</span><b>{r.id}</b>
              <span>Enabled on</span><b>{(skinRows[r.id] || []).length} skin{(skinRows[r.id] || []).length === 1 ? "" : "s"}</b>
              <span>Updated</span><b>{hswFmtTs(r.updated)}</b>
            </div>
            {r.code === HSW_VOUCHER_CODE && (
              <button className="hsw-cardflag" style={{ fontFamily: "inherit", cursor: "pointer" }} onClick={hsw2GoToVouchers}>
                <Icon name="receipt" size={11} /> Backs the Voucher screen <Icon name="external" size={10} />
              </button>
            )}
            <details className="hsw-mdetails">
              <summary>Description &amp; per-skin enablement</summary>
              <p className="hsw-mdesc">{r.desc}</p>
              <HswDetail m={r} rows={skinRows[r.id] || []} dupOf={dupBy[r.id]} />
            </details>
            <div className="hsw-cardacts">
              <button className="hsw-btn hsw-btn--ghost hsw-btn--sm" onClick={() => setDel(r)}><Icon name="trash" size={12} /> Delete</button>
              <button className="hsw-btn hsw-btn--primary hsw-btn--sm" onClick={() => setForm(r)}><Icon name="edit" size={12} /> Edit</button>
            </div>
          </>
        )}
      />
        </>)}
      </HrsAsync>

      {/* lengthMenu [5,10,25,50], pageLength 50 (ajax.js:17-24). The real endpoint
          reports the filtered count as BOTH totals (divergence 5); this pager
          separates them. */}
      <HrsPager page={page} pageSize={pageSize} total={filtered.length} sizes={[5, 10, 25, 50]}
        onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(0); }} />

      {form && <HswFormModal row={form} catalog={catalog} onClose={() => setForm(null)} onSave={save} />}
      {del && <HswDeleteModal row={del} skinRows={skinRows} onClose={() => setDel(null)} onDelete={remove} />}
    </HrsShell>
  );
};

window.SetWithdrawalMethods = SetWithdrawalMethods;
