// Represents: GET /depositmethods/ · DepositMethodsController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Deposit methods"
/* ====================================================================
   DEPOSIT METHODS — Settings ▾ rebuild (Batch 3), Hrs* shell
   ====================================================================
   Real screen: `GET /depositmethods/` (unnamed route, routes/admin.php
   L1364-1366, inside middleware ['admin','adminsettings'] + ['auth','admin',
   '2fa','g2fa']) → DepositMethodsController::index (L66-74). Siblings:
   getDepositmethodsTable (L87-167, DataTables JSON), depositmethodForm
   (L184-201, modal form HTML), saveDepositmethod (L211-306, create + update),
   delete (L309-320, GET). Blade admin/depositmethods/index.blade.php +
   modals/depositmethod.blade.php (generaModalGestione) + forms/
   depositmethod.blade.php; JS driver public/js/pages/depositmethods/ajax.js.

   TWO TABLES, TWO SCREENS — both are rebuilt here:
   1. `deposit_methods` — the super-admin GLOBAL CATALOG. That is what this
      route edits: id, name, method_code, description, img (+ addedTime /
      updateTime as unix ints). Migration 2026_06_24_155202_create_deposit_
      methods_table.php.
   2. `skin_deposit_methods` — PER-SKIN enablement + limits, edited on the
      skin's Deposit methods tab: `GET /skins/{id}/depositmethods/` →
      SkinsController::showSkinDepositMethods (SC:2166-2183), saved through
      the shared `POST /skins/saveSkin/{id}/depositmethods` → saveEditSkin
      case "depositmethods" (SC:3551-3590) → updateSkinDeposits (SC:2319-2341).
      Columns: skin_id, deposit_id, min_dep, max_dep, limit_day/week/month,
      bo_status, agents, limited (+ fee_pct, currency added by
      2026_07_24_100003_add_config_columns_to_skin_method_tables.php).
   The second section below carries its real route in its own subtitle — it is
   the documented sibling screen surfaced next to the catalog it depends on,
   not an invented tab.
   // <!-- SUGGESTION: the catalog and its per-skin enablement are two halves of
   //      one job but live under two menus (Settings ▾ vs Skins → tab). Link
   //      them from each other, or host the per-skin grid on this screen. -->

   ENABLEMENT SEMANTICS (reference §Batch 3 "Deposit methods" → Notes):
   - row exists in skin_deposit_methods  = method visible to the frontend
     (getDepositMethods with a skin_id effectively inner-joins)
   - bo_status = 1                       = additionally gates the real payment-
     init path via PaymentLimitService::isActiveDepositeMethod() (L104-113)
   - limited = 1                         = opts the method into day/week/month
     deposit-limit enforcement (PaymentLimitService::getDepositeLimits, L74-83)
   Both lookups are cached 1h under `payment_deposite_active_%s_%s` /
   `payment_deposit_limit_%s_%s` and busted per method_code by
   updateSkinDeposits (SC:2336-2340).

   GATES (surfaced, not hidden — brief §3):
   - Sidebar: sidebar.blade.php L530-535, inside `@if (isadmin())` (L522);
     the Settings ▾ dropdown itself is `isadmin() || isSkinAdmin() ||
     $enable_agents_operators` (L504). `isadmin()` = user_level 0 / SUPER_ADMIN
     (app/Helpers/utils.php:529-531).
   - No `checkUserBoPerm` permission, no skin feature flag.
   - WEAK AUTHORIZATION: server-side only `delete()` (L310) and the skin-tab
     pages / saveEditSkin re-check `isadmin()`. index / getDepositmethodsTable
     / depositmethodForm / saveDepositmethod have NO role check beyond the
     shared admin middleware — any authenticated backoffice session that knows
     the URL can list, create and edit catalog methods. Rendered in the header
     Tip rather than quietly "fixed".
     // <!-- SUGGESTION: add the same isadmin() guard delete() already has to
     //      index/getDepositmethodsTable/depositmethodForm/saveDepositmethod. -->

   FAITHFUL ABSENCES (nothing added — brief §3):
   - NO KPIs / totals: the reference records "KPIs / totals shown: n/a".
   - NO export: "Export: none". No CSV/XLSX button is invented here.
   - NO bulk actions.
   - Table columns are the real three (ID · Name · Actions). method_code,
     description and img are NOT in the DataTables payload — they are only
     loaded by `GET /depositmethods/form/?id=` for the edit modal, so they are
     shown in the row expander / mobile card (same data, disclosed inline)
     instead of as new columns.
     // <!-- SUGGESTION: return method_code in getDepositmethodsTable so Code
     //      can be a real sortable + searchable column — operators identify a
     //      method by its code (the payment-flow join key), not by its name. -->
   - Sortable columns are only `id` and `name` (server whitelist L117-135);
     default order ID desc (JS order [[0,"desc"]], server fallback id ASC).
   - Pagination 50/page, lengthMenu [5,10,25,50], serverSide.
   - Description stays a plain textarea: index.blade.php loads TinyMCE from
     cdnjs but never initialises it.
     // <!-- SUGGESTION: drop the unused TinyMCE + datepicker CDN bundles from
     //      admin/depositmethods/index.blade.php, or actually initialise them. -->
   - Dead controller code is not represented: stati() L76-85 (0=Disabled,
     1=Active), tipologieDepositmethods() L170-182 (default/slick) and
     getDepositmethodsList() L203-210 have no callers.

   DIVERGENCES — evident intent implemented, bug recorded (known-bug policy):
   1. PER-SKIN SAVE WIPES COLUMNS (the headline bug). updateSkinDeposits is
      flagged `///// DA SISTEMARE!!!!!!!!!` ("to fix", SkinsController.php:2318):
      it DELETEs every skin_deposit_methods row for the skin and re-INSERTs
      only min_dep/max_dep/limit_day/limit_week/limit_month/bo_status/limited —
      so `fee_pct`, `currency`, `agents` (and `limit_year`) written by the
      modern Payments admin / RegisterCriptenPaymentMethods /
      RegisterMercurioPaymentMethods / CopySkin are silently destroyed on every
      save of this legacy tab. This rebuild PRESERVES them (see hsdCommitSkin)
      and shows them read-only so the loss is visible before it happens.
      // <!-- SUGGESTION: replace the delete-and-recreate in updateSkinDeposits
      //      (SC:2319-2341) with updateOrCreate on (skin_id, deposit_id)
      //      touching only the posted columns, and delete only rows the
      //      operator actually unchecked — fee_pct / currency / agents /
      //      limit_year then survive a save of the legacy skin tab. -->
   2. "ACTIVE ON BO" / "LIMITED" CAN CREATE A ROW ON THEIR OWN. `deposits[{id}]`
      (the Active scalar) shares its POST namespace with `deposits[{id}]
      [bo_status]` / `[limited]`, and PHP array parsing discards the scalar
      once a sub-key is present — so ticking only "Active on BO" still creates
      the row (the controller iterates the `deposits` keys), silently enabling
      the method on the frontend. Here Active is the single row-existence
      switch and the two sub-switches are disabled until it is on.
      // <!-- SUGGESTION: rename the sub-switches to deposits_bo_status[{id}] /
      //      deposits_limited[{id}] so the Active checkbox is the only thing
      //      that decides whether a skin_deposit_methods row exists. -->
   3. COLLAPSING VALIDATION ERRORS. Per-skin min/max are required only for
      enabled methods, but the error array is keyed by the (empty) VALUE —
      `$errors[$min_dep[$dep_key]]` (SC:3564/3567) — so every such error
      collapses into one entry and the operator sees a single message for N
      broken rows. Errors are per-row here.
      // <!-- SUGGESTION: key the per-skin error array by $dep_key (the method
      //      id) instead of by the submitted value. -->
   4. MAX-DEPOSIT PLACEHOLDER. The Maximum deposit input reuses the Minimum
      deposit placeholder (`backend.mininum_deposit`, depositmethods.blade.php
      L60 — and that key is itself misspelled at source). Correct placeholders
      here.
      // <!-- SUGGESTION: fix the L60 placeholder to the maximum-deposit key,
      //      and rename backend.mininum_deposit → minimum_deposit. -->
   5. DELETE LEAVES ORPHANS. `delete()` hard-deletes the catalog row without
      checking or cleaning skin_deposit_methods, and without flushing the
      PaymentLimitService caches — orphan per-skin rows survive pointing at a
      method id that no longer exists. Delete is guarded here: a method that is
      still enabled on any skin cannot be deleted, and the blocking skins are
      named.
      // <!-- SUGGESTION: make delete() refuse (or cascade + bust the
      //      payment_deposite_active_* / payment_deposit_limit_* caches) when
      //      skin_deposit_methods still references the method; and make it a
      //      POST/DELETE — today it is a plain GET behind a JS confirm. -->
   6. CREATE DISCARDS THE NEW ID. The create branch returns the (empty) request
      `id` in the JSON `params` instead of the insert id (L296-299); the table
      reload masks it. New rows get their real id here.
      // <!-- SUGGESTION: return DepositMethod::create($datip)->id. -->
   7. RESET BUTTON. ajax.js binds a `#kt_reset` handler but the view renders no
      reset button — a dead handler. The button is rendered here.
      // <!-- SUGGESTION: add the missing #kt_reset button to
      //      admin/depositmethods/index.blade.php. -->
   8. LOGO REQUIREDNESS. The form marks `img` required with obbligatorio() but
      saveDepositmethod treats it as optional (`image` rule only when a file is
      posted). Shown as optional, with the mismatch noted in-place.
      // <!-- SUGGESTION: drop obbligatorio() from the Logo field or make the
      //      backend rule required — today the asterisk lies. -->

   LABELS: `backend.limit_day` / `limit_week` / `limit_month` resolve in no
   committed lang file (storage/lang is gitignored) — operator-facing labels
   are written and marked "label inferred" per the build policy. Same for the
   never-rendered `agents` column and the Payments-admin `fee_pct` / `currency`.
   The modal's hardcoded Italian fallback title "Nuovo depositmethod"
   (modal.blade.php) is overridden by the translated title the index passes and
   is therefore not represented.

   UNCLEAR (carried, not guessed): the media path split — uploads go to the
   Laravel storage disk (storage/app/public/deposit/img) while display URLs are
   built from media.DEPOSIT_IMG_WEB_PATH = MEDIA_DOMAIN/deposit/img/
   (config/media.php:35); whether the media domain serves that storage path is
   not answerable from the repo. `limit_year` is in the model $fillable but
   absent from the create migration (commands guard with Schema::hasColumn, the
   base schema lives in a prod dump) — it is listed among the preserved columns
   but no input is rendered for it.

   Mock data is deterministic (hsdRng, a seeded mulberry32) so the grid renders
   identically on every load. Catalog names/ids match the ones the legacy stub
   carried; method codes documented elsewhere in the reference (wire-argentina
   on the Deposits screen, bank) are used verbatim, the gateway-registered ones
   are marked ° = inferred. Demo session = super admin "admin" (level 0),
   matching the other Host screens.

   All new top-level names are Hsd/HSD/hsd-prefixed except the required page
   component `SetDepositMethods` (app.jsx case "settings-deposit-methods");
   this file loads after HostSettings.jsx so this definition wins over the
   legacy stub there. Companion CSS: `hsd-` classes.
   ==================================================================== */

const { useState: hsdUseState, useMemo: hsdUseMemo, useEffect: hsdUseEffect } = React;

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

/* ---------- deterministic PRNG (mulberry32) — same grid on every load ---------- */
const hsdRng = (seed) => {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
};
const hsdStep = (n, step) => String(Math.max(step, Math.round(n / step) * step));

/* ---------- deposit_methods — the global catalog ----------
   ids + names as carried by the previous build (real backoffice rows).
   `code` = deposit_methods.method_code, the join key every payment flow
   matches on (ordini.payment_method). Codes marked inferred are the
   gateway-registered ones (RegisterCriptenPaymentMethods /
   RegisterMercurioPaymentMethods write these rows outside this screen) whose
   exact spelling the reference does not quote. */
/* ---------- the catalogue --------------------------------------------------
   Was seven invented deposit methods (Cripten PIX, VamosPago, 123Hub...) with
   hand-written timestamps and fabricated descriptions.

   Now `payment_methods` filtered to flow = 'deposit'. Deposit and withdrawal
   are ONE table in this schema split by `flow`, because in isystem they were
   two near-identical tables that drifted — the withdrawal one is missing the
   description column its own screen tries to render.

   `inferred` is gone: it marked which names this file had guessed. Nothing is
   guessed now, so the flag has nothing to mark. */
const hsdRow = (m) => ({
  id: m.id,
  name: m.name,
  code: m.code,
  img: m.logo_url,
  description: m.description,
  added: m.created_at,
  updated: m.created_at,
});

/* ---------- Auth::user()->getSkins() — super admin sees all skins.
   Same mock ids/names/currencies the Report ▾ rebuilds use (HostReportBusiness
   HRBZ_SKINS) so one prototype persona spans the whole build. ---------- */
const HSD_SKINS = [
  { id: 47, name: "win24hs",       cur: "ARS" },
  { id: 52, name: "apostando365",  cur: "PYG" },
  { id: 55, name: "apuestadepana", cur: "CLP" },
  { id: 58, name: "PlaySpin",      cur: "BOB" },
  { id: 60, name: "Anchodeespada", cur: "ARS" },
  { id: 62, name: "Jokerenvivo",   cur: "ARS" },
  { id: 64, name: "Donjoker",      cur: "ARS" },
  { id: 66, name: "Juegojoker",    cur: "ARS" },
  { id: 68, name: "Tucasino",      cur: "ARS" },
  { id: 70, name: "Jugaygana",     cur: "ARS" },
];

const HSD_PAGE_SIZES = [5, 10, 25, 50];

/* A skin_deposit_methods row. `active` models row EXISTENCE (there is no
   status column — reference: front-office "Active" is row presence). The
   preserved trio + limit_year are the columns updateSkinDeposits destroys. */
const hsdBlankRow = () => ({
  active: false, bo_status: false, limited: false,
  min_dep: "", max_dep: "", limit_day: "", limit_week: "", limit_month: "",
  fee_pct: "", currency: "", agents: "", limit_year: "",
});

/* hsdGenSkin / hsdGenAll / hsdCommitSkin lived here. They fabricated a
   deterministic per-skin enablement grid and then merged edits back into it.
   Both are gone: the grid is skin_payment_methods now, and the merge was the
   local-write path that made "Saved" mean nothing. */

const hsdUsage = (methodId, all) => HSD_SKINS.filter((s) => all[s.id] && all[s.id][methodId] && all[s.id][methodId].active);
const hsdNum = (v) => (v === "" || v == null ? "—" : Number(v).toLocaleString("en-US"));

/* ---------- small presentational bits ---------- */
const HsdCode = ({ row }) => (
  <span className="hsd-code">
    {row.code}
    {row.inferred && <abbr className="hsd-inf" title="Code inferred — this method is registered by a console command (RegisterCriptenPaymentMethods / RegisterMercurioPaymentMethods) and the reference does not quote its exact method_code.">°</abbr>}
  </span>
);

const HsdBug = ({ title, children }) => (
  <div className="hsd-bug">
    <div className="hsd-bug__ic"><Icon name="alert" size={13} /></div>
    <div>
      <div className="hsd-bug__t">{title}</div>
      <div className="hsd-bug__b">{children}</div>
    </div>
  </div>
);

/* Catalog fields that only `GET /depositmethods/form/?id=` returns — shown in
   the row expander / mobile card, not as new table columns. */
const HsdCatalogDetail = ({ row, usage }) => (
  <div className="hsd-detail">
    <div className="hsd-detail__logo">
      <div className="hsd-logo"><Icon name="credit_card" size={18} /></div>
      <div className="hsd-detail__file" title={row.img}>{row.img}</div>
    </div>
    <div className="hsd-detail__grid">
      <span>Code<Tip size={12}>`deposit_methods.method_code` — the join key every payment flow matches on (<code>ordini.payment_method</code>, <code>PaymentLimitService</code> cache keys, <code>getInfoSkinMethod($skin_id, $method_code)</code>).</Tip></span>
      <b><HsdCode row={row} /></b>
      <span>Description</span>
      <b className="hsd-detail__desc">{row.description}</b>
      <span>Enabled on</span>
      <b>{usage.length === 0
        ? <em className="hsd-muted">No skin</em>
        : usage.map((s) => <span key={s.id} className="hsd-pill">{s.name}</span>)}</b>
      <span>Added / updated<Tip size={12}>`addedTime` / `updateTime` are stored as unix ints on `deposit_methods`, not as Laravel timestamps.</Tip></span>
      <b className="hsd-muted">{row.added} · {row.updated}</b>
    </div>
  </div>
);

/* ---------- create / edit modal ----------
   admin/depositmethods/forms/depositmethod.blade.php via generaModalGestione().
   Inline validation from saveDepositmethod L228-257 — the real messages are
   backend.insert_name / insert_code / insert_description, returned as
   ajaxError with a `campierrati` field list. */
const HsdMethodModal = ({ row, onClose, onSave }) => {
  const isNew = !row.id;
  const [d, setD] = hsdUseState(() => ({
    name: row.name || "", code: row.code || "", description: row.description || "",
    img: row.img || "", imgRemove: false,
  }));
  const [errs, setErrs] = hsdUseState({});
  const set = (k, v) => setD((s) => ({ ...s, [k]: v }));

  const submit = () => {
    const e = {};
    if (!d.name.trim()) e.name = "Insert name";                 // backend.insert_name
    if (!d.code.trim()) e.code = "Insert code";                 // backend.insert_code
    if (!d.description.trim()) e.description = "Insert description"; // backend.insert_description
    setErrs(e);
    if (Object.keys(e).length) { hsdToast("Correct the following errors", true); return; }
    onSave({ ...row, name: d.name.trim(), code: d.code.trim(), description: d.description.trim(), img: d.imgRemove ? "" : d.img });
  };

  return (
    <div className="bp-modal-scrim hsd-scrim" onClick={onClose}>
      <div className="bp-modal hsd-modal" onClick={(e) => e.stopPropagation()}>
        <div className="hsd-modal__head">
          <div className="hsd-modal__title">{isNew ? "New method" : `Edit ${row.name}`}</div>
          <button className="hrs-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="hsd-modal__body">
          <div className="hsd-f">
            <label className="form-label">* Name</label>
            <input className={`input input--sm${errs.name ? " hsd-input--err" : ""}`} value={d.name} onChange={(e) => set("name", e.target.value)} autoFocus />
            {errs.name && <div className="hsd-err">{errs.name}</div>}
          </div>
          <div className="hsd-f">
            <label className="form-label">* Code<Tip size={12}>Written to <code>deposit_methods.method_code</code>. Everything downstream joins on this string — <code>ordini.payment_method</code>, <code>getInfoSkinMethod()</code>, and the two <code>PaymentLimitService</code> cache keys. Changing it on a live method orphans every existing order.</Tip></label>
            <input className={`input input--sm hsd-mono${errs.code ? " hsd-input--err" : ""}`} value={d.code} onChange={(e) => set("code", e.target.value)} placeholder="e.g. wire-argentina" />
            {errs.code && <div className="hsd-err">{errs.code}</div>}
            {/* <!-- SUGGESTION: saveDepositmethod has no uniqueness rule on method_code (nor on name) — two catalog rows can share a code and silently collide on every join. Add a unique rule + a DB index. --> */}
            <div className="hsd-hint">No uniqueness rule exists server-side — a duplicate code collides on every payment-flow join.</div>
          </div>
          <div className="hsd-f">
            <label className="form-label">* Description</label>
            <textarea className={`input input--sm hsd-ta${errs.description ? " hsd-input--err" : ""}`} rows={4} value={d.description} onChange={(e) => set("description", e.target.value)} />
            {errs.description && <div className="hsd-err">{errs.description}</div>}
            <div className="hsd-hint">Plain textarea — the real index loads TinyMCE from a CDN but never initialises it.</div>
          </div>
          <div className="hsd-f">
            <label className="form-label">Logo<Tip size={12}>Stored as <code>altnome(name)_uniqid().ext</code> under <code>storage/app/public/deposit/img</code>, but displayed from <code>MEDIA_DOMAIN/deposit/img/</code> — whether the media domain serves that storage path is UNCLEAR from the repo.</Tip></label>
            <div className="hsd-fileline">
              <input className="input input--sm" type="file" accept=".png,.jpg,.jpeg"
                onChange={(e) => { const f = e.target.files && e.target.files[0]; if (f) { set("img", f.name); set("imgRemove", false); } }} />
              {d.img && !d.imgRemove && <span className="hsd-filename" title={d.img}>{d.img}</span>}
            </div>
            {row.img && (
              <label className="hsd-check">
                <input type="checkbox" checked={d.imgRemove} onChange={(e) => set("imgRemove", e.target.checked)} />
                Remove current logo
                <Tip size={12}>Posts the hidden <code>img_remove</code> flag. The sibling Withdrawal-methods controller never reads it; the deposit side is not verified in the reference.</Tip>
              </label>
            )}
            {/* Divergence 8: the Blade marks this field required with obbligatorio()
                but saveDepositmethod validates `image` only when a file is posted. */}
            <div className="hsd-hint">Optional. The real form marks it required with <code>obbligatorio()</code>; the backend accepts a save without it.</div>
          </div>
        </div>
        <div className="hsd-modal__foot">
          <button className="hrs-btn hrs-btn--reset" onClick={onClose}>Close</button>
          <button className="hrs-btn hrs-btn--search" onClick={submit}>Save</button>
        </div>
      </div>
    </div>
  );
};

/* ---------- per-skin method card ----------
   One card per catalog method, ordered id ASC (the real Blade queries the
   catalog inline at depositmethods.blade.php L43-45 with that order). */
const HsdSkinMethodCard = ({ meta, cfg, cur, err, onSet }) => {
  const off = !cfg.active;
  const shown = cfg.currency || cur;
  return (
    <div className={`hsd-mcard${off ? " hsd-mcard--off" : ""}`}>
      <div className="hsd-mcard__head">
        <div className="hsd-mcard__id">#{meta.id}</div>
        <div>
          <div className="hsd-mcard__title">{meta.name}</div>
          <HsdCode row={meta} />
        </div>
        <div className="hsd-mcard__sw">
          {/* Row existence in skin_deposit_methods = "Active" on the frontend.
              Divergence 2: on the real form the two sub-switches can create the
              row on their own; here they follow Active. */}
          <Toggle value={cfg.active} onChange={(v) => onSet("active", v)} onLabel="Active" offLabel="Not enabled" size="sm" />
        </div>
      </div>

      <div className="hsd-mgrid">
        <div className="hsd-f hsd-f--sw">
          <label className="form-label">Active on BO<Tip size={12}><code>bo_status = 1</code>. Gates the real payment-init path via <code>PaymentLimitService::isActiveDepositeMethod()</code> (cached 1h under <code>payment_deposite_active_&#123;skin&#125;_&#123;code&#125;</code>).</Tip></label>
          <Toggle value={cfg.bo_status} onChange={(v) => onSet("bo_status", v)} disabled={off} onLabel="On" offLabel="Off" size="sm" />
        </div>
        <div className="hsd-f hsd-f--sw">
          <label className="form-label">Limited<Tip size={12}><code>limited = 1</code> opts this method into day / week / month deposit-limit enforcement (<code>PaymentLimitService::getDepositeLimits()</code>).</Tip></label>
          <Toggle value={cfg.limited} onChange={(v) => onSet("limited", v)} disabled={off} onLabel="On" offLabel="Off" size="sm" />
        </div>
        <div className="hsd-f">
          <label className="form-label">* Minimum deposit</label>
          <input className={`input input--sm${err && err.min ? " hsd-input--err" : ""}`} inputMode="decimal" disabled={off}
            value={cfg.min_dep} placeholder={`Minimum deposit (${shown})`} onChange={(e) => onSet("min_dep", e.target.value)} />
          {err && err.min && <div className="hsd-err">{err.min}</div>}
        </div>
        <div className="hsd-f">
          <label className="form-label">* Maximum deposit</label>
          {/* Divergence 4: the real input reuses backend.mininum_deposit as its placeholder. */}
          <input className={`input input--sm${err && err.max ? " hsd-input--err" : ""}`} inputMode="decimal" disabled={off}
            value={cfg.max_dep} placeholder={`Maximum deposit (${shown})`} onChange={(e) => onSet("max_dep", e.target.value)} />
          {err && err.max && <div className="hsd-err">{err.max}</div>}
        </div>
        <div className="hsd-f">
          <label className="form-label">Limit day{/* label inferred */}</label>
          <input className="input input--sm" inputMode="decimal" disabled={off || !cfg.limited} value={cfg.limit_day} onChange={(e) => onSet("limit_day", e.target.value)} />
        </div>
        <div className="hsd-f">
          <label className="form-label">Limit week{/* label inferred */}</label>
          <input className="input input--sm" inputMode="decimal" disabled={off || !cfg.limited} value={cfg.limit_week} onChange={(e) => onSet("limit_week", e.target.value)} />
        </div>
        <div className="hsd-f">
          <label className="form-label">Limit month{/* label inferred */}</label>
          <input className="input input--sm" inputMode="decimal" disabled={off || !cfg.limited} value={cfg.limit_month} onChange={(e) => onSet("limit_month", e.target.value)} />
        </div>
      </div>

      {/* Columns this tab must NOT own — read-only, preserved on save.
          Divergence 1: the real save deletes and recreates the row, wiping them. */}
      {cfg.active && (
        <div className="hsd-pres">
          <div className="hsd-pres__h">
            <Icon name="lock" size={11} /> Preserved on save — written elsewhere
            <Tip size={12}>The modern Payments admin (<code>SkinDepositMethodRepository</code>), the <code>RegisterCriptenPaymentMethods</code> / <code>RegisterMercurioPaymentMethods</code> console commands and <code>Skins/CopySkin</code> write these columns. The real legacy save destroys them; this rebuild carries them through untouched.</Tip>
          </div>
          <div className="hsd-pres__row">
            <div className="hsd-pres__i"><span>Fee %{/* label inferred */}</span><b>{cfg.fee_pct === "" ? "—" : `${cfg.fee_pct}%`}</b></div>
            <div className="hsd-pres__i"><span>Currency{/* label inferred */}</span><b>{cfg.currency || <em className="hsd-muted">skin default ({cur})</em>}</b></div>
            <div className="hsd-pres__i"><span>Agents{/* label inferred */}</span><b>{cfg.agents || "—"}<Tip size={12}>`skin_deposit_methods.agents`, a string defaulting to <code>''</code>. No screen in the reference renders or writes it and its semantics are not documented — carried verbatim.</Tip></b></div>
            <div className="hsd-pres__i"><span>Limit year{/* label inferred */}</span><b>{hsdNum(cfg.limit_year)}<Tip size={12}>In the model <code>$fillable</code> but absent from the create migration; the console commands guard with <code>Schema::hasColumn</code>. Whether the production column exists is UNCLEAR — no input is rendered, the stored value is carried through.</Tip></b></div>
          </div>
        </div>
      )}
    </div>
  );
};

/* ---------- per-skin enablement panel ---------- */
const HsdSkinPanel = ({ all, catalog }) => {
  const [skinId, setSkinId] = hsdUseState(68); // Tucasino — the flagship the Report ▾ rebuilds default to
  /* A skin with NO rows in skin_payment_methods has no entry in `all` at all.
     The generator this replaced always produced one per skin, so cloning it was
     safe; against live data it is not, and JSON.parse(JSON.stringify(undefined))
     crashes the whole screen. Absent means "nothing enabled", which is an empty
     object, not a missing one. */
  const hsdClone = (v) => JSON.parse(JSON.stringify(v || {}));
  const [draft, setDraft] = hsdUseState(() => hsdClone(all[68]));
  const [errs, setErrs] = hsdUseState({});

  hsdUseEffect(() => { setDraft(hsdClone(all[skinId])); setErrs({}); }, [skinId, all]);

  const skin = HSD_SKINS.find((s) => s.id === skinId);
  const set = (mid, k, v) => setDraft((d) => {
    const row = { ...(d[mid] || hsdBlankRow()), [k]: v };
    /* Divergence 2 — Active owns row existence; switching it off parks the
       sub-switches instead of letting them recreate the row on their own. */
    if (k === "active" && !v) { row.bo_status = false; row.limited = false; }
    if (k === "limited" && !v) { row.limit_day = ""; row.limit_week = ""; row.limit_month = ""; }
    return { ...d, [mid]: row };
  });

  const enabledCount = Object.values(draft).filter((r) => r && r.active).length;
  const dirty = JSON.stringify(draft) !== JSON.stringify(all[skinId] || {});

  const save = () => {
    /* Divergence 3 — per-row errors instead of the real array keyed by the
       (empty) submitted value, which collapses every message into one. */
    const e = {};
    catalog.forEach((m) => {
      const r = draft[m.id];
      if (!r || !r.active) return;
      const row = {};
      if (String(r.min_dep).trim() === "") row.min = "Fill in the minimum deposit field";
      if (String(r.max_dep).trim() === "") row.max = "Fill in the maximum deposit field";
      if (Object.keys(row).length) e[m.id] = row;
    });
    setErrs(e);
    if (Object.keys(e).length) { hsdToast(`Correct the following errors (${Object.keys(e).length} method(s))`, true); return; }
    /* Saving per-skin enablement is a write across skin_payment_methods, and
       src/supabase.js is read-only by design (stage 7). It used to commit into
       a local object and toast "Saved" — which reads as success and survives
       nothing. */
    hsdToast(`Not saved — no write path yet. Would upsert ${enabledCount} row(s) in skin_payment_methods for ${skin.name} and delete the unchecked ones.`, true);
  };

  return (
    <>
      <HsdBug title="Divergence — the real save destroys four columns">
        <code>updateSkinDeposits</code> (SkinsController.php:2319-2341, flagged
        <code>///// DA SISTEMARE!!!!!!!!!</code>) deletes every
        <code>skin_deposit_methods</code> row for the skin and re-inserts only the
        posted columns — so <b>fee_pct</b>, <b>currency</b>, <b>agents</b> and
        <b>limit_year</b> are silently wiped on every save of this tab. This
        rebuild merges the posted columns onto the stored row and carries those
        four through untouched (shown read-only on each enabled method below).
      </HsdBug>

      <div className="hsd-skinbar">
        <div className="hsd-f hsd-f--skin">
          <label className="form-label">Skin<Tip size={12}>The <code>&#123;id&#125;</code> segment of <code>GET /skins/&#123;id&#125;/depositmethods/</code>. <code>showSkinDepositMethods</code> aborts 404 for anyone who is not <code>isadmin()</code>, and the save re-checks the policy again.</Tip></label>
          <select className="select input--sm" value={skinId} onChange={(e) => setSkinId(Number(e.target.value))}>
            {HSD_SKINS.map((s) => <option key={s.id} value={s.id}>{s.id} — {s.name} ({s.cur})</option>)}
          </select>
        </div>
        <div className="hsd-skinbar__meta">
          <b>{enabledCount}</b> of {catalog.length} catalog methods enabled on <b>{skin.name}</b>
          <span className="hsd-muted"> · a row exists = the method is visible on the frontend</span>
        </div>
        <div className="hsd-skinbar__act">
          {dirty && <span className="hsd-dirty"><Icon name="alert" size={11} /> Unsaved changes</span>}
          <button className="hrs-btn hrs-btn--search" onClick={save}><Icon name="check" size={14} /> Save</button>
        </div>
      </div>

      <div className="hsd-mlist">
        {catalog.slice().sort((a, b) => a.id - b.id).map((m) => (
          <HsdSkinMethodCard key={m.id} meta={m} cur={skin.cur}
            cfg={draft[m.id] || hsdBlankRow()} err={errs[m.id]}
            onSet={(k, v) => set(m.id, k, v)} />
        ))}
      </div>

      <div className="hsd-note">
        Saving also flushes the two 1h <code>PaymentLimitService</code> caches per
        method code — <code>payment_deposite_active_&#123;skin&#125;_&#123;code&#125;</code> and
        <code>payment_deposit_limit_&#123;skin&#125;_&#123;code&#125;</code> — plus the skin cache
        (<code>flushSkinCache</code>), exactly as the real save does.
      </div>
    </>
  );
};

/* ==================================================================
   PAGE
   ================================================================== */
const SetDepositMethods = () => {
  const feed = useHrsFetch(() => window.sb.list("paymentMethods",
    { limit: 200, filters: { flow: "deposit" } }), []);
  const catalog = hsdUseMemo(() => (feed.data || []).map(hsdRow), [feed.data]);

  /* Per-skin enablement lives in skin_payment_methods. Read live; the editor
     below still assembles a draft, but saving it is a write and there is no
     write path yet (stage 7 of docs/WORK_PLAN.md). */
  const skinFeed = useHrsFetch(() => window.sb.list("skinPaymentMethods", { limit: 500 }), []);
  const all = hsdUseMemo(() => {
    const out = {};
    (skinFeed.data || []).forEach(r => {
      (out[r.skin_id] = out[r.skin_id] || {})[r.method_id] = {
        enabled: !!r.enabled, limited: !!r.limited, agents: !!r.agents_enabled,
        min: r.min_amount, max: r.max_amount, fee_pct: r.fee_pct,
        currency: r.currency, limit_day: r.limit_day, limit_week: r.limit_week,
        limit_month: r.limit_month,
      };
    });
    return out;
  }, [skinFeed.data]);

  /* apply-on-Search: the real per-column DataTables search only fires on the
     #kt_search click, so draft and applied filter state stay separate. */
  const [draft, setDraft] = hsdUseState({ id: "", name: "" });
  const [applied, setApplied] = hsdUseState({ id: "", name: "" });
  const [sort, setSort] = hsdUseState({ key: "id", dir: "desc" }); // JS order [[0,"desc"]]
  const [page, setPage] = hsdUseState(0);
  const [pageSize, setPageSize] = hsdUseState(50);
  const [modal, setModal] = hsdUseState(null);

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "list", placeholder: "Exact ID", width: 150,
      tip: <>Exact match on <code>deposit_methods.id</code> — not a range, not a LIKE.</> },
    { key: "name", label: "Name", type: "text", icon: "search", placeholder: "Name contains…", grow: true,
      tip: <><code>LIKE %…%</code> on <code>deposit_methods.name</code>. There is no filter on <code>method_code</code> or <code>description</code>.</> },
  ];

  const rows = hsdUseMemo(() => {
    let out = catalog.filter((r) =>
      (!applied.id || String(r.id) === String(applied.id).trim()) &&
      (!applied.name || r.name.toLowerCase().includes(applied.name.trim().toLowerCase())));
    const dir = sort.dir === "asc" ? 1 : -1;
    out = out.slice().sort((a, b) => (sort.key === "name"
      ? a.name.localeCompare(b.name) * dir
      : (a.id - b.id) * dir));
    return out;
  }, [catalog, applied, sort]);

  const paged = rows.slice(page * pageSize, page * pageSize + pageSize);

  /* The catalogue is writable. `flow` is set on create and never sent on an
     edit — this screen IS the deposit list, and a method that changes flow
     disappears from the screen that owns it while staying enabled on every skin
     that had it. */
  const hsdSave = useHrsSave([feed]);
  const saveMethod = (m) => {
    const payload = {
      name: m.name, code: m.code, description: m.description || null,
      logo_url: m.img || null,
    };
    /* A THUNK, not a promise. `useHrsSave.run` refuses while a save is in
       flight — building the promise here would have fired the request before
       that check ran, so the guard against a double-submit would guard
       nothing. */
    const call = () => (m.id
      ? window.sb.update("paymentMethods", m.id, payload)
      : window.sb.create("paymentMethods", { ...payload, flow: "deposit" }));
    hsdSave.run(call, {
      done: m.id ? `${m.name} updated` : `${m.name} created`,
      fail: m.id ? `${m.name} was not updated` : `${m.name} was not created`,
    }).then(res => { if (res && res.ok) setModal(null); });
  };

  const removeMethod = (row) => {
    /* Divergence 5 — the real delete() hard-deletes without checking or
       cleaning skin_deposit_methods, leaving orphan per-skin rows behind. The
       in-use check below is the evident intent; the soft delete is the schema's
       decision (write_allowlist.soft_delete), not this screen's. */
    const used = hsdUsage(row.id, all);
    if (used.length) {
      hsdToast(`${row.name} is still enabled on ${used.length} skin(s): ${used.map((s) => s.name).join(", ")}`, true);
      return;
    }
    hsdSave.run(() => window.sb.remove("paymentMethods", row.id), {
      done: `${row.name} deleted`,
      fail: `${row.name} was not deleted`,
    });
  };

  const COLUMNS = [
    { key: "id", label: "ID", sortable: true, firstDir: "desc", width: 90 },
    { key: "name", label: "Name", sortable: true, render: (r) => (
      <button className="hsd-namelink" onClick={() => setModal(r)}>
        {r.name}<Icon name="chevron_right" size={12} />
      </button>
    ) },
    { key: "_act", label: "Actions", align: "center", width: 120, render: (r) => {
      const blocked = hsdUsage(r.id, all).length;
      return (
        <div className="hsd-acts">
          <button className={`hsd-act hsd-act--danger${blocked ? " hsd-act--blocked" : ""}`}
            title={blocked ? `Enabled on ${blocked} skin(s) — remove it there first` : "Delete"}
            onClick={() => removeMethod(r)}><Icon name="trash" size={13} /></button>
          <button className="hsd-act hsd-act--edit" title="Edit" onClick={() => setModal(r)}><Icon name="edit" size={13} /></button>
        </div>
      );
    } },
  ];

  return (
    <HrsShell
      title="Deposit methods"
      subtitle="GET /depositmethods/ · DepositMethodsController"
      gate={<>Sidebar visibility is <code>isadmin()</code> (SUPER_ADMIN, <code>user_level 0</code>) only — there is no <code>checkUserBoPerm</code> permission and no skin feature flag on this screen. </>}
      gateNote={<>Surfaced honestly: server-side, only <code>delete()</code> and the per-skin tab re-check <code>isadmin()</code>. <code>index</code>, <code>getDepositmethodsTable</code>, <code>depositmethodForm</code> and <code>saveDepositmethod</code> carry no role check beyond the shared admin middleware, so any authenticated backoffice session that knows the URL can list, create and edit catalog methods.</>}
      explainer={{
        title: "What this screen configures, in plain English",
        body: <>A deposit method exists twice: once in the platform-wide catalog this route edits, and once per skin that actually offers it.</>,
        bullets: [
          <><b>Catalog (<code>deposit_methods</code>)</b> — name, code, description, logo. Creating a row here makes the method <i>available</i>, not <i>live</i>.</>,
          <><b>Per-skin (<code>skin_deposit_methods</code>)</b> — the row's mere existence is what makes the method visible on that skin's frontend; there is no status column.</>,
          <><b><code>bo_status = 1</code></b> additionally gates the real payment-init path (<code>PaymentLimitService::isActiveDepositeMethod()</code>), and <b><code>limited = 1</code></b> opts the method into day / week / month deposit-limit enforcement. Both are cached for an hour and busted per method code on save.</>,
          <><b><code>method_code</code></b> is the join key everything downstream matches on — <code>ordini.payment_method</code> on the Deposits queue, the admin and frontend deposit forms, and the Conversion report.</>,
        ],
      }}
      actions={<button className="hrs-btn hrs-btn--search" onClick={() => setModal({})}><Icon name="plus" size={14} /> New method</button>}
    >
      <HrsSection
        title="Global catalog — deposit_methods"
        sub="GET /depositmethods/ · rows from GET /depositmethods/getDepositmethodsTable (server-side DataTables)"
      >
        <HrsFilters
          fields={FIELDS}
          values={draft}
          onChange={(k, v) => setDraft((d) => ({ ...d, [k]: v }))}
          onSearch={(v) => { setApplied(v); setPage(0); }}
          /* Divergence 7 — ajax.js binds #kt_reset but the Blade renders no
             reset button; the dead handler is given its button here. */
          onReset={() => { setDraft({ id: "", name: "" }); setApplied({ id: "", name: "" }); setPage(0); }}
          resultLabel={`${rows.length} of ${catalog.length}`}
        />

        <HrsAsync state={feed} skeletonRows={7} skeletonCols={5}
                  empty="No deposit methods configured yet. Payment methods are your business data — the schema seeds none.">
          {() => (<>
        <HrsTable
          columns={COLUMNS}
          rows={paged}
          sort={sort}
          onSort={(s) => { setSort(s); setPage(0); }}
          rowDetail={(r) => <HsdCatalogDetail row={r} usage={hsdUsage(r.id, all)} />}
          renderCard={(r) => (
            <>
              <div className="hrs-card__top">
                <b>{r.name}</b>
                <span className="hsd-idtag">#{r.id}</span>
              </div>
              <div className="hrs-card__grid">
                <span>Code</span><b><HsdCode row={r} /></b>
                <span>Enabled on</span><b>{hsdUsage(r.id, all).length} skin(s)</b>
              </div>
              <details className="hsd-cardmore">
                <summary>Details &amp; actions</summary>
                <HsdCatalogDetail row={r} usage={hsdUsage(r.id, all)} />
                <div className="hsd-acts hsd-acts--card">
                  <button className="hrs-btn hrs-btn--reset" onClick={() => removeMethod(r)}><Icon name="trash" size={13} /> Delete</button>
                  <button className="hrs-btn hrs-btn--search" onClick={() => setModal(r)}><Icon name="edit" size={13} /> Edit</button>
                </div>
              </details>
            </>
          )}
          empty={applied.id || applied.name
            ? "No catalog method matches this search."
            : "The catalog is empty — create the first method with “New method”."}
        />
          </>)}
        </HrsAsync>

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

        <div className="hsd-note">
          No export and no KPI strip: the real screen ships neither. Delete is a
          plain <code>GET /depositmethods/delete/&#123;id&#125;/</code> behind a JS
          confirm and is the only catalog action that re-checks{" "}
          <code>isadmin()</code>; editing does not.
        </div>
      </HrsSection>

      <HrsSection
        title="Per-skin enablement — skin_deposit_methods"
        sub="GET /skins/{id}/depositmethods/ · saved via POST /skins/saveSkin/{id}/depositmethods (SkinsController::updateSkinDeposits)"
      >
        <HsdSkinPanel all={all} catalog={catalog} />
      </HrsSection>

      {modal && <HsdMethodModal row={modal} onClose={() => setModal(null)} onSave={saveMethod} />}
    </HrsShell>
  );
};

window.SetDepositMethods = SetDepositMethods;
