/* Mock data for the prototype */

/* ------- the seeded PRNG, once -------------------------------------------
   49 page files each carried their own copy of this exact mulberry32, with a
   page-unique alias to dodge the implicit-global collision. Identical
   algorithm, 49 declarations. New code uses these; the existing copies are
   being retired file by file (they are byte-identical, so a retrofit is a
   rename, but 49 renames in one pass is not worth the risk of a silent
   shadow).

     const rnd = pbRng(pbSeed("network-liabilities"));
     rnd()            // [0,1), deterministic for the seed

   Determinism is the point: every reload must render the same numbers, or a
   screenshot diff and a click audit both become noise. */
const pbRng = (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;
  };
};

/* FNV-1a — turn a page name into a stable seed so two screens do not
   accidentally share a stream by both passing 42. */
const pbSeed = (str) => {
  let h = 0x811c9dc5;
  for (let i = 0; i < String(str).length; i++) {
    h ^= String(str).charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return h >>> 0;
};

window.pbRng = pbRng;
window.pbSeed = pbSeed;

/* ------- White Label role hierarchy (pyramidal, top → bottom) -------
   Admin + Skin Access are internal; Master..Player are the network
   distribution chain. Payment-method access + entity suspension both
   key off this ladder. */
const ROLES = [
  { id: "ADMIN",       label: "Admin",       tier: 0, internal: true,  forced_on: true,  color: "#0f1729", desc: "Platform admin — always-on, cannot be toggled off" },
  { id: "SKIN_ACCESS", label: "Skin Access", tier: 1, internal: true,  forced_on: false, color: "#1e293b", desc: "White-label skin operator" },
  { id: "MASTER",      label: "Master",      tier: 2, internal: false, forced_on: false, color: "#7c3aed", desc: "Top of the network tree" },
  { id: "PROMOTER",    label: "Promoter",    tier: 3, internal: false, forced_on: false, color: "#db2777", desc: "Regional / affiliate layer" },
  { id: "AGENT",       label: "Agent",       tier: 4, internal: false, forced_on: false, color: "#ea580c", desc: "Agency manager" },
  { id: "SHOP",        label: "Shop",        tier: 5, internal: false, forced_on: false, color: "#d97706", desc: "Retail shop / betting venue" },
  { id: "CASHIER",     label: "Cashier",     tier: 6, internal: false, forced_on: false, color: "#0891b2", desc: "Front-desk cashier operating inside a shop" },
  { id: "PLAYER",      label: "Player",      tier: 7, internal: false, forced_on: false, color: "#16a34a", desc: "End-user player account" },
];

// Mirrors the skins configured in CMS → Skins (src/pages/HostSkins.jsx's
// SK_SKINS) so the Payments brand switcher only ever offers real, currently
// configured skins — each in its own configured currency. HostSkins reads
// this same array back (see HostSkins.jsx) so both pages share one source
// of truth; add/rename/remove a brand here (or eventually via a real
// Settings brand editor) and both places update together.
const BRANDS = [
  { id: "jokerenvivo",  name: "Jokerenvivo",   short: "JV", color: "linear-gradient(135deg,#e6a82c,#c48a14)", currency: "ARS", market: "Argentina" },
  { id: "donjoker",     name: "Donjoker",      short: "DJ", color: "linear-gradient(135deg,#1f9d57,#0c5a30)", currency: "ARS", market: "Argentina" },
  { id: "juegojoker",   name: "Juegojoker",    short: "JJ", color: "linear-gradient(135deg,#3b82f6,#1d4ed8)", currency: "ARS", market: "Argentina" },
  { id: "win24hs",      name: "win24hs",       short: "W2", color: "linear-gradient(135deg,#db2777,#9d174d)", currency: "ARS", market: "Argentina" },
  { id: "tucasino",     name: "Tucasino",      short: "TC", color: "linear-gradient(135deg,#f97316,#c2410c)", currency: "ARS", market: "Argentina" },
  { id: "goldensky",    name: "GoldenSky",     short: "GS", color: "linear-gradient(135deg,#7c3aed,#4c1d95)", currency: "LBP", market: "Lebanon" },
  { id: "gb24",         name: "GB24",          short: "GB", color: "linear-gradient(135deg,#f2c257,#9a6b0c)", currency: "EUR", market: "EU" },
  { id: "playspin",     name: "PlaySpin",      short: "PS", color: "linear-gradient(135deg,#0891b2,#0e7490)", currency: "BOB", market: "Bolivia" },
  { id: "apostando365", name: "apostando365",  short: "AP", color: "linear-gradient(135deg,#ef4444,#b91c1c)", currency: "PYG", market: "Paraguay" },
];

const METHODS = [
  { id: "visa",       name: "Visa",          short: "VISA", icon: "credit_card", kind: "Card",        color: "#1a1f71" },
  { id: "mastercard", name: "Mastercard",    short: "MC",   icon: "credit_card", kind: "Card",        color: "#eb001b" },
  { id: "bankwire",   name: "Bank Wire",     short: "WIRE", icon: "globe",       kind: "Bank",        color: "#475569" },
  { id: "bitcoin",    name: "Bitcoin",       short: "BTC",  icon: "zap",         kind: "Crypto",      color: "#f7931a" },
  { id: "ethereum",   name: "Ethereum",      short: "ETH",  icon: "zap",         kind: "Crypto",      color: "#627eea" },
  { id: "skrill",     name: "Skrill",        short: "SKR",  icon: "wallet",      kind: "E-wallet",    color: "#862165" },
  { id: "neteller",   name: "Neteller",      short: "NET",  icon: "wallet",      kind: "E-wallet",    color: "#00ac41" },
  { id: "paysafe",    name: "Paysafecard",   short: "PSC",  icon: "credit_card", kind: "Voucher",     color: "#1d3557" },
];

const LEVELS = [
  { id: "l0", name: "Newbie",  color: "#7c8593", auto_approve_under: 100, priority: 5, players: 18240 },
  { id: "l1", name: "Bronze",  color: "#b47e3c", auto_approve_under: 250, priority: 4, players: 9820 },
  { id: "l2", name: "Silver",  color: "#9aa3ad", auto_approve_under: 500, priority: 3, players: 4310 },
  { id: "l3", name: "Gold",    color: "#e6a82c", auto_approve_under: 1500, priority: 2, players: 1820 },
  { id: "l4", name: "VIP",     color: "#7a4ad6", auto_approve_under: 5000, priority: 1, players: 412 },
];

const STATUSES = {
  created:    { label: "Created",    chip: "chip--neutral", dot: "var(--n-400)",   desc: "Initiated on our side" },
  pending:    { label: "Pending",    chip: "chip--warn",    dot: "var(--warn-500)", desc: "Awaiting provider" },
  declined:   { label: "Declined",   chip: "chip--err",     dot: "var(--err-500)",  desc: "Declined on our side (limits)" },
  failed:     { label: "Failed",     chip: "chip--err",     dot: "var(--err-500)",  desc: "Rejected by provider" },
  rejected:   { label: "Rejected",   chip: "chip--err",     dot: "var(--err-500)",  desc: "Rejected by operator" },
  to_confirm: { label: "To confirm", chip: "chip--info",    dot: "var(--info-500)", desc: "Needs manual confirm" },
  approved:   { label: "Approved",   chip: "chip--ok",      dot: "var(--ok-500)",   desc: "Approved by operator" },
  balanced:   { label: "Balanced",   chip: "chip--ok",      dot: "var(--ok-600)",   desc: "Auto-completed within limits" },
  // Legacy aliases — keep so older rows still render
  completed:  { label: "Balanced",   chip: "chip--ok",      dot: "var(--ok-500)",   desc: "Auto-completed within limits" },
  review:     { label: "To confirm", chip: "chip--info",    dot: "var(--info-500)", desc: "Needs manual confirm" },
  cancelled:  { label: "Declined",   chip: "chip--neutral", dot: "var(--n-400)",    desc: "Cancelled" },
};
const STATUS_KEYS = ["created","pending","declined","failed","rejected","to_confirm","approved","balanced"];

/* Deterministic pseudo-random for stable mocks */
function seed(s){ let x = 0; for (let i=0;i<s.length;i++) x = (x*31 + s.charCodeAt(i)) | 0; return () => { x = (x * 1664525 + 1013904223) | 0; return ((x>>>0) % 10000) / 10000; }; }
const r = seed("psp-backoffice");
const pick = (arr, rnd=r) => arr[Math.floor(rnd()*arr.length)];

function genTx(n){
  const out = [];
  const countries = ["DE","IT","ES","FR","UK","NL","PT","SE","PL","IE","CH","AT","BE","NO"];
  const names = ["Anna K.","Marco R.","Jens B.","Sofia L.","Paul M.","Lena S.","Tomas W.","Julia F.","Dimitri V.","Marta P.","Ivan T.","Elena D.","Oscar V.","Nils H.","Clara N.","Bruno A.","Helena Z.","Felix G.","Nora Q.","Luca C."];
  const now = Date.now();
  for (let i=0;i<n;i++){
    // Store the brand SLOT rather than a hard id so that when an operator
    // renames or adds brands in Settings the existing transactions still
    // resolve to a real brand. resolveTransactionBrands() rewrites the
    // brand_* fields from this slot whenever window.MOCK.BRANDS changes.
    const brand_slot = i % BRANDS.length;
    const brand = BRANDS[brand_slot];
    const method = pick(METHODS);
    const type = r() > 0.55 ? "Deposit" : "Withdrawal";
    const amt = Math.round((20 + r()*5800) * 100)/100;
    const feeRate = type === "Deposit" ? 0.012 : 0.018;
    const fee = Math.round(amt * feeRate * 100)/100;
    const statusRoll = r();
    let status;
    if (type === "Deposit") {
      // Deposits never sit in To Confirm — they're either approved/balanced
      // by the auto-approval engine or terminate in a failure status. Manual
      // confirm is a withdrawal-only flow.
      status = statusRoll < 0.62 ? "balanced"
             : statusRoll < 0.75 ? "approved"
             : statusRoll < 0.85 ? "pending"
             : statusRoll < 0.92 ? "created"
             : statusRoll < 0.96 ? "failed"
             : statusRoll < 0.98 ? "declined"
             : "rejected";
    } else {
      // Withdrawals get more to_confirm / approved / rejected
      status = statusRoll < 0.40 ? "balanced"
             : statusRoll < 0.58 ? "approved"
             : statusRoll < 0.72 ? "to_confirm"
             : statusRoll < 0.82 ? "pending"
             : statusRoll < 0.88 ? "created"
             : statusRoll < 0.93 ? "declined"
             : statusRoll < 0.97 ? "rejected"
             : "failed";
    }
    // Spread created_at across ranges: ~8% last 15m, ~12% last 30m, ~18% last 1h,
    // ~30% last 12h, ~50% last 24h, rest up to 14d — so the timeframe widget has data.
    const bucketRoll = r();
    let ts;
    if (bucketRoll < 0.08) ts = now - Math.floor(r() * 15 * 60_000);
    else if (bucketRoll < 0.16) ts = now - Math.floor(15*60_000 + r() * 15 * 60_000);
    else if (bucketRoll < 0.26) ts = now - Math.floor(30*60_000 + r() * 30 * 60_000);
    else if (bucketRoll < 0.45) ts = now - Math.floor(1*3600_000 + r() * 11 * 3600_000);
    else if (bucketRoll < 0.65) ts = now - Math.floor(12*3600_000 + r() * 12 * 3600_000);
    else if (bucketRoll < 0.85) ts = now - Math.floor(24*3600_000 + r() * 6 * 86400_000);
    else ts = now - Math.floor(7*86400_000 + r() * 7 * 86400_000);
    const env = r() > 0.92 ? "staging" : "prod";
    const nameIdx = Math.floor(r()*names.length);
    const uid = 100000 + Math.floor(r()*899999);
    // Promo code — ~22% of deposits carry one; withdrawals never do
    // (bonus codes are a deposit-side construct). Real engine ships
    // with this captured at create-time in the player frontend.
    const PROMO_POOL = ["WELCOME100","VIP50","RELOAD25","SUMMER30","CASHBACK10","FREESPIN20","REFERRAL15","WEEKEND50"];
    const promo_code = (type === "Deposit" && r() < 0.22)
      ? PROMO_POOL[Math.floor(r() * PROMO_POOL.length)]
      : null;
    out.push({
      id: `TX${(90000000 + i * 113 + Math.floor(r()*97)).toString()}`,
      brand_slot,
      brand: brand.id, brand_name: brand.name, brand_short: brand.short, brand_color: brand.color,
      internal_id: `INT-${Math.floor(r()*99999999).toString().padStart(8,'0')}`,
      method: method.id, method_name: method.name, method_kind: method.kind, method_icon: method.icon, method_color: method.color,
      user_id: `U${uid}`,
      user_name: names[nameIdx],
      phone: `+${39 + Math.floor(r()*9)} ${Math.floor(r()*899+100)} ${Math.floor(r()*899999+100000)}`,
      transaction_user_id: `TU-${uid}-${Math.floor(r()*9999)}`,
      transaction_id: `${method.short}-${Math.floor(r()*9999999999).toString().padStart(10,'0')}`,
      transaction_origin_id: `ORG-${Math.floor(r()*99999999).toString().padStart(8,'0')}`,
      amount: amt, fee,
      currency: brand.currency,
      type, status,
      environment: env,
      country: pick(countries),
      level: pick(LEVELS).name,
      promo_code,
      created_at: ts,
      updated_at: ts + Math.floor(r()*600000),
    });
  }
  return out.sort((a,b)=> b.created_at - a.created_at);
}

const TRANSACTIONS = genTx(500);

/* ------- Dashboard series (last 14 days) ------- */
function series(seed0, base, vol){
  const rr = seed(seed0);
  const out = [];
  let v = base;
  for (let i=0;i<14;i++){
    v = Math.max(base*0.4, v + (rr()-0.5)*vol*2);
    out.push(Math.round(v));
  }
  return out;
}

const DASH = {
  deposits_14d: series("dep", 82000, 18000),
  withdrawals_14d: series("wd", 41000, 12000),
  approvals_14d: series("apv", 220, 50),
  declines_14d: series("dec", 18, 8),
  methods_mix: [
    { id: "visa",       value: 28 },
    { id: "mastercard", value: 18 },
    { id: "bankwire",   value: 10 },
    { id: "skrill",     value: 14 },
    { id: "neteller",   value: 10 },
    { id: "bitcoin",    value: 8  },
    { id: "ethereum",   value: 6  },
    { id: "paysafe",    value: 6  },
  ],
};

/* ------- Player 360 ------- */
const PLAYER = {
  user_id: "U382941",
  name: "Marco Ricci",
  email: "marco.ricci@example.com",
  phone: "+39 348 5512004",
  country: "IT",
  brand: BRANDS[0].id,
  currency: BRANDS[0].currency,
  level: "Gold",
  kyc: "Verified",
  joined_at: Date.now() - 1000*60*60*24*214,
  last_active: Date.now() - 1000*60*23,
  lifetime_deposits: 18420.55,
  lifetime_withdrawals: 9210.30,
  net_deposits: 9210.25,
  avg_deposit: 185.40,
  deposits_count: 99,
  withdrawals_count: 32,
  open_balance: 412.60,
  risk_score: 22,
  auto_approve: true,
  notes: "Consistent player, low-risk. Verified ID and address. Moved to Gold 2026-01-14.",
  limits: {
    deposit: { daily: 1500, weekly: 5000, monthly: 15000, used_daily: 620, used_weekly: 3050, used_monthly: 8400 },
    withdraw:{ daily: 2500, weekly: 8000, monthly: 20000, used_daily: 0, used_weekly: 1200, used_monthly: 4100 },
  },
};

const PLAYER_TX = TRANSACTIONS.slice(0, 22).map((t,i)=>({ ...t, user_id: "U382941", user_name: "Marco Ricci", brand: BRANDS[0].id, brand_short: BRANDS[0].short, currency: BRANDS[0].currency }));

/* ------- Operator activity log ------- */
const OP_USERS = [
  { id: "op1", name: "Jules Moreau",   role: "Platform admin",  avatar: "JM", color: "#3b55f0" },
  { id: "op2", name: "Sara Bianchi",   role: "Ops manager",     avatar: "SB", color: "#e11d74" },
  { id: "op3", name: "David Novak",    role: "Payments analyst",avatar: "DN", color: "#1f9d57" },
  { id: "op4", name: "Ines Duarte",    role: "Risk officer",    avatar: "ID", color: "#7c3aed" },
  { id: "op5", name: "Max Weiss",      role: "VIP manager",     avatar: "MW", color: "#c48a14" },
  { id: "op6", name: "Laila Ahmed",    role: "Compliance lead", avatar: "LA", color: "#0d9488" },
];

function genActivity(n){
  const out = [];
  const now = Date.now();
  const actions = [
    { verb:"approved",                kind:"approve",     icon:"check",            color:"var(--ok-600)"  },
    { verb:"rejected",                kind:"reject",      icon:"x",                color:"var(--err-600)" },
    { verb:"confirmed",               kind:"approve",     icon:"check",            color:"var(--ok-600)"  },
    { verb:"added note on player",    kind:"note",        icon:"message",          color:"var(--text-secondary)" },
    { verb:"logged into PSP for",     kind:"psp_login",   icon:"lock",             color:"var(--p-600)" },
    { verb:"edited payment method on",kind:"method_edit", icon:"edit",             color:"var(--info-500)" },
  ];
  for (let i=0; i<n; i++){
    const user = OP_USERS[Math.floor(r()*OP_USERS.length)];
    const act = actions[Math.floor(r()*actions.length)];
    const tx = TRANSACTIONS[Math.floor(r() * TRANSACTIONS.length)];
    // More recent activity weighted toward last 24h
    const bucket = r();
    let ts;
    if (bucket < 0.15) ts = now - Math.floor(r()*30*60_000);
    else if (bucket < 0.40) ts = now - Math.floor(30*60_000 + r()*90*60_000);
    else if (bucket < 0.75) ts = now - Math.floor(2*3600_000 + r()*22*3600_000);
    else ts = now - Math.floor(24*3600_000 + r()*6*86400_000);
    const amt = tx.amount.toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2});
    // Mirrors ui.jsx's CURRENCY_SYMBOLS — duplicated here (rather than
    // reading window.currencySymbol) because data.jsx runs before ui.jsx
    // loads, so that helper isn't defined yet at generation time.
    const CUR_SYM = { EUR:"€", USD:"$", GBP:"£", BOB:"Bs", PYG:"₲" };
    const cur = CUR_SYM[tx.currency] || tx.currency+" ";
    out.push({
      id: `A${i}`,
      ts,
      user,
      action: act,
      tx_id: tx.id,
      tx_type: tx.type,
      tx_amount: `${cur}${amt}`,
      tx_brand: tx.brand,
      tx_brand_name: tx.brand_name,
      tx_brand_short: tx.brand_short,
      tx_brand_color: tx.brand_color,
      player: tx.user_name,
      player_id: tx.user_id,
      reason: act.kind === "reject" ? ["Limit exceeded in Iwakiri side","Limit exceeded in Provider side","Network error in Iwakiri side","Network error in Provider side","Provider timeout","Transaction rejected by operator"][Math.floor(r()*6)]
            : act.kind === "note" ? ["Confirmed identity over phone","Player flagged for review","KYC docs requested","VIP escalation contact","Discussed delayed withdrawal","Player reported card issue"][Math.floor(r()*6)]
            : act.kind === "psp_login" ? ["Stripe","Adyen","Trustly","Skrill PSP","Checkout"][Math.floor(r()*5)]
            : act.kind === "method_edit" ? ["raised auto-approve threshold","disabled withdrawal","added BRL currency","updated cascade triggers","lowered min amount"][Math.floor(r()*5)]
            : null,
    });
  }
  return out.sort((a,b)=> b.ts - a.ts);
}
const ACTIVITY = genActivity(260);

const OPS_QUEUE = [
  { id: "Q1", kind: "review", title: "Withdrawal flagged — amount near limit", sub: "U382941 · Marco Ricci · €2,480", age: "3m" },
  { id: "Q2", kind: "review", title: "Manual approval required", sub: "U118290 · €1,200 · Skrill", age: "11m" },
  { id: "Q3", kind: "alert", title: "PSP Trustly — 3 failed in a row", sub: "Auto-paused route at 14:02", age: "22m" },
  { id: "Q4", kind: "review", title: "Daily limit override requested", sub: "VIP manager · U944012", age: "1h" },
  { id: "Q5", kind: "info", title: "Reconciliation ready", sub: "14 Apr · 612 tx · €312K", age: "2h" },
];

/* ------- Reports extras: synthesized aggregates that can't be derived
   from TRANSACTIONS alone (chargebacks, PSP performance, cohort retention,
   decline-reason mix over time). All seeded deterministically. ------- */

const rr = seed("reports-extras");

// Chargebacks + refunds — small synthesized stream.
const CHARGEBACKS = (() => {
  const out = [];
  const reasons = ["Fraud","Not recognized","Service not provided","Duplicate","Player dispute","KYC chargeback"];
  const now = Date.now();
  for (let i = 0; i < 48; i++) {
    const brand = BRANDS[Math.floor(rr()*BRANDS.length)];
    const method = METHODS[Math.floor(rr()*3)]; // cards dominate chargebacks
    const kind = rr() < 0.7 ? "chargeback" : "refund";
    const amt = Math.round((40 + rr()*1200) * 100) / 100;
    out.push({
      id: `CB${i}`,
      kind,
      brand: brand.id, brand_name: brand.name, brand_color: brand.color,
      method: method.id, method_name: method.name, method_color: method.color,
      amount: amt,
      currency: brand.currency,
      reason: reasons[Math.floor(rr()*reasons.length)],
      player_id: `U${100000 + Math.floor(rr()*899999)}`,
      created_at: now - Math.floor(rr() * 30 * 86400_000),
    });
  }
  return out.sort((a,b) => b.created_at - a.created_at);
})();

// PSP / provider performance — synthesized since engine doesn't route through PSPs.
const PSP_PERF = [
  { id: "stripe",  name: "Stripe",    methods: ["visa","mastercard"], approval: 94.2, p95_ms: 320, volume_30d: 412000, paused: false, decline_mix: { insufficient:32, fraud:18, card_limit:14, other:36 } },
  { id: "adyen",   name: "Adyen",     methods: ["visa","mastercard"], approval: 95.6, p95_ms: 280, volume_30d: 388000, paused: false, decline_mix: { insufficient:28, fraud:22, card_limit:18, other:32 } },
  { id: "trustly", name: "Trustly",   methods: ["bankwire"],          approval: 88.1, p95_ms: 1840, volume_30d: 92000, paused: true,  decline_mix: { insufficient:12, fraud:8,  card_limit:0,  other:80 } },
  { id: "skrill",  name: "Skrill",    methods: ["skrill","neteller"], approval: 96.4, p95_ms: 210, volume_30d: 142000, paused: false, decline_mix: { insufficient:8,  fraud:12, card_limit:0,  other:80 } },
  { id: "coinify", name: "Coinify",   methods: ["bitcoin","ethereum"],approval: 91.0, p95_ms: 2600, volume_30d: 78000, paused: false, decline_mix: { insufficient:0,  fraud:4,  card_limit:0,  other:96 } },
  { id: "paysafe", name: "Paysafe",   methods: ["paysafe"],           approval: 97.2, p95_ms: 420, volume_30d: 38000,  paused: false, decline_mix: { insufficient:2,  fraud:2,  card_limit:0,  other:96 } },
];

// Cohort retention — rows = signup cohort (weeks ago), cols = weeks since signup.
// Values are % of cohort that deposited in that week.
const COHORT_RETENTION = (() => {
  const weeks = 8;
  const rows = [];
  for (let c = 0; c < weeks; c++) {
    const row = { cohort: `W-${weeks - c}`, size: Math.round(180 + rr()*320), values: [] };
    for (let w = 0; w <= c; w++) {
      // Start at 100% week 0, decay to ~35-55% by week 8 with some noise.
      const base = w === 0 ? 100 : Math.max(28, 100 - w*8 - Math.floor(rr()*10));
      row.values.push(base);
    }
    rows.push(row);
  }
  return rows;
})();

// Decline-reason mix — canonical set + per-method AND per-provider
// breakdown so the dashboard widget can drill in to see which rail and
// which PSP contributed each failure. Counts are seeded to add up to a
// realistic total across the TX window.
const DECLINE_REASONS = [
  { code: "LIMIT_IWAKIRI",     label: "Limit exceeded in Iwakiri side",  count: 142, pct: 28,
    methods:   { visa: 56, mastercard: 42, bankwire: 18, skrill: 12, neteller: 8, bitcoin: 4, ethereum: 1, paysafe: 1 },
    providers: { stripe: 30, adyen: 26, checkout: 18, worldpay: 6, trustly: 18, skrillpsp: 20, coinify: 5, paysafe: 1, nuvei: 8, braintree: 10 } },
  { code: "LIMIT_PROVIDER",    label: "Limit exceeded in Provider side", count: 96,  pct: 19,
    methods:   { visa: 40, mastercard: 32, bankwire:  6, skrill: 5, neteller: 3, bitcoin: 4, ethereum: 4, paysafe: 2 },
    providers: { stripe: 32, adyen: 26, checkout: 12, worldpay: 4, trustly:  6, skrillpsp:  8, coinify: 6, paysafe: 2, nuvei: 0, braintree: 0 } },
  { code: "NETWORK_PROVIDER",  label: "Network error in Provider side",  count: 78,  pct: 16,
    methods:   { visa: 14, mastercard: 12, bankwire: 28, skrill: 4,  neteller: 4, bitcoin: 8, ethereum: 6, paysafe: 2 },
    providers: { stripe:  4, adyen:  6, checkout:  8, worldpay: 0, trustly: 28, skrillpsp:  8, coinify:14, paysafe: 2, nuvei: 4, braintree: 4 } },
  { code: "TIMEOUT_PROVIDER",  label: "Provider timeout",                count: 64,  pct: 13,
    methods:   { visa:  6, mastercard:  4, bankwire: 22, skrill: 2,  neteller: 4, bitcoin:14, ethereum:10, paysafe: 2 },
    providers: { stripe:  2, adyen:  2, checkout:  4, worldpay: 0, trustly: 22, skrillpsp:  6, coinify:24, paysafe: 2, nuvei: 0, braintree: 2 } },
  { code: "REJECTED_OPERATOR", label: "Transaction rejected by operator",count: 81,  pct: 16,
    methods:   { visa: 24, mastercard: 20, bankwire: 12, skrill: 8,  neteller: 6, bitcoin: 6, ethereum: 3, paysafe: 2 },
    providers: { stripe: 18, adyen: 14, checkout: 10, worldpay: 2, trustly: 12, skrillpsp: 14, coinify: 9, paysafe: 2, nuvei: 0, braintree: 0 } },
  { code: "NETWORK_IWAKIRI",   label: "Network error in Iwakiri side",   count: 38,  pct: 8,
    methods:   { visa:  9, mastercard:  6, bankwire: 10, skrill: 3,  neteller: 3, bitcoin: 3, ethereum: 2, paysafe: 2 },
    providers: { stripe:  6, adyen:  4, checkout:  4, worldpay: 0, trustly: 10, skrillpsp:  6, coinify: 5, paysafe: 2, nuvei: 1, braintree: 0 } },
];

// Brand × day volumes (14d) for Brands tab stacked chart.
const BRAND_SERIES_14D = BRANDS.map(b => ({
  id: b.id, name: b.name, color: (b.color.match(/#[0-9a-f]{6}/i) || ["#3b55f0"])[0],
  deposits: series(`bd-${b.id}`, 120000 + Math.floor(rr()*80000), 28000),
  withdrawals: series(`bw-${b.id}`, 55000 + Math.floor(rr()*30000), 14000),
  fees: series(`bf-${b.id}`, 2400 + Math.floor(rr()*1200), 600),
}));

// Anomaly feed — auto-generated alerts the Reports page surfaces.
const ANOMALIES = [
  { id: "AN1", severity: "high",   metric: "Approval rate", brand: BRANDS[0].id, change: "-3.2 pp", window: "last 6h",  msg: `${BRANDS[0].name} approval rate dropped from 94.1% → 90.9% — check Stripe routing.` },
  { id: "AN2", severity: "medium", metric: "Volume",         brand: BRANDS[1].id, change: "+42%",    window: "last 1h",  msg: `${BRANDS[1].name} deposits spiked 42% above 14d average — campaign impact?` },
  { id: "AN3", severity: "high",   metric: "Decline code",   brand: null,          change: "new",     window: "last 30m", msg: "New decline code NETWORK_PROVIDER appearing 14× on Bank Wire — Trustly auto-paused." },
  { id: "AN4", severity: "low",    metric: "Chargebacks",    brand: BRANDS[2].id, change: "+3",      window: "last 24h", msg: `${BRANDS[2].name} chargebacks ticked up — 3 new disputes on Visa, manual review suggested.` },
  { id: "AN5", severity: "medium", metric: "Time-to-decide", brand: null,          change: "+48s",    window: "last 4h",  msg: "Avg manual-review time grew from 2m 10s → 2m 58s — queue backlog building." },
];

/* ------- Default method-access policy per role -------
   A method is enabled for a role unless blocked here. ADMIN + SKIN_ACCESS
   are always enabled (forced_on in ROLES). Sparse map keyed by method id,
   value = array of role ids the method is BLOCKED for. */
const METHOD_ROLE_BLOCKS = {
  // Crypto blocked for Shops + Cashiers by default (they handle cash-in-hand).
  bitcoin:  ["SHOP", "CASHIER"],
  ethereum: ["SHOP", "CASHIER"],
  // Bank wire gated off for Cashiers — too slow for retail settlement.
  bankwire: ["CASHIER"],
};

window.MOCK = {
  ROLES, METHOD_ROLE_BLOCKS,
  BRANDS, METHODS, LEVELS, STATUSES, STATUS_KEYS, TRANSACTIONS, DASH,
  PLAYER, PLAYER_TX, OPS_QUEUE, ACTIVITY, OP_USERS,
  CHARGEBACKS, PSP_PERF, COHORT_RETENTION, DECLINE_REASONS, BRAND_SERIES_14D, ANOMALIES,
};

// Rebind transactions to the current brand list. Each tx carries a
// brand_slot from generation time; that slot is mapped modulo the current
// brand count so renames / adds / deletes never leave a tx orphaned.
window.resolveTransactionBrands = function () {
  const brs = window.MOCK.BRANDS || [];
  if (!brs.length) return;
  for (const t of window.MOCK.TRANSACTIONS || []) {
    const slot = (typeof t.brand_slot === "number" ? t.brand_slot : 0) % brs.length;
    const b = brs[slot];
    if (!b) continue;
    t.brand        = b.id;
    t.brand_name   = b.name;
    t.brand_short  = b.short;
    t.brand_color  = b.color;
    t.currency     = b.currency || t.currency;
  }
  // Also resync the activity log so its tx_brand_* fields stay aligned.
  const txById = Object.fromEntries((window.MOCK.TRANSACTIONS || []).map(t => [t.id, t]));
  for (const a of window.MOCK.ACTIVITY || []) {
    const t = txById[a.tx_id];
    if (!t) continue;
    a.tx_brand        = t.brand;
    a.tx_brand_name   = t.brand_name;
    a.tx_brand_short  = t.brand_short;
    a.tx_brand_color  = t.brand_color;
  }
};
window.resolveTransactionBrands();
