// NO PROD JSON API (bucket C) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /payments/players/{id}/ · AdminPaymentsController::playerDetail() — see docs/ISYSTEM_REFERENCE.md §Batch 10.1
/* Traced Aug 2026 (architecture item 2). Its own live route and its own blade
   (admin/payments/sections/player.blade.php), built by PaymentPlayerService —
   not a modal over the list. Reached from PlayersList.jsx. Distinct from the
   host player editor (PlayersController, HostPlayers.jsx), which has ten tabs
   and a different field set. */
/* Player 360 */

/* Page-scoped CSS (design-alignment Wave 1). This screen used to carry every
   style inline — pervasive style attributes, hex literals (#16a34a, #fee2e2,
   #991b1b, #b91c1c, #fff) and alpha-composited colors (color+'22') among
   them. Everything below draws only on styles/tokens.css tokens and the
   --fs-* type scale; the p360- prefix is grep-verified unique across src/
   and styles/, and the single 860px breakpoint matches the app-wide one
   (canon §2.16). */
const P360_CSS = `
.p360-back { margin-bottom: 10px; }
.p360-id { display: flex; align-items: center; gap: 14px; }
.p360-avatar { background: linear-gradient(135deg, var(--p-200), var(--p-500)); color: var(--text-on-primary); }
.p360-titlerow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.p360-role { margin-bottom: 12px; display: flex; align-items: center; gap: 8px; font-size: var(--fs-xs); }
.p360-kpis { margin-bottom: 14px; }
.p360-kpi-sub { font-size: var(--fs-xs); color: var(--text-tertiary); }
.p360-pos { color: var(--ok-600); }
.p360-neg { color: var(--err-500); }
.p360-cols { display: grid; grid-template-columns: 1.6fr 1fr; gap: 14px; align-items: start; }
.p360-main { overflow: hidden; }
.p360-tabs { padding: 10px 14px; border-bottom: 1px solid var(--border-subtle); }
.p360-tabbody { padding: 16px; display: flex; flex-direction: column; gap: 16px; }
.p360-h { font-size: var(--fs-xxs); font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--text-tertiary); margin-bottom: 8px; }
.p360-def { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; font-size: var(--fs-md); }
.p360-empty-note { padding: 10px 12px; border: 1px dashed var(--border-default); border-radius: var(--r-md); background: var(--n-25); font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.55; }
.p360-txwrap { overflow-x: auto; }
.p360-r { text-align: right; }
.p360-td-desc { font-size: var(--fs-xs); }
.p360-td-amt { text-align: right; font-weight: 600; }
.p360-td-date { font-size: var(--fs-xs); color: var(--text-tertiary); }
.p360-cap { font-size: var(--fs-xxs); color: var(--text-tertiary); line-height: 1.5; margin-top: 6px; }
.p360-limintro { font-size: var(--fs-sm); color: var(--text-secondary); margin-bottom: 12px; }
.p360-limits { border: 1px solid var(--border-default); border-radius: var(--r-sm); }
.p360-limits th { font-size: var(--fs-xxs); }
.p360-lk { font-weight: 600; font-size: var(--fs-sm); text-transform: capitalize; }
.p360-lp { font-size: var(--fs-sm); text-transform: capitalize; }
.p360-lsetby { font-size: var(--fs-xs); }
.p360-lsched { font-size: var(--fs-xs); }
.p360-footnote { margin-top: 14px; font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.55; }
.p360-notes-list { display: flex; flex-direction: column; gap: 8px; }
.p360-note-card { padding: 10px 12px; border: 1px solid var(--border-default); border-radius: var(--r-lg); background: var(--surface-panel); }
.p360-note-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.p360-note-author { font-size: var(--fs-xs); font-weight: 650; }
.p360-note-when { font-size: var(--fs-xxs); color: var(--text-tertiary); }
.p360-note-body { font-size: var(--fs-md); color: var(--text-primary); line-height: 1.5; white-space: pre-wrap; }
.p360-composer { padding: 10px 12px; border: 1px solid var(--border-default); border-radius: var(--r-lg); background: var(--n-25); }
.p360-notearea { resize: vertical; min-height: 64px; }
.p360-composer-foot { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.p360-composer-hint { font-size: var(--fs-xxs); color: var(--text-tertiary); }
.p360-mla { margin-left: auto; }
.p360-st { display: inline-flex; align-items: center; }
.p360-side { display: flex; flex-direction: column; gap: 14px; }
.p360-qa { padding: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.p360-methods { padding: 4px 12px 12px; display: flex; flex-direction: column; gap: 4px; }
.p360-method { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--r-sm); border: 1px solid transparent; }
.p360-method.is-off { background: var(--n-25); border-color: var(--border-subtle); }
.p360-method-name { flex: 1; font-size: var(--fs-sm); font-weight: 500; color: var(--text-primary); }
.p360-method.is-off .p360-method-name { color: var(--text-tertiary); text-decoration: line-through; }
.p360-method-state { font-size: var(--fs-xxs); font-weight: 700; letter-spacing: .04em; color: var(--text-tertiary); }
.p360-method.is-off .p360-method-state { color: var(--err-700); }
@media (max-width: 860px) {
  .p360-cols { grid-template-columns: 1fr; }
  .p360-tabbody .grid-2 { grid-template-columns: 1fr; }
}
`;

const Player360 = ({ brand, player, onBack }) => {
  window.useLocale && window.useLocale();
  const T = window.T || ((k, fb) => fb || k);
  /* THE CURATED PROFILE IS GONE. `MOCK.PLAYER` was one rich fake player —
     "Marco" — and every selected player was merged ON TOP of it, so the limits,
     the saved payment methods and the notes on screen belonged to Marco no
     matter whose page you opened. The fields the list passed in took
     precedence; everything it did not carry silently stayed his.

     Four real feeds instead, all keyed on the player actually selected. */
  const pid = player && player.id;
  const p360LimitFeed = useHrsFetch(
    () => (pid ? window.sb.list("userLimits", { limit: 50, filters: { user: pid } })
               : Promise.resolve({ ok: true, data: [] })), [pid]);
  const p360NoteFeed = useHrsFetch(
    () => (pid ? window.sb.list("userNotes", { limit: 100, filters: { user: pid } })
               : Promise.resolve({ ok: true, data: [] })), [pid]);
  const p360BlockFeed = useHrsFetch(
    () => (pid ? window.sb.list("userPaymentMethodBlocks", { limit: 100, filters: { user: pid } })
               : Promise.resolve({ ok: true, data: [] })), [pid]);
  const p360TxFeed = useHrsFetch(
    () => (pid ? window.sb.list("ledger", { limit: 300, filters: { user: pid, wallet: "real" } })
               : Promise.resolve({ ok: true, data: [] })), [pid]);
  /* Async coverage is per feed, rendered in the tab that owns it (canon
     §2.7): Transactions reads p360TxFeed, Notes reads p360NoteFeed, Limits
     reads p360LimitFeed, and the sidebar's method panel gates on the method
     AND block feeds together. The old aggregate p360Busy/p360Err flags were
     computed and never rendered — the exact defect (pattern 6) this pass
     removes — and a slow notes fetch must not skeleton the transactions tab,
     so per-feed states replace them. */

  /* formatTs() is `new Date(ts).toLocaleString()`, and `new Date(null)` is the
     epoch — so an absent timestamp renders as "Jan 01, 00:00", which is a date
     rather than an absence. Every nullable timestamp on this page goes through
     here instead. */
  const p360When = (ts) => (ts ? formatTs(ts) : "—");

  /* The player's own limit rows. `user_limits` stores CAPS — one per
     (kind, period) — not consumption. */
  const p360Limits = useMemo(() => (p360LimitFeed.data || []).map(l => ({
    id: l.id,
    kind: l.kind,
    period: l.period,
    amount: l.amount,
    currency: l.currency,
    set_by: l.set_by,
    setByName: l.setBy ? l.setBy.username : null,
    effective_from: l.effective_from,
    pending_amount: l.pending_amount,
    pending_from: l.pending_from,
  })), [p360LimitFeed.data]);

  /* The ledger, in the shape this page's transaction table already reads. */
  const PLAYER_TX = useMemo(() => (p360TxFeed.data || []).map(r => ({
    id: String(r.id),
    type: Number(r.type_id) === 1 ? "Deposit" : Number(r.type_id) === 2 ? "Withdrawal" : (r.type ? r.type.label : "Movement"),
    amount: Math.abs(Number(r.amount) || 0),
    currency: r.currency || "",
    status: "balanced",
    method_name: r.description || "",
    created_at: Date.parse(r.created_at) || 0,
    user_id: pid,
  })), [p360TxFeed.data, pid]);

  /* The 300-row feed is paginated client-side through the shared HrsPager
     (canon §2.6) instead of one 300-row scroll well. */
  const [p360TxPage, setP360TxPage] = useState(0);
  const [p360TxSize, setP360TxSize] = useState(25);
  React.useEffect(() => { setP360TxPage(0); }, [pid]);
  const p360TxPageSafe = Math.min(p360TxPage, Math.max(0, Math.ceil(PLAYER_TX.length / p360TxSize) - 1));
  const p360TxRows = PLAYER_TX.slice(p360TxPageSafe * p360TxSize, (p360TxPageSafe + 1) * p360TxSize);

  /* NO BASE TO FALL BACK ON. A field the list did not carry is absent, and
     absent renders as "—" rather than as somebody else's value. (The previous
     fallback here named `BASE` — a global that does not exist anywhere, so
     opening the page without a player object threw. An empty object renders
     the same honest absences without the crash.) */
  const P = player ? {
    user_id: player.id,
    name: player.name,
    email: player.email,
    phone: player.phone,
    country: player.country,
    brand: player.brand,
    currency: player.currency,
    level: (player.level || "STANDARD")[0] + (player.level || "STANDARD").slice(1).toLowerCase(),
    kyc: player.kyc,
    last_active: player.last_active,
    joined_at: player.joined_at,
    lifetime_deposits: player.lifetime_deposits,
    lifetime_withdrawals: player.lifetime_withdrawals,
    net_deposits: player.net_position,
    deposits_count: player.deposit_count,
    withdrawals_count: player.withdrawal_count,
    role: player.role || "PLAYER",
  } : {};
  // Each tab gets its own URL (e.g. /payments/players/transactions) via
  // useUrlTab (src/routes.jsx).
  const PLAYER360_TAB_ROUTES = [["overview", "Overview", ""], ["transactions", "Transactions", "transactions"], ["limits", "Limits", "limits"], ["kyc", "KYC", "kyc"], ["notes", "Notes", "notes"]];
  const [tab, setTab] = window.useUrlTab("/payments/players", PLAYER360_TAB_ROUTES, "overview");

  /* Suspension — local state seeds from the record the list carried and
     advances only after the write returns ok, so the switch shows the truth. */
  const [suspended, setSuspended] = useState(player?.suspended || false);
  /* PRESENCE IS THE BLOCK. `user_payment_method_blocks` has one row per
     blocked (player, method) — an absent row is not a disabled method, and the
     old `disabled_methods` array on a fake player record conflated the two. */
  const disabledMethods = useMemo(
    () => (p360BlockFeed.data || []).map(b => Number(b.method_id)),
    [p360BlockFeed.data]);
  const p360Save = useHrsSave([]);
  /* WAS AN IN-MEMORY DATABASE. `persist` wrote the patch into MOCK.PLAYERS_LIST
     and into the passed-in object, so a suspension survived navigating back to
     the list and looked saved — until a reload. `users.blocked` is the real
     column, written through the same allowlist path the Players screen uses. */
  const persist = (patch) => {
    if (!player) return Promise.resolve(null);
    return p360Save.run(() => window.sb.update("users", player.id, patch), {
      done: `${player.name || player.id} updated`,
      fail: `${player.name || player.id} was not updated`,
    });
  };
  /* The switch advances only after the write returns ok — a refused block
     leaves it where it was, which is the truth. */
  const toggleSuspend = () => {
    const next = !suspended;
    persist({ blocked: next }).then(res => { if (res && res.ok) setSuspended(next); });
  };
  /* NO WRITE PATH YET. `user_payment_method_blocks` is readable (028) and
     revoked for writes — insert/update/delete are off for `authenticated`, and
     it is not in the write allowlist, so blocking a method for one player is a
     decision with no RPC behind it. Shown, not toggled: a switch that flips and
     does nothing is the pattern this pass removes.
     <!-- SUGGESTION: blocking one payment method for one player needs an RPC — the row records who blocked it and why, so it is an audited decision rather than a column list, the same shape set_kyc_status takes. --> */
  const p360MethodFeed = useHrsFetch(() => window.sb.list("paymentMethods", { limit: 300 }), []);
  const p360Methods = useMemo(
    () => (p360MethodFeed.data || []).map(m => ({ id: Number(m.id), name: m.name })),
    [p360MethodFeed.data]);
  /* The sidebar's method panel gates on BOTH feeds: "AVAILABLE" while the
     block rows are still in flight (or failed to load) is the same mid-fetch
     confidence the old "All methods enabled" line had (canon §2.7 — empties
     and confident states never show mid-fetch). */
  const p360PmBusy = p360MethodFeed.loading || p360BlockFeed.loading;
  const p360PmErr = p360MethodFeed.error || p360BlockFeed.error;

  /* Player notes — operator-authored, persisted on the player record. */
  /* `user_notes` is the real table. The old version seeded the list from a
     string on the curated profile — so every player opened showing Marco's
     note, attributed to "System". */
  const notes = useMemo(() => (p360NoteFeed.data || []).map(n => ({
    id: String(n.id),
    author: n.author ? n.author.username : "—",
    ts: Date.parse(n.created_at) || 0,
    text: n.body || "",
    internal: !!n.internal,
    pinned: !!n.pinned,
  })), [p360NoteFeed.data]);
  const [noteDraft, setNoteDraft] = useState("");
  /* THE AUTHOR IS NOT THE BROWSER'S TO DECIDE. This read `MOCK.OP_USERS[0].name`
     — the first row of a fixture list — so every note in the prototype was
     signed by the same invented person regardless of who was signed in.
     `author_id` is filled by a trigger from the session (supabase/056) and is
     not in the write allowlist, so it cannot be sent from here at all. The
     label below names whoever is actually signed in, purely so the operator can
     see what the note will say. */
  const [p360Me, setP360Me] = useState(null);
  React.useEffect(() => {
    let alive = true;
    Promise.resolve(window.sb.me()).then(r => { if (alive && r && r.ok) setP360Me(r.data); });
    return () => { alive = false; };
  }, []);
  const operatorName = (p360Me && (p360Me.username || p360Me.email)) || "the signed-in operator";
  const p360NoteSave = useHrsSave([p360NoteFeed]);
  const addNote = () => {
    const text = noteDraft.trim();
    if (!text || !pid) return;
    p360NoteSave.run(
      () => window.sb.create("userNotes", {
        user_id: pid, body: text, internal: true, pinned: false,
      }),
      { done: "Note added", fail: "Note not added" },
    ).then(res => { if (res && res.ok) setNoteDraft(""); });
  };
  /* Soft delete, decided by the schema — app_write reads
     write_allowlist.soft_delete and stamps deleted_at with the SERVER's clock.
     The note stays on the record; it leaves the list. */
  const removeNote = (id) => {
    p360NoteSave.run(() => window.sb.remove("userNotes", id),
      { done: "Note withdrawn", fail: "Note not withdrawn" });
  };

  /* The demo-toast quick actions are gone. Both Quick actions render through
     NoBackend (honest disabled controls), and the helper they used fired
     `PAYBO.emitToast` with the message stuffed into `tx_id` and currency
     "DEMO" — the exact shape-hack the design canon bans (§2.13). Dead code
     either way; deleted rather than kept as a temptation. (`LimitBar`, the
     inline-styled bar widget from the invented-limits era, went the same way
     — nothing rendered it since the real `user_limits` feed landed.) */

  const initials = (P.name || "??").split(/\s+/).filter(Boolean).map(s => s[0].toUpperCase()).slice(0,2).join("");

  /* Tab strip content — labels + hover hints. The strip itself is the shared
     PillTabs (canon §2.15); the per-tab Tip popovers became native `title`
     hints, which is what PillTabs carries. */
  const tabDefs = [
    { key: "overview", label: "Overview", title: "Snapshot of the player's profile and quick actions for this player." },
    { key: "transactions", label: "Transactions", title: "Full transaction log for this player, scoped to this brand. Same status / type filters as the global Transactions page." },
    { key: "limits", label: "Limits", title: "Money limits (Daily / Weekly / Monthly) set on this player, with who set them and any scheduled change." },
    { key: "kyc", label: "KYC", title: "KYC documents and screening results. Placeholder in v1 — ships fully in the next version." },
    { key: "notes", label: "Notes", title: "Operator-written notes attached to this player. Persist on the account record and are visible to every operator working the player." },
  ];

  return (
    <div className="page">
      <style>{P360_CSS}</style>
      {onBack && (
        <button className="btn btn--ghost btn--sm p360-back" onClick={onBack}>
          <Icon name="chevron_left" size={12}/> Back to Players
        </button>
      )}
      <div className="page__header">
        <div className="p360-id">
          <div className="avatar avatar--lg p360-avatar">{initials}</div>
          <div>
            <div className="p360-titlerow">
              <div className="page__title">{P.name}</div>
              <span className="chip chip--gold"><Icon name="crown" size={10}/> {P.level}</span>
              <span className="chip chip--ok"><Icon name="shield" size={10}/> KYC {P.kyc || "—"}</span>
              <span className="chip chip--neutral">{P.country || "—"}</span>
            </div>
            <div className="page__subtitle">
              <span className="mono">{P.user_id}</span> · {P.email || "—"} · {P.phone || "—"} · Joined {P.joined_at ? new Date(P.joined_at).toLocaleDateString("en-GB") : "—"} · Last seen {(() => {
                /* An absent last_active used to render "just now" (the diff
                   fell back to Date.now()) — an invented recency. Absent is
                   "—". */
                if (!P.last_active) return "—";
                const diff = Date.now() - P.last_active;
                if (diff < 60_000) return "just now";
                if (diff < 3600_000) return `${Math.floor(diff/60_000)}m ago`;
                if (diff < 86400_000) return `${Math.floor(diff/3600_000)}h ago`;
                if (diff < 7*86400_000) return `${Math.floor(diff/86400_000)}d ago`;
                return new Date(P.last_active).toLocaleDateString("en-GB", { day:"2-digit", month:"short" });
              })()}
            </div>
          </div>
        </div>
        <div className="page__actions">
          {/* While suspended the prominent action is red — that used to be an
              inline err-500 override on btn--primary; .btn--danger is the
              shared form (§2.4). */}
          <button className={`btn btn--sm ${suspended ? "btn--danger" : "btn--secondary"}`} onClick={toggleSuspend}>
            <Icon name="lock" size={13}/> {suspended ? T("btn.reactivate","Reactivate account") : T("btn.suspend","Suspend account")}
          </button>
          <Tip>
            {suspended
              ? <>Lift the suspension and let the player transact again. Their saved methods, limits, notes and KYC state are preserved — only the suspend flag flips back to false.</>
              : <>Block every transaction (deposit and withdrawal) on this account until you reactivate. The account record, methods, limits and notes are kept on file — you can lift the suspension at any time from the same button.</>}
          </Tip>
          <NoBackend className="btn btn--primary btn--sm" what="Edit player"
            need="the player-record editor endpoint (role, country, KYC level, contact, limit overrides)">
            <Icon name="edit" size={13}/> {T("btn.editPlayer","Edit player")}
          </NoBackend>
          <Tip>Opens the player-record editor — change role, country, KYC level, contact info, and (where supported) override individual money / count limits without touching the level defaults.</Tip>
        </div>
      </div>

      {/* Suspension banner — shown when the entity is globally suspended.
          Shared HrsNotice callout (canon §4.8) instead of the hand-rolled
          err-tinted strip with hex fallbacks. */}
      {suspended && (
        <HrsNotice tone="err" icon="lock" title="Account suspended"
          actions={<span className="chip chip--err">SUSPENDED</span>}>
          Login + all payment methods blocked. Deposits and withdrawals will be rejected at the engine. Click "Reactivate account" above to lift.
        </HrsNotice>
      )}

      {/* Role badge row — helps operators see at a glance what tier they're
          looking at. The old chip was painted from a hardcoded hex map
          (#16a34a + a color+'22' alpha composite) and always said "Player",
          even though this row only renders for NON-player roles. Chip tone
          ramp, and the role the list actually carried. */}
      {player?.role && player.role !== "PLAYER" && (
        <div className="p360-role">
          <span className="chip chip--ok"><Icon name="crown" size={10}/> {P.role}</span>
          <span className="dim">Network account — sits above end-players in the distribution tree.</span>
        </div>
      )}

      <Explainer compact title="What this is, in plain English"
        bullets={[
          <><strong>Lifetime deposits</strong> — sum of every successful deposit this player has made on this brand.</>,
          <><strong>Lifetime withdrawals</strong> — sum of every successful withdrawal. Settled only — pending and to-confirm are not counted until they leave the wallet.</>,
          <><strong>Net deposits</strong> = lifetime deposits − lifetime withdrawals. Positive = casino is up on the player; negative = player is net-up (a heavy-winner signal).</>,
          <><strong>Tabs</strong> — Overview (profile), Transactions (full log), Limits (money caps with who set them), KYC (placeholder in v1), Notes (operator-written context).</>,
        ]}>
        The 360° view of <strong>{P.name}</strong>. Click the back arrow to return to the player list.
      </Explainer>

      {/* KPIs */}
      <div className="grid grid-3 p360-kpis">
        <div className="kpi">
          <div className="kpi__label">Lifetime deposits
            <Tip><strong>Lifetime deposits</strong> = sum of every successful deposit this player has ever made on this brand. The sub-row shows the count so you can tell a high-volume / low-stake player from a true VIP.</Tip>
          </div>
          <div className="kpi__value"><Money amount={P.lifetime_deposits} currency={P.currency}/></div>
          {/* The sub-row's "avg" figure read P.avg_deposit — a field the
              mapper never produces (it belonged to the deleted curated
              profile). The count is real and stays; the average goes rather
              than render as NaN wearing a currency symbol. */}
          <div className="p360-kpi-sub">{P.deposits_count} deposits</div>
        </div>
        <div className="kpi">
          <div className="kpi__label">Lifetime withdrawals
            <Tip><strong>Lifetime withdrawals</strong> = sum of every successful withdrawal this player has cashed out. Includes only settled withdrawals — pending and to-confirm ones are not counted until they actually leave the wallet.</Tip>
          </div>
          <div className="kpi__value"><Money amount={P.lifetime_withdrawals} currency={P.currency}/></div>
          <div className="p360-kpi-sub">{P.withdrawals_count} withdrawals</div>
        </div>
        <div className="kpi">
          <div className="kpi__label">Net deposits
            <Tip><strong>Net deposits</strong> = Lifetime deposits − Lifetime withdrawals. Positive means the casino has taken in more than it has paid out to this player overall (the usual case for active players); negative means the player is net-up on lifetime — a heavy-winner signal worth a review.</Tip>
          </div>
          {/* The value used to be green unconditionally with a static
              "Positive lifetime value" caption — for a figure that can be
              negative. Sign decides both now; a green negative is an invented
              claim. */}
          <div className={`kpi__value ${Number(P.net_deposits) < 0 ? "p360-neg" : "p360-pos"}`}><Money amount={P.net_deposits} currency={P.currency}/></div>
          <div className="p360-kpi-sub">{Number(P.net_deposits) < 0 ? "Player is net-up lifetime — heavy-winner signal" : "Positive lifetime value"}</div>
        </div>
      </div>

      <div className="p360-cols">
        <div className="panel p360-main">
          <div className="p360-tabs">
            <PillTabs tabs={tabDefs} active={tab} onChange={setTab} ariaLabel="Player sections"/>
          </div>
          {tab === "overview" && (
            <div className="p360-tabbody">
              <div className="grid grid-2">
                <div>
                  <div className="p360-h">Profile</div>
                  {/* Two rows are gone from this grid: "Auto-approve Enabled
                      (threshold €1,500)" was an invented configuration — no
                      auto-approve threshold exists on the player record or on
                      any feed this page reads — and "Open balance" read
                      P.open_balance, which the mapper never produces
                      (balances live on wallets, and the list does not carry
                      one). */}
                  <div className="p360-def">
                    <div className="dim">Email</div><div>{P.email || "—"}</div>
                    <div className="dim">Phone</div><div>{P.phone || "—"}</div>
                    <div className="dim">Country</div><div>{P.country || "—"}</div>
                    <div className="dim">Level</div><div><span className="chip chip--gold">{P.level}</span></div>
                  </div>
                </div>
                <div>
                  <div className="p360-h">Saved payment methods</div>
                  {/* THE CARDS WERE PROPS. Visa •••• 4421 / Skrill / Trustly
                      belonged to the deleted curated profile and rendered for
                      every player opened. No feed exposes stored payment
                      instruments, so the honest state is the absence, not
                      somebody's invented wallet.
                      <!-- SUGGESTION: listing stored instruments needs a feed
                           (PSP token vault / a user_payment_instruments
                           table); nothing in the readable schema carries them
                           today. --> */}
                  <div className="p360-empty-note">
                    No feed exposes this player's stored payment instruments (card tokens,
                    wallet accounts) in this prototype. Which platform methods the player
                    may use is listed under "Payment methods" on the right.
                  </div>
                </div>
              </div>
              {/* The old "Notes" callout here rendered P.notes — the single
                  seed comment on the deleted curated profile, shown for every
                  player. Real operator notes live on the Notes tab. */}
            </div>
          )}
          {tab === "transactions" && (
            <div className="p360-tabbody">
              {p360TxFeed.loading && <HrsSkeleton rows={8} cols={6} />}
              {!p360TxFeed.loading && p360TxFeed.error && (
                <HrsError error={p360TxFeed.error} onRetry={p360TxFeed.retry} />
              )}
              {!p360TxFeed.loading && !p360TxFeed.error && PLAYER_TX.length === 0 && (
                <HrsEmpty>
                  No ledger movements are recorded for this player. Nothing in <code>ledger_entries</code> names
                  this account's real wallet.
                </HrsEmpty>
              )}
              {!p360TxFeed.loading && !p360TxFeed.error && PLAYER_TX.length > 0 && (
                <div>
                  <div className="p360-txwrap">
                    <table className="data-table">
                      {/* METHOD AND PROVIDER ARE GONE, and their absence is the point.
                          A `ledger_entries` row records that money moved — user,
                          type, amount, wallet, balance_after. It carries NO payment
                          method: the method lives on `deposit_requests`, one table
                          away, and only when a request settled into that entry at
                          all. The columns that used to sit here read `t.method`,
                          `t.brand`, `t.brand_name` and `t.brand_short` off a row this
                          screen's own mapper never produces — every one of them was
                          `undefined`, so the Provider lookup was called with an empty
                          brand and an empty method id and could only ever return
                          nothing. A column that cannot populate is worse than a
                          missing one: it reads as "this transaction had no method".
                          <!-- SUGGESTION: to show the method here the feed needs
                               deposit_requests joined on ledger_entry_id, the way
                               report_player_conversion does it. --> */}
                      <thead><tr><th>ID</th><th>Type</th><th>Description</th><th className="p360-r">Amount</th><th>Status</th><th>Date</th></tr></thead>
                      <tbody>
                        {p360TxRows.map(t => (
                          <tr key={t.id}>
                            <td><CopyableId value={t.id}/></td>
                            <td><TypeChip type={t.type}/></td>
                            {/* The ledger's own description, whole. It used to be
                                `.split(" ")[0]` — the first word of a sentence,
                                presented as if it were a method name. */}
                            <td className="p360-td-desc">{t.method_name || <span className="dim">—</span>}</td>
                            <td className="p360-td-amt"><Money amount={t.amount} currency={t.currency}/></td>
                            <td><StatusChip status={t.status}/></td>
                            <td className="p360-td-date">{formatTs(t.created_at)}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                  {/* The feed's own cap, said out loud rather than silently
                      truncated (honesty canon — the same reasoning the Limits
                      tab documents). */}
                  {PLAYER_TX.length === 300 && (
                    <div className="p360-cap">The feed returns the most recent 300 ledger entries — older movements are not fetched.</div>
                  )}
                  <HrsPager page={p360TxPageSafe} pageSize={p360TxSize} total={PLAYER_TX.length}
                    onPage={setP360TxPage}
                    onPageSize={(n) => { setP360TxSize(n); setP360TxPage(0); }} />
                </div>
              )}
            </div>
          )}
          {tab === "limits" && (
            <div className="p360-tabbody">
              {/* THE WHOLE TAB WAS INVENTED, in two different ways.

                  The amount bars read P.limits.deposit.used_daily — fields of
                  the curated fake player, so every player's page showed the
                  same three deposit caps and the same consumption against them.

                  The count table below them was worse, because it was labelled
                  LIVE: the counts were real (derived from the transaction
                  list), the CAPS were four hardcoded arrays with a comment
                  saying "Default caps mirror Methods.jsx defaults — in
                  production these would come from the method-level limit
                  record". A real number over an invented denominator is a
                  breach indicator that fires on nothing.

                  `user_limits` is the real table and it stores CAPS, not
                  consumption — one row per (kind, period) with who set it and
                  what it is scheduled to become. Consumption against a limit is
                  not stored anywhere and is deliberately not computed here: the
                  ledger feed on this page is the most recent 300 entries, so a
                  monthly total taken from it would be a real-looking number
                  that is short by however much fell off the end.
                  <!-- SUGGESTION: consumption-against-limit needs a server-side
                       aggregate (sum of settled deposits/withdrawals per rolling
                       window per player), the same shape as the report views. It
                       cannot be derived from a page of ledger rows without
                       silently under-reporting. --> */}
              <div className="p360-limintro">
                Limits set on this player specifically. A limit the PLAYER set on themselves is not one an
                operator may raise — <code>set_by</code> is what makes that rule expressible, so it is shown.
                A raise is stored as pending rather than applied, because a reduction takes effect
                immediately and a raise must not.
              </div>
              {p360LimitFeed.loading && <HrsSkeleton rows={3} />}
              {!p360LimitFeed.loading && p360LimitFeed.error && (
                <HrsError error={p360LimitFeed.error} onRetry={p360LimitFeed.retry} />
              )}
              {!p360LimitFeed.loading && !p360LimitFeed.error && p360Limits.length === 0 && (
                <HrsEmpty>
                  No limits are set on this player. Nothing in <code>user_limits</code> names this account, so only
                  the platform and skin defaults apply — which is not the same as unlimited.
                </HrsEmpty>
              )}
              {!p360LimitFeed.loading && !p360LimitFeed.error && p360Limits.length > 0 && (
                <table className="data-table p360-limits">
                  <thead>
                    <tr>
                      <th>Limit</th>
                      <th>Period</th>
                      <th className="p360-r">Amount</th>
                      <th>Set by</th>
                      <th>In force from</th>
                      <th>Scheduled change</th>
                    </tr>
                  </thead>
                  <tbody>
                    {p360Limits.map(l => (
                      <tr key={l.id}>
                        <td className="p360-lk">{l.kind}</td>
                        <td className="p360-lp">{l.period}</td>
                        <td className="p360-td-amt">
                          <Money amount={l.amount} currency={l.currency || P.currency}/>
                        </td>
                        <td className="p360-lsetby">
                          {l.set_by}{l.setByName ? ` · ${l.setByName}` : ""}
                        </td>
                        <td className="p360-td-date">{p360When(l.effective_from)}</td>
                        <td className="p360-lsched">
                          {l.pending_amount == null
                            /* Not "none" — an em dash, because "no scheduled
                               change" and "a change we failed to read" must not
                               render the same. */
                            ? <span className="dim">—</span>
                            : <><Money amount={l.pending_amount} currency={l.currency || P.currency}/>
                                <span className="dim"> from {p360When(l.pending_from)}</span></>}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              )}
              {/* The two buttons that used to sit here — "Edit player limits"
                  and "Reset to Gold defaults" — did nothing, and the second
                  named a tier scheme this schema does not have. `user_limits`
                  has no write path: raising a player-set limit is a regulated
                  act with a cooling-off period, and a column list cannot
                  express it.
                  <!-- SUGGESTION: setting a player limit needs an RPC that
                       refuses to raise a set_by='player' limit outright, and
                       writes a raise into pending_amount/pending_from rather
                       than applying it — the reduction-now/raise-later split
                       the table was designed for. --> */}
              <div className="p360-footnote">
                Limits are read-only here. Changing one is not a field edit — a reduction applies immediately,
                a raise must be scheduled, and a limit the player set on themselves cannot be raised by an
                operator at all. None of that fits a save button, so there is not one.
              </div>
            </div>
          )}
          {tab === "kyc" && (
            <div className="p360-tabbody">
              {/* The shared V2Placeholder — same copy the bespoke dashed card
                  carried, on the app-wide "parked for a future release"
                  chrome. */}
              <V2Placeholder title="It will be displayed in a second version">
                KYC documents, sanctions / PEP screening, and provider integration ship in the next version. Hook is reserved here.
              </V2Placeholder>
            </div>
          )}
          {tab === "notes" && (
            <div className="p360-tabbody">
              {/* The notes feed's states were never rendered — the tab claimed
                  "No notes yet" while the fetch was still in flight or had
                  failed (defect pattern 6). Same skeleton/error/empty contract
                  as the Limits tab now. */}
              {p360NoteFeed.loading && <HrsSkeleton rows={3} cols={2} />}
              {!p360NoteFeed.loading && p360NoteFeed.error && (
                <HrsError error={p360NoteFeed.error} onRetry={p360NoteFeed.retry} />
              )}
              {!p360NoteFeed.loading && !p360NoteFeed.error && (
                <div className="p360-notes-list">
                  {notes.length === 0 && (
                    <HrsEmpty>No notes yet. Use the editor below to add the first one.</HrsEmpty>
                  )}
                  {notes.map(n => (
                    <div key={n.id} className="p360-note-card">
                      <div className="p360-note-head">
                        <span className="p360-note-author">{n.author}</span>
                        <span className="p360-note-when">
                          {new Date(n.ts).toLocaleString("en-GB", { day:"2-digit", month:"short", year:"numeric", hour:"2-digit", minute:"2-digit" })}
                        </span>
                        <button className="btn btn--ghost btn--icon btn--sm p360-mla" title="Delete note" onClick={() => removeNote(n.id)}>
                          <Icon name="trash" size={11}/>
                        </button>
                      </div>
                      <div className="p360-note-body">{n.text}</div>
                    </div>
                  ))}
                </div>
              )}
              <div className="p360-composer">
                <div className="p360-h">
                  Add note as {operatorName}
                </div>
                <textarea className="input p360-notearea" value={noteDraft} onChange={e => setNoteDraft(e.target.value)}
                  onKeyDown={e => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); addNote(); } }}
                  rows={3}
                  placeholder="Spoke with player about delayed withdrawal, escalating to ops…"/>
                <div className="p360-composer-foot">
                  <span className="p360-composer-hint">Notes are tied to this account and visible to every operator.</span>
                  <button className="btn btn--primary btn--sm p360-mla" disabled={!noteDraft.trim()} onClick={addNote}>
                    <Icon name="plus" size={12}/> Add note
                  </button>
                </div>
              </div>
            </div>
          )}
        </div>

        <div className="p360-side">
          <div className="panel">
            <div className="section__head">
              <div className="section__title p360-st">
                Quick actions
                <Tip>Operator-driven money movements on the player's account. Both move real balance and write a ledger row, so neither is simulated here — they need the operator credit/debit endpoints. On the real platform each opens a guided flow (counterparty, amount, reason) and is disabled while the account is suspended.</Tip>
              </div>
            </div>
            <div className="p360-qa">
              <NoBackend className="btn btn--secondary btn--sm" what="Manual deposit"
                need="the operator-credit endpoint — moves real balance and writes a ledger row"
                title="Credit the player's balance with an operator-driven deposit (e.g. recovering a bank wire that didn't auto-match) — not wired in this prototype.">
                <Icon name="arrow_down" size={13}/> {T("btn.manualDeposit","Manual deposit")}
              </NoBackend>
              <NoBackend className="btn btn--secondary btn--sm" what="Manual withdraw"
                need="the operator-debit endpoint — moves real balance and writes a ledger row"
                title="Book an operator-driven withdrawal from the player's balance (e.g. payout outside the standard cash-out flow) — not wired in this prototype.">
                <Icon name="arrow_up" size={13}/> {T("btn.manualWithdraw","Manual withdraw")}
              </NoBackend>
            </div>
          </div>

          {/* Payment method gating — which methods are BLOCKED for this player.
              The list is the real payment_methods catalogue; the blocks are
              rows in user_payment_method_blocks (presence is the block). The
              master switch and the per-row switches are gone: there is no write
              path, and a switch that flips and does nothing is worse than no
              switch, because the operator walks away believing the method is
              off.
              <!-- SUGGESTION: blocking one payment method for one player needs
                   an RPC — the row records who blocked it and why, so it is an
                   audited decision rather than a column list, the same shape
                   set_kyc_status takes. --> */}
          <div className="panel">
            <div className="section__head">
              <div>
                <div className="section__title">Payment methods</div>
                <div className="section__desc">
                  {p360PmBusy
                    /* NOT "All methods enabled" WHILE LOADING. The old line
                       computed this from two empty arrays, so every player
                       showed a confident "All methods enabled" for as long as
                       the fetch took — including forever, if it failed. Both
                       feeds gate the claim: "available" while the BLOCK rows
                       are still in flight is the same lie. */
                    ? "Loading the method catalogue…"
                    : p360PmErr
                      ? "The method list could not be loaded."
                      : disabledMethods.length === 0
                        ? `All ${p360Methods.length} method(s) available to this player`
                        : `${disabledMethods.length} of ${p360Methods.length} method(s) blocked`}
                </div>
              </div>
            </div>
            <div className="p360-methods">
              {p360PmBusy && <HrsSkeleton rows={4} cols={2} />}
              {!p360PmBusy && p360PmErr && (
                <HrsError error={p360PmErr} onRetry={(p360MethodFeed.error ? p360MethodFeed : p360BlockFeed).retry} />
              )}
              {!p360PmBusy && !p360PmErr && p360Methods.length === 0 && (
                <HrsEmpty>No payment methods are configured on the platform yet.</HrsEmpty>
              )}
              {!p360PmBusy && !p360PmErr && p360Methods.map(m => {
                const off = disabledMethods.includes(m.id);
                return (
                  <div key={m.id} className={`p360-method${off ? " is-off" : ""}`}>
                    <span className="p360-method-name">{m.name}</span>
                    <span className="p360-method-state">{off ? "BLOCKED" : "AVAILABLE"}</span>
                  </div>
                );
              })}
              <div className="p360-cap">
                Read-only. Blocking a method for one player is an audited decision, not a field edit —
                the row records who blocked it and why, and there is no RPC for it yet.
              </div>
            </div>
          </div>

          {/* THE 90-DAY CHART WAS A PROP. A "Deposit vs withdraw (90d)" panel
              rendered a hardcoded Sparkline series with hardcoded
              "Jan 18 / Apr 18" axis labels and a raw rgba() fill — the same
              picture for every player ever opened. A real series cannot be
              derived here either: the ledger feed is the most recent 300
              entries, so a 90-day aggregate folded from it would silently
              under-report (the same reasoning the Limits tab documents), so
              the panel is gone rather than faked.
              <!-- SUGGESTION: a deposit-vs-withdraw time series needs a
                   server-side aggregate (per-day sums per player), the same
                   shape as the report views — not a client-side fold over one
                   page of ledger rows. --> */}

        </div>
      </div>
    </div>
  );
};

window.Player360 = Player360;
