/* PayBO v1 Engine — feature flags, auto-approval, limits reservation,
   activity log actions, CSV export, toast bus.
   Runs AFTER data.jsx: augments window.MOCK with v1-compliant fields and
   exposes window.PAYBO with the pure engine API. */

(() => {
  const FEATURE_KYC    = false;
  const FEATURE_RISK   = false;
  const FEATURE_LEVELS = false;

  /* ---------- STATUS + ACTION TAXONOMY ---------- */
  const STATUS = {
    CREATED:    "created",
    TO_CONFIRM: "to_confirm",
    DECLINED:   "declined",
    REJECTED:   "rejected",
    APPROVED:   "approved",
    PENDING:    "pending",
    BALANCED:   "balanced",
    FAILED:     "failed",
  };
  const TERMINAL = new Set(["declined","rejected","balanced","failed"]);
  const RESERVING = new Set(["created","to_confirm","approved","pending","balanced"]);

  const ACTION = {
    CREATED:"CREATED", AUTO_APPROVED:"AUTO_APPROVED",
    FLAGGED_TO_CONFIRM:"FLAGGED_TO_CONFIRM", DECLINED:"DECLINED",
    OPERATOR_APPROVED:"OPERATOR_APPROVED", OPERATOR_REJECTED:"OPERATOR_REJECTED",
    OPERATOR_PARTIAL_APPROVED:"OPERATOR_PARTIAL_APPROVED",
    PARTIAL_REJECT:"PARTIAL_REJECT",
    SENT_TO_PROVIDER:"SENT_TO_PROVIDER",
    PROVIDER_ACCEPTED:"PROVIDER_ACCEPTED",
    PROVIDER_REJECTED:"PROVIDER_REJECTED",
    BALANCED:"BALANCED", FAILED:"FAILED",
  };

  const REASONS = {
    approve: [
      { code: "VERIFIED_BY_PHONE", label: "Verified by phone" },
      { code: "KYC_COMPLETE",      label: "KYC complete" },
      { code: "TRUSTED_PLAYER",    label: "Trusted player" },
      { code: "OTHER",             label: "Other" },
    ],
    reject: [
      { code: "LIMIT_IWAKIRI",     label: "Limit exceeded in Iwakiri side" },
      { code: "LIMIT_PROVIDER",    label: "Limit exceeded in Provider side" },
      { code: "NETWORK_IWAKIRI",   label: "Network error in Iwakiri side" },
      { code: "NETWORK_PROVIDER",  label: "Network error in Provider side" },
      { code: "TIMEOUT_PROVIDER",  label: "Provider timeout" },
      { code: "REJECTED_OPERATOR", label: "Transaction rejected by operator" },
    ],
    partial: [
      { code: "PARTIAL_LIMIT",    label: "Partial limit" },
      { code: "RISK_MITIGATION",  label: "Risk mitigation" },
      { code: "OTHER",            label: "Other" },
    ],
  };

  /* ---------- METHOD THRESHOLDS + LIMITS (v1 seed) ---------- */
  // Per-method × per-currency threshold above which tx → To Confirm.
  const METHOD_THRESHOLDS = {
    visa:      { EUR: 500,  USD: 550,  GBP: 450, BRL: 2500, CAD: 700 },
    mastercard:{ EUR: 500,  USD: 550,  GBP: 450, BRL: 2500, CAD: 700 },
    bankwire:  { EUR: 2000, USD: 2200, GBP: 1800, BRL: 10000, CAD: 2800 },
    bitcoin:   { EUR: 1000, USD: 1100, GBP: 900, BRL: 5000, CAD: 1400 },
    ethereum:  { EUR: 1000, USD: 1100, GBP: 900, BRL: 5000, CAD: 1400 },
    skrill:    { EUR: 600,  USD: 650,  GBP: 550, BRL: 3000, CAD: 850 },
    neteller:  { EUR: 600,  USD: 650,  GBP: 550, BRL: 3000, CAD: 850 },
    paysafe:   { EUR: 250,  USD: 275,  GBP: 225, BRL: 1250, CAD: 350 },
    // legacy aliases so the existing prototype data keeps working
    trustly:   { EUR: 800,  USD: 880,  GBP: 720, BRL: 4000, CAD: 1100 },
    crypto:    { EUR: 1000, USD: 1100, GBP: 900, BRL: 5000, CAD: 1400 },
  };
  const WINDOWS = ["DAILY","WEEKLY","MONTHLY"];
  const WINDOW_MS = { DAILY: 86400_000, WEEKLY: 7*86400_000, MONTHLY: 30*86400_000 };
  // Per-method × per-currency × window × tx-type limit caps (player-level).
  const METHOD_LIMITS = (() => {
    const scale = { DAILY: 1, WEEKLY: 5, MONTHLY: 15 };
    const base = {
      visa:       { EUR: 1500, USD: 1700, GBP: 1300, BRL: 7500,  CAD: 2100 },
      mastercard: { EUR: 1500, USD: 1700, GBP: 1300, BRL: 7500,  CAD: 2100 },
      bankwire:   { EUR: 5000, USD: 5500, GBP: 4500, BRL: 25000, CAD: 7000 },
      bitcoin:    { EUR: 3000, USD: 3300, GBP: 2700, BRL: 15000, CAD: 4200 },
      ethereum:   { EUR: 3000, USD: 3300, GBP: 2700, BRL: 15000, CAD: 4200 },
      skrill:     { EUR: 2000, USD: 2200, GBP: 1800, BRL: 10000, CAD: 2800 },
      neteller:   { EUR: 2000, USD: 2200, GBP: 1800, BRL: 10000, CAD: 2800 },
      paysafe:    { EUR: 1000, USD: 1100, GBP: 900,  BRL: 5000,  CAD: 1400 },
      trustly:    { EUR: 2500, USD: 2750, GBP: 2250, BRL: 12500, CAD: 3500 },
      crypto:     { EUR: 3000, USD: 3300, GBP: 2700, BRL: 15000, CAD: 4200 },
    };
    const out = {};
    Object.keys(base).forEach(m => {
      out[m] = {};
      Object.keys(base[m]).forEach(cur => {
        out[m][cur] = {};
        WINDOWS.forEach(w => {
          out[m][cur][w] = {
            DEPOSIT:    Math.round(base[m][cur] * scale[w]),
            WITHDRAWAL: Math.round(base[m][cur] * scale[w] * 1.3),
          };
        });
      });
    });
    return out;
  })();

  /* ---------- ENGINE (pure functions) ---------- */
  const getGlobalThreshold = (method, currency) => {
    const row = METHOD_THRESHOLDS[method];
    if (!row) return 500; // fallback
    return row[currency] ?? row.EUR ?? 500;
  };
  const getPlayerLevelThreshold = (playerId, currency) => {
    // v2 stub — never called while FEATURE_LEVELS=false
    return getGlobalThreshold("visa", currency);
  };

  const kycRuleTriggered = (tx) => false; // v2 stub — always passes

  const computeRiskScore = (tx) => 0;     // v2 stub — always 0

  // Rolling-window used capacity for (player × method × currency × type × window)
  const usedInWindow = (tx, win, transactions) => {
    const since = Date.now() - WINDOW_MS[win];
    let used = 0;
    const source = transactions || (typeof window !== "undefined" && window.TX_STORE) || [];
    source.forEach(t => {
      if (!t) return;
      if (t.id === tx.id) return;
      if (t.user_id !== tx.user_id) return;
      if (t.method !== tx.method) return;
      if (t.currency !== tx.currency) return;
      if ((t.type || "").toLowerCase() !== (tx.type || "").toLowerCase()) return;
      if (!RESERVING.has(t.status)) return;
      if ((t.created_at || 0) < since) return;
      used += (t.approved_amount != null ? t.approved_amount : t.amount);
    });
    return used;
  };

  const exceedsLimits = (tx, transactions) => {
    const txType = (tx.type || "Deposit").toUpperCase();
    const methodLims = METHOD_LIMITS[tx.method];
    if (!methodLims) return { over:false };
    const curLims = methodLims[tx.currency] || methodLims.EUR;
    if (!curLims) return { over:false };
    for (const w of WINDOWS) {
      const cap = curLims[w]?.[txType];
      if (cap == null) continue;
      const used = usedInWindow(tx, w, transactions);
      if (used + (tx.approved_amount ?? tx.amount) > cap) {
        return { over:true, window:w, cap, used };
      }
    }
    return { over:false };
  };

  const availableCapacity = (playerId, method, currency, type, win, transactions) => {
    const cap = (METHOD_LIMITS[method]?.[currency]?.[win]?.[type.toUpperCase()]) ?? null;
    if (cap == null) return null;
    const fakeTx = { id:"__probe", user_id: playerId, method, currency, type, amount: 0, status:"created" };
    return Math.max(0, cap - usedInWindow(fakeTx, win, transactions));
  };

  // Core spec evaluator. Order: limits → KYC → risk → threshold → approve.
  const evaluate = (tx, transactions) => {
    const lim = exceedsLimits(tx, transactions);
    if (lim.over) {
      return { status: STATUS.DECLINED, reason: "LIMIT_IWAKIRI",
               meta: { window: lim.window, cap: lim.cap, used: lim.used } };
    }
    if (FEATURE_KYC && kycRuleTriggered(tx)) {
      return { status: STATUS.DECLINED, reason: "KYC_BLOCKED" };
    }
    // Business rule: manual confirm is a withdrawal-only flow. Deposits
    // never sit in To Confirm — they either auto-approve or terminate in
    // a failure status earlier in the pipeline.
    const isWithdrawal = (tx.type || "").toLowerCase() === "withdrawal";
    const risk = FEATURE_RISK ? computeRiskScore(tx) : 0;
    if (isWithdrawal && risk >= 70) {
      return { status: STATUS.TO_CONFIRM, reason: "HIGH_RISK", riskScore: risk };
    }
    const threshold = FEATURE_LEVELS
      ? getPlayerLevelThreshold(tx.user_id, tx.currency)
      : getGlobalThreshold(tx.method, tx.currency);
    if (isWithdrawal && (tx.approved_amount ?? tx.amount) > threshold) {
      return { status: STATUS.TO_CONFIRM, reason: "AMOUNT_OVER_THRESHOLD",
               meta: { threshold } };
    }
    return { status: STATUS.APPROVED, reason: "AUTO_APPROVED" };
  };

  /* ---------- ACTIVITY LOG (immutable, append-only) ---------- */
  let _seq = 1;
  const _log = [];
  const newId = () => `L${(_seq++).toString().padStart(6,"0")}`;

  const logEntry = (e) => {
    const entry = Object.freeze({
      id: newId(),
      transaction_id: e.transaction_id || null,
      timestamp: e.timestamp || Date.now(),
      actor_type: e.actor_type || "SYSTEM",
      actor_id: e.actor_id || null,
      action: e.action,
      reason_code: e.reason_code || null,
      note: e.note || null,
      before_state: e.before_state || null,
      after_state: e.after_state || null,
      metadata: e.metadata ? Object.freeze({ ...e.metadata }) : null,
    });
    _log.push(entry);
    return entry;
  };

  /* ---------- TOAST BUS (pub/sub) ---------- */
  const toastListeners = new Set();
  const emitToast = (t) => toastListeners.forEach(fn => { try { fn(t); } catch(e){} });
  const onToast = (fn) => { toastListeners.add(fn); return () => toastListeners.delete(fn); };

  /* ---------- CSV EXPORT ---------- */
  const toCSV = (rows, columns) => {
    const esc = (v) => {
      if (v == null) return "";
      const s = typeof v === "object" ? JSON.stringify(v) : String(v);
      return /[",\n]/.test(s) ? `"${s.replace(/"/g,'""')}"` : s;
    };
    const head = columns.map(c => esc(c.label ?? c.key)).join(",");
    const body = rows.map(r => columns.map(c => esc(c.get ? c.get(r) : r[c.key])).join(","));
    return [head, ...body].join("\n");
  };
  /* `notify` defaults on: a browser download is easy to miss (no page change,
     and Chrome's shelf is off by default), so every PayBO export used to give
     the operator no acknowledgement at all while the Host reports did. Host
     callers arrive through hrsCsv, which passes notify:false because HrsExport
     already posts its own "Export ready" line — otherwise they double up. */
  const downloadCSV = (filename, rows, columns, opts) => {
    const csv = toCSV(rows, columns);
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = filename;
    document.body.appendChild(a); a.click(); a.remove();
    if (!opts || opts.notify !== false) {
      emitToast({
        id: `csv-${filename}-${Date.now()}`,
        tx_id: "Export ready",
        amount: 0, currency: "CSV",
        player: `${(rows || []).length} row${(rows || []).length === 1 ? "" : "s"}`,
        reason: `${filename} downloaded. Check your browser's downloads if you do not see it.`,
      });
    }
    setTimeout(() => URL.revokeObjectURL(url), 250);
  };

  /* ---------- V1 SEED AUGMENTATION of window.MOCK ---------- */
  // Augments existing prototype tx to satisfy v1 schema without breaking pages.
  // Normalises status, injects approved_amount, reason_code, provider_ref, ip,
  // risk_score=0, risk_factors=[], and a per-tx activity log.
  /* SEEDED, NOT RANDOM — and this is the fix that matters most about this
     block, more than the fact that the values are placeholders at all.

     `Math.random()` here meant a transaction's PSP reference and a player's
     join date were DIFFERENT ON EVERY PAGE LOAD. Nine screens still read
     window.MOCK, so on those screens: a screenshot could not be reproduced, a
     bug report named a reference that no longer existed by the time anyone
     looked, and pressing F5 to check something changed the thing being
     checked. That is not "fake data is fine in a prototype" — it is fake data
     that will not hold still long enough to be argued with.

     pbRng is the seeded generator the rest of the codebase uses (src/data.jsx).
     Same shape, same placeholders, but the same ones every load.

     These values remain INVENTED and this file remains GENERATED in the
     wiring ledger. The remaining consumers are, in full:
       Activity · Config · Frontend · HostReportDaily · HostSkinsUnified
       Methods · PlayersList · Settings · Transactions
     Each is on the list to be wired; until then, at least it does not move. */
  const engRnd = pbRng(20260811);
  const rnd = () => engRnd();
  const RANDOM_REASON_BY_STATUS = {
    declined: "LIMIT_IWAKIRI",
    rejected: "REJECTED_OPERATOR",
    to_confirm: "AMOUNT_OVER_THRESHOLD",
    approved: "AUTO_APPROVED",
    balanced: "AUTO_APPROVED",
    pending: "AUTO_APPROVED",
    failed: "PROVIDER_REJECTED",
    created: null,
  };
  const ipLike = () => Array(4).fill(0).map(()=> 10 + Math.floor(rnd()*245)).join(".");

  const buildTxActivity = (tx) => {
    const out = [];
    const t0 = tx.created_at;
    const push = (dt, e) => out.push({ ...e, timestamp: t0 + dt });
    push(0, { action: ACTION.CREATED, actor_type: "SYSTEM",
              before_state: null, after_state: STATUS.CREATED,
              metadata: { amount: tx.amount, method: tx.method, ip: tx.ip_address } });
    const s = tx.status;
    if (s === "declined") {
      push(400, { action: ACTION.DECLINED, actor_type: "SYSTEM",
                  reason_code: "LIMIT_IWAKIRI",
                  before_state: STATUS.CREATED, after_state: STATUS.DECLINED,
                  metadata: { rule: "rolling-window" } });
    } else if (s === "to_confirm") {
      push(600, { action: ACTION.FLAGGED_TO_CONFIRM, actor_type: "SYSTEM",
                  reason_code: "AMOUNT_OVER_THRESHOLD",
                  before_state: STATUS.CREATED, after_state: STATUS.TO_CONFIRM });
    } else if (s === "rejected") {
      push(600, { action: ACTION.FLAGGED_TO_CONFIRM, actor_type: "SYSTEM",
                  before_state: STATUS.CREATED, after_state: STATUS.TO_CONFIRM });
      push(180_000, { action: ACTION.OPERATOR_REJECTED, actor_type: "OPERATOR", actor_id: "op1",
                       reason_code: "REJECTED_OPERATOR",
                       note: "Operator review — escalated to compliance.",
                       before_state: STATUS.TO_CONFIRM, after_state: STATUS.REJECTED });
    } else if (s === "approved" || s === "pending" || s === "balanced" || s === "failed") {
      const autoPath = s === "approved" || rnd() > 0.35;
      if (autoPath) {
        push(500, { action: ACTION.AUTO_APPROVED, actor_type: "SYSTEM",
                    reason_code: "AUTO_APPROVED",
                    before_state: STATUS.CREATED, after_state: STATUS.APPROVED });
      } else {
        push(500, { action: ACTION.FLAGGED_TO_CONFIRM, actor_type: "SYSTEM",
                    reason_code: "AMOUNT_OVER_THRESHOLD",
                    before_state: STATUS.CREATED, after_state: STATUS.TO_CONFIRM });
        push(90_000, { action: ACTION.OPERATOR_APPROVED, actor_type: "OPERATOR", actor_id: "op1",
                       reason_code: "VERIFIED_BY_PHONE",
                       note: "Confirmed identity via phone; trusted player profile.",
                       before_state: STATUS.TO_CONFIRM, after_state: STATUS.APPROVED });
      }
      if (s === "pending" || s === "balanced" || s === "failed") {
        push(100_000, { action: ACTION.SENT_TO_PROVIDER, actor_type: "SYSTEM",
                         before_state: STATUS.APPROVED, after_state: STATUS.PENDING,
                         metadata: { provider_ref: tx.provider_ref } });
      }
      if (s === "balanced") {
        push(360_000, { action: ACTION.PROVIDER_ACCEPTED, actor_type: "PROVIDER",
                         before_state: STATUS.PENDING, after_state: STATUS.PENDING });
        push(380_000, { action: ACTION.BALANCED, actor_type: "SYSTEM",
                         before_state: STATUS.PENDING, after_state: STATUS.BALANCED });
      } else if (s === "failed") {
        push(360_000, { action: ACTION.PROVIDER_REJECTED, actor_type: "PROVIDER",
                         before_state: STATUS.PENDING, after_state: STATUS.FAILED });
        push(380_000, { action: ACTION.FAILED, actor_type: "SYSTEM",
                         before_state: STATUS.PENDING, after_state: STATUS.FAILED });
      }
    }
    return out;
  };

  const augmentTransactions = () => {
    const TX = (window.MOCK?.TRANSACTIONS || []);
    TX.forEach(tx => {
      // Normalise aliases → v1 statuses
      if (tx.status === "completed") tx.status = "balanced";
      if (tx.status === "review")    tx.status = "to_confirm";
      if (tx.status === "cancelled") tx.status = "declined";
      // v1 fields
      if (tx.approved_amount === undefined) tx.approved_amount = null;
      if (tx.reason_code    === undefined)  tx.reason_code = RANDOM_REASON_BY_STATUS[tx.status] ?? null;
      /* DERIVED FROM THE TRANSACTION, not from the clock or a die. A PSP
         reference is the string an operator quotes to a payment provider when
         something is disputed; one that changes on reload is worse than none,
         because it looks quotable. Keyed on the tx id so it is stable and so
         two transactions never collide. */
      if (tx.provider_ref   === undefined)  tx.provider_ref = `PSP-${Number(String(tx.id).replace(/\D/g, "") || 0).toString(36).toUpperCase().padStart(6, "0")}`;
      if (tx.ip_address     === undefined)  tx.ip_address = ipLike();
      if (tx.risk_score     === undefined)  tx.risk_score = 0;      // v2 stub
      if (tx.risk_factors   === undefined)  tx.risk_factors = [];   // v2 stub
      if (tx.balanced_at    === undefined)  tx.balanced_at = tx.status === "balanced" ? tx.updated_at : null;
      if (tx.tx_type        === undefined)  tx.tx_type = (tx.type || "Deposit").toUpperCase();
      if (tx.threshold      === undefined)  tx.threshold = getGlobalThreshold(tx.method, tx.currency);
      if (tx.net_amount     === undefined)  tx.net_amount = +(tx.amount - tx.fee).toFixed(2);
      if (tx.player_level   === undefined)  tx.player_level = "STANDARD"; // v2 stub
      // Per-tx immutable activity (used by drawer)
      if (!tx.activity) {
        const entries = buildTxActivity(tx).map(e => ({ ...e, transaction_id: tx.id }));
        entries.forEach(e => logEntry(e));
        tx.activity = entries.map(e => ({ ...e, id: _log[_log.length - entries.length + entries.indexOf(e)].id }));
      }
    });
    window.TX_STORE = TX;
  };

  /* ---------- QUEUE helpers ---------- */
  const queueSort = (list) =>
    [...list].sort((a,b) => (b.amount - a.amount) || (a.created_at - b.created_at));

  /* ---------- STATUS VISUALIZER ---------- */
  const STATE_PILLS = [
    { key: "created",    label: "Created" },
    { key: "to_confirm", label: "To Confirm" },
    { key: "approved",   label: "Approved" },
    { key: "pending",    label: "Pending" },
    { key: "balanced",   label: "Balanced" },
  ];
  const ALT_PILLS = {
    declined: { key: "declined", label: "Declined" },
    rejected: { key: "rejected", label: "Rejected" },
    failed:   { key: "failed",   label: "Failed" },
  };

  /* ---------- BRAND helpers (single brand context) ---------- */
  const txsInBrand = (brand) => {
    const all = window.MOCK.TRANSACTIONS;
    if (!brand || brand.isAll) return all;
    return all.filter(t => t.brand === brand.id);
  };

  /* ---------- DASHBOARD KPIs ---------- */
  const kpis = (brand) => {
    const TX = txsInBrand(brand);
    const now = Date.now();
    const today = TX.filter(t => (now - t.created_at) < 86400_000);
    const volToday = today.filter(t => ["approved","pending","balanced","to_confirm"].includes(t.status))
                           .reduce((a,t) => a + t.amount, 0);
    const pendingReview = TX.filter(t => t.status === "to_confirm").length;
    const balancedToday = today.filter(t => t.status === "balanced").length;
    const terminal = TX.filter(t => TERMINAL.has(t.status));
    const approvalRate = terminal.length
      ? (TX.filter(t => ["approved","balanced"].includes(t.status)).length / TX.length) : 0;
    const failureRate  = terminal.length
      ? (TX.filter(t => ["failed","declined","rejected"].includes(t.status)).length / TX.length) : 0;
    const reviewEntered = TX.filter(t =>
      (t.activity || []).some(a => a.action === ACTION.FLAGGED_TO_CONFIRM)).length;
    const manualReviewRate = TX.length ? reviewEntered / TX.length : 0;
    return {
      volToday, pendingReview, balancedToday,
      total: TX.length,
      totalToday: today.length,
      last24h: TX.filter(t => (now - t.created_at) < 86400_000).length,
      approvedToday: today.filter(t => t.status === "approved").length,
      declinedToday: today.filter(t => t.status === "declined").length,
      approvalRate, failureRate, manualReviewRate,
    };
  };

  /* ---------- EXPORT ---------- */
  window.PAYBO = {
    FEATURES: { KYC: FEATURE_KYC, RISK: FEATURE_RISK, LEVELS: FEATURE_LEVELS },
    STATUS, TERMINAL, RESERVING, ACTION, REASONS,
    WINDOWS, WINDOW_MS,
    METHOD_THRESHOLDS, METHOD_LIMITS,
    getGlobalThreshold, getPlayerLevelThreshold,
    kycRuleTriggered, computeRiskScore,
    exceedsLimits, usedInWindow, availableCapacity,
    evaluate,
    log: _log, logEntry,
    onToast, emitToast,
    toCSV, downloadCSV,
    queueSort,
    STATE_PILLS, ALT_PILLS,
    txsInBrand, kpis,
    augmentTransactions,
  };

  /* ---------- PLAYERS LIST SEED (derived from transactions) ---------- */
  // Build a unique player list keyed by (brand_id, user_id) by aggregating
  // the seeded transactions. Enrich each with realistic KYC / level / country
  // / last-active / lifetime figures for the list view.
  const seedPlayersFromTx = () => {
    const TX = window.MOCK?.TRANSACTIONS || [];
    const byKey = new Map();
    TX.forEach(t => {
      const key = `${t.brand}:${t.user_id}`;
      if (!byKey.has(key)) {
        byKey.set(key, {
          id: t.user_id,
          brand: t.brand,
          brand_name: t.brand_name,
          brand_short: t.brand_short,
          brand_color: t.brand_color,
          name: t.user_name,
          email: (t.user_name || "player").toLowerCase()
                   .replace(/[^a-z]/g,".")
                   .replace(/\.+/g,".")
                   .replace(/^\.|\.$/g,"") + "@example.com",
          phone: t.phone,
          country: t.country,
          currency: t.currency,
          /* Was `Date.now() - random days`, which moved on every reload AND
             drifted forward in real time — a registration date that gets later
             the longer you leave the tab open. Derived from the player's own
             id instead: stable, and still plainly a placeholder. */
          joined_at: Date.now() - (30 + (Number(String(t.user_id || t.id).replace(/\D/g, "") || 0) % 700)) * 86400_000,
          last_active: t.created_at,
          lifetime_deposits: 0,
          lifetime_withdrawals: 0,
          deposit_count: 0,
          withdrawal_count: 0,
          risk_score: 0, // v2 stub — always 0 in v1
          level: "STANDARD",
        });
      }
      const p = byKey.get(key);
      p.last_active = Math.max(p.last_active, t.created_at);
      if (t.type === "Deposit") {
        p.deposit_count++;
        if (t.status === "balanced" || t.status === "approved" || t.status === "pending") {
          p.lifetime_deposits += (t.approved_amount ?? t.amount);
        }
      } else {
        p.withdrawal_count++;
        if (t.status === "balanced" || t.status === "approved" || t.status === "pending") {
          p.lifetime_withdrawals += (t.approved_amount ?? t.amount);
        }
      }
    });
    const players = Array.from(byKey.values());
    // Distribute KYC across Verified / Pending / Rejected with a realistic mix,
    // and assign a player level from the v2 placeholder ladder so the list
    // has chips to filter on (level remains a v2 feature but the field is
    // part of the schema and can already be populated).
    const KYC_STATES = ["Verified","Verified","Verified","Verified","Pending","Pending","Rejected"];
    const LEVELS = ["STANDARD","STANDARD","STANDARD","STANDARD","SILVER","SILVER","GOLD","VIP","DIAMOND"];
    players.forEach((p, i) => {
      p.kyc   = KYC_STATES[i % KYC_STATES.length];
      p.level = LEVELS[(i * 3 + 1) % LEVELS.length];
      p.net_position = +(p.lifetime_deposits - p.lifetime_withdrawals).toFixed(2);
      // Network role — defaults to PLAYER; some are promoted to higher tiers
      // via seedNetworkRoles() below so the Accounts list has representatives
      // of each pyramid level.
      p.role = "PLAYER";
      // Suspension state — global entity lockout, toggled from Player360.
      p.suspended = false;
      // Per-entity payment-method blocklist. A method id in this array means
      // "this account cannot use this method" regardless of role defaults.
      p.disabled_methods = [];
    });
    // Guarantee the pre-seeded Marco Ricci profile exists in the list so the
    // rich Player360 page still has a match.
    if (window.MOCK?.PLAYER && !players.some(p => p.id === window.MOCK.PLAYER.user_id)) {
      const P = window.MOCK.PLAYER;
      const defaultBrand = (window.MOCK.BRANDS || []).find(b => b.id === P.brand) || (window.MOCK.BRANDS || [])[0] || {};
      players.unshift({
        id: P.user_id,
        brand: defaultBrand.id,
        brand_name: defaultBrand.name, brand_short: defaultBrand.short,
        brand_color: defaultBrand.color,
        name: P.name, email: P.email, phone: P.phone, country: P.country,
        currency: defaultBrand.currency,
        joined_at: P.joined_at, last_active: P.last_active,
        lifetime_deposits: P.lifetime_deposits,
        lifetime_withdrawals: P.lifetime_withdrawals,
        deposit_count: P.deposits_count, withdrawal_count: P.withdrawals_count,
        risk_score: P.risk_score || 0,
        level: (P.level || "STANDARD").toUpperCase(),
        kyc: P.kyc || "Verified",
        net_position: P.net_deposits,
        role: "PLAYER",
        suspended: false,
        disabled_methods: [],
      });
    }
    // Promote a sprinkle of accounts to non-Player roles so the Accounts
    // list has representatives of each network tier. Deterministic by
    // index so the demo is stable across reloads.
    const PROMOTIONS = [
      { role: "MASTER",   count: 2 },
      { role: "PROMOTER", count: 3 },
      { role: "AGENT",    count: 5 },
      { role: "SHOP",     count: 8 },
      { role: "CASHIER",  count: 12 },
    ];
    const ROLE_NAME_PREFIX = { MASTER: "Master", PROMOTER: "Promoter", AGENT: "Agent", SHOP: "Shop", CASHIER: "Cashier" };
    let cursor = 0;
    for (const { role, count } of PROMOTIONS) {
      for (let k = 0; k < count && cursor < players.length; k++, cursor++) {
        const target = players[cursor];
        target.role = role;
        // Network accounts get a role-prefixed display name so they're
        // scannable in the list ("Shop — Marco R." etc.) while keeping
        // the original contact details intact.
        target.original_name = target.name;
        target.name = `${ROLE_NAME_PREFIX[role]} — ${target.name}`;
      }
    }
    // Seed a couple of suspended accounts + a couple with disabled methods
    // so the UI has realistic states to render out of the box.
    if (players[7])  players[7].suspended = true;
    if (players[22]) players[22].suspended = true;
    if (players[3])  players[3].disabled_methods = ["bitcoin", "ethereum"];
    if (players[15]) players[15].disabled_methods = ["bankwire"];
    // Sort most-recently-active first
    players.sort((a, b) => b.last_active - a.last_active);
    window.MOCK.PLAYERS_LIST = players;
    return players;
  };

  /* ---------- bootstrap ---------- */
  // Data script has already run. Perform the augment + seed a fake toast timer.
  augmentTransactions();
  seedPlayersFromTx();

  /* The simulated "New - To Confirm" feed was removed (Arbi, Aug 2026).
     It re-fired every 45-90s forever, so a toast reappeared no matter what the
     operator was doing; on a phone it covered the page it was reporting on.
     Nothing depended on it - the To Confirm queue is already visible on the
     Transactions and Deposits screens - and a demo of realtime arrival is not
     worth an interruption that cannot be turned off.
     ToastHost and HostNotices remain: real feedback from real clicks still
     shows. This only removes the timer that invented notifications.
     <!-- SUGGESTION: if a live feed is wanted later, drive it from a real
          subscription and give the operator a mute that persists. --> */
})();
