// Mock data + live tick simulator for the Zwap solver monitor.
// All numbers are synthetic but shaped to match the real domain
// (FSM states, sat/zat units, txid format, BTC/ZEC heights).

const FSM_STATES = [
  "created",
  "keys_generated_alice",
  "keys_generated_bob",
  "alice_partially_initiated",
  "alice_initiated",
  "bob_partially_initiated",
  "bob_initiated",
  "alice_secret_shared",
  "bob_redeemed",
  "alice_redeemed",
  "lock_timelock_expired",
  "refund_submitted",
  "alice_refunded",
  "bob_refunded",
  "refund_timelock_expired",
  "bob_claimed_from_refund",
  "alice_claimed_after_refund",
];

const STATE_LABELS = {
  created: "Created",
  keys_generated_alice: "Keys A",
  keys_generated_bob: "Keys B",
  alice_partially_initiated: "A: Partial Init",
  alice_initiated: "A: BTC Locked",
  bob_partially_initiated: "B: Partial Init",
  bob_initiated: "B: ZEC Sent",
  alice_secret_shared: "A: Secret Shared",
  bob_redeemed: "B: Claimed BTC",
  alice_redeemed: "A: Claimed ZEC",
  lock_timelock_expired: "Lock CSV ⏰",
  refund_submitted: "Refund TX",
  alice_refunded: "A: Refunded",
  bob_refunded: "B: Salvaged",
  refund_timelock_expired: "Refund CSV ⏰",
  bob_claimed_from_refund: "B: Claimed CSV",
  alice_claimed_after_refund: "A: Claimed CSV",
};

const TERMINAL = new Set([
  "alice_redeemed",
  "bob_refunded",
  "alice_claimed_after_refund",
  "bob_claimed_from_refund",
]);

const HAPPY_PATH = [
  "created",
  "keys_generated_alice",
  "keys_generated_bob",
  "alice_partially_initiated",
  "alice_initiated",
  "bob_partially_initiated",
  "bob_initiated",
  "alice_secret_shared",
  "bob_redeemed",
  "alice_redeemed",
];

const REFUND_BRANCH = [
  "lock_timelock_expired",
  "refund_submitted",
  "alice_refunded",
  "bob_refunded",
];

const CLAIM_BRANCH = [
  "refund_timelock_expired",
  "bob_claimed_from_refund",
  "alice_claimed_after_refund",
];

// Deterministic-ish PRNG so reloads don't reshuffle history wildly.
function mulberry32(seed) {
  return function () {
    let t = (seed += 0x6d2b79f5);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function hex(n, rng) {
  const chars = "0123456789abcdef";
  let s = "";
  for (let i = 0; i < n; i++) s += chars[Math.floor(rng() * 16)];
  return s;
}

const NA = "N/A";

function isFiniteNumber(value) {
  return value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value));
}
function fmtMaybe(value, formatter = (v) => String(v)) {
  return isFiniteNumber(value) ? formatter(Number(value)) : NA;
}
function fmtInt(value) {
  return fmtMaybe(value, (v) => v.toLocaleString());
}
function fmtMs(value) {
  return fmtMaybe(value, (v) => `${Math.round(v)} ms`);
}
function fmtDurationMs(value) {
  if (!isFiniteNumber(value)) return NA;
  const ms = Math.max(0, Number(value));
  if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
  if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
  return `${Math.floor(ms / 3_600_000)}h ${Math.floor((ms % 3_600_000) / 60_000)}m`;
}

function fmtSats(sats) {
  if (!isFiniteNumber(sats)) return NA;
  const btc = Number(sats) / 1e8;
  if (btc >= 1) return btc.toFixed(4);
  if (btc >= 0.001) return btc.toFixed(5);
  return btc.toFixed(6);
}
function fmtZats(zats) {
  if (!isFiniteNumber(zats)) return NA;
  const zec = Number(zats) / 1e8;
  if (zec >= 1) return zec.toFixed(4);
  if (zec >= 0.001) return zec.toFixed(5);
  return zec.toFixed(6);
}
function fmtUsd(n) {
  if (!isFiniteNumber(n)) return NA;
  const num = Number(n);
  const sign = num < 0 ? "-" : "";
  const a = Math.abs(num);
  if (a >= 1000) return sign + "$" + a.toLocaleString(undefined, { maximumFractionDigits: 0 });
  return sign + "$" + a.toFixed(2);
}
function fmtRelTime(ts, now) {
  if (!isFiniteNumber(ts) || !isFiniteNumber(now) || Number(ts) <= 0) return NA;
  const dt = Math.max(0, (now - ts) / 1000);
  if (dt < 60) return Math.floor(dt) + "s";
  if (dt < 3600) return Math.floor(dt / 60) + "m " + Math.floor(dt % 60) + "s";
  if (dt < 86400) return Math.floor(dt / 3600) + "h " + Math.floor((dt % 3600) / 60) + "m";
  return Math.floor(dt / 86400) + "d";
}
function fmtCountdown(secs) {
  if (!isFiniteNumber(secs)) return NA;
  if (secs <= 0) return "00:00";
  const h = Math.floor(secs / 3600);
  const m = Math.floor((secs % 3600) / 60);
  const s = Math.floor(secs % 60);
  if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}

function shortHash(h, headLen = 6, tailLen = 4) {
  if (!h || h === NA) return NA;
  if (h.length <= headLen + tailLen + 1) return h;
  return h.slice(0, headLen) + "…" + h.slice(-tailLen);
}

// --- Build mock data ---------------------------------------------------------

const SOLVER_IDS = ["solver-alpha", "solver-beta", "solver-gamma"];
const COUNTERPARTIES = [
  "user_3f9a", "user_b0d2", "user_71e4", "user_a8c1", "user_2d77",
  "user_55ab", "user_e119", "user_4f06", "user_9c83", "user_1b2f",
];

function buildSwap(rng, idx, now) {
  const id = "swap_" + hex(8, rng);
  const direction = rng() > 0.45 ? "btc-to-zec" : "zec-to-btc";
  const matched_role = direction === "btc-to-zec" ? "bob" : "alice";
  const counterparty = COUNTERPARTIES[Math.floor(rng() * COUNTERPARTIES.length)];
  const solverId = SOLVER_IDS[Math.floor(rng() * SOLVER_IDS.length)];

  // pick a state weighted toward in-flight
  const r = rng();
  let state, branch;
  if (r < 0.55) {
    // happy in-flight
    branch = "happy";
    state = HAPPY_PATH[1 + Math.floor(rng() * (HAPPY_PATH.length - 2))];
  } else if (r < 0.75) {
    // recently terminal (happy)
    branch = "happy";
    state = "alice_redeemed";
  } else if (r < 0.88) {
    // refund branch
    branch = "refund";
    state = REFUND_BRANCH[Math.floor(rng() * REFUND_BRANCH.length)];
  } else if (r < 0.95) {
    // claim-after-refund
    branch = "claim";
    state = CLAIM_BRANCH[Math.floor(rng() * CLAIM_BRANCH.length)];
  } else {
    branch = "stuck";
    state = HAPPY_PATH[1 + Math.floor(rng() * 4)];
  }

  const stuck = branch === "stuck";

  // amounts
  const btcSats = Math.floor((0.005 + rng() * 0.5) * 1e8);
  const rate = 0.000045 + rng() * 0.00001; // BTC per ZEC ~ rough
  const zecZats = Math.floor(btcSats / rate);

  const startedAt = now - (rng() * 4 * 3600 + 60) * 1000 - (stuck ? 7200_000 : 0);
  const updatedAt = stuck
    ? startedAt + (15 + rng() * 30) * 60_000
    : now - rng() * 90_000;

  // timelocks
  const timelock0_blocks = 144; // ~24h
  const timelock1_blocks = 144;
  const lockHeight = 924_500 + Math.floor(rng() * 200);
  const csvDeadline =
    branch === "refund"
      ? now + (rng() * 600 - 100) * 1000
      : now + (rng() * 18 + 1) * 3600 * 1000;
  const csv2Deadline =
    branch === "claim"
      ? now + (rng() * 600 - 100) * 1000
      : null;

  // tx ids
  const lockTxid = HAPPY_PATH.indexOf(state) >= 4 || branch !== "happy" ? hex(64, rng) : null;
  const zecDepositTxid = HAPPY_PATH.indexOf(state) >= 6 ? hex(64, rng) : null;
  const buyTxid = HAPPY_PATH.indexOf(state) >= 8 ? hex(64, rng) : null;
  const orchardTxid = state === "alice_redeemed" ? hex(64, rng) : null;
  const refundTxid = REFUND_BRANCH.indexOf(state) >= 1 ? hex(64, rng) : null;

  // estimated P&L
  const spreadBps = 30 + Math.floor(rng() * 50);
  const usdValue = 100 + rng() * 4000;
  const pnlUsd = TERMINAL.has(state)
    ? usdValue * (spreadBps / 10000) * (1 - rng() * 0.3)
    : null;

  // build event log
  const events = [];
  let t = startedAt;
  events.push({ ts: t, kind: "match_accepted", note: "orderbook match observed" });
  const path =
    branch === "happy"
      ? HAPPY_PATH.slice(0, HAPPY_PATH.indexOf(state) + 1)
      : branch === "refund"
        ? [...HAPPY_PATH.slice(0, 5), ...REFUND_BRANCH.slice(0, REFUND_BRANCH.indexOf(state) + 1)]
        : branch === "claim"
          ? [...HAPPY_PATH.slice(0, 5), ...REFUND_BRANCH.slice(0, 3), ...CLAIM_BRANCH.slice(0, CLAIM_BRANCH.indexOf(state) + 1)]
          : HAPPY_PATH.slice(0, HAPPY_PATH.indexOf(state) + 1);
  for (let i = 1; i < path.length; i++) {
    t += (60 + rng() * 600) * 1000;
    events.push({
      ts: t,
      kind: "fsm_transition",
      from: path[i - 1],
      to: path[i],
      note: STATE_LABELS[path[i]],
    });
  }
  if (stuck) {
    t += 600_000;
    events.push({ ts: t, kind: "warning", note: "no transition for 18m — stalled?" });
  }

  return {
    id,
    direction,
    role: matched_role,
    counterparty,
    solverId,
    state,
    branch,
    stuck,
    btcSats,
    zecZats,
    rateZecPerBtc: 1 / rate,
    spreadBps,
    pnlUsd,
    usdValue,
    startedAt,
    updatedAt,
    lockHeight,
    timelock0_blocks,
    timelock1_blocks,
    csvDeadline,
    csv2Deadline,
    lockTxid,
    zecDepositTxid,
    buyTxid,
    orchardTxid,
    refundTxid,
    events,
    quoteId: "q_" + hex(10, rng),
    aliceAddress: "tb1q" + hex(38, rng),
    bobAddress: "u1" + hex(72, rng),
    swapHash: hex(64, rng),
  };
}

function buildAlerts(rng, swaps, now) {
  const items = [];
  swaps.forEach((s) => {
    if (s.stuck) {
      items.push({
        id: "alert_" + s.id,
        severity: "warn",
        ts: s.updatedAt,
        title: "Swap stalled",
        body: `${s.id} has not transitioned in 18m at ${STATE_LABELS[s.state]}.`,
        swapId: s.id,
      });
    }
    if (s.branch === "refund" && s.csvDeadline < now + 30 * 60 * 1000 && s.state !== "alice_refunded") {
      items.push({
        id: "alert_csv_" + s.id,
        severity: "warn",
        ts: now - rng() * 120_000,
        title: "Refund CSV near deadline",
        body: `${s.id} has < 30m to broadcast refund TX.`,
        swapId: s.id,
      });
    }
  });

  // stand-alone solver-level alerts
  items.push({
    id: "alert_kraken",
    severity: "info",
    ts: now - 240_000,
    title: "Quote source heartbeat slow",
    body: "Kraken BTC/USD feed observed 6.2s lag (threshold 5s).",
  });
  items.push({
    id: "alert_zec_scan",
    severity: "warn",
    ts: now - 540_000,
    title: "ZEC scan lag",
    body: "Orchard scanner is 12 blocks behind tip (zebrad height 2,612,401).",
  });
  items.push({
    id: "alert_btc_fee",
    severity: "info",
    ts: now - 1_840_000,
    title: "BTC fee estimate spike",
    body: "Confirmation target 6 → 64 sat/vB (was 22 sat/vB at 09:12).",
  });
  items.push({
    id: "alert_preflight",
    severity: "ok",
    ts: now - 3_600_000,
    title: "Preflight passed",
    body: "All 18 mainnet safety gates green at 14:02 UTC.",
  });

  return items.sort((a, b) => b.ts - a.ts);
}

function buildHealth(rng, now) {
  return {
    btc: {
      rpc: { status: "ok", latencyMs: 38 + Math.floor(rng() * 12), endpoint: "http://btc-core.internal:8332/wallet/zwap" },
      height: 928_412,
      mempoolFee: { sats_vb_econ: 22, sats_vb_fast: 64, sats_vb_min: 8 },
      walletBalance: 1.4823 * 1e8,
      utxoCount: 47,
      reservedSats: 0.42 * 1e8,
      pendingSpends: 2,
      lastBlockMs: 184_000,
    },
    zec: {
      rpc: { status: "ok", latencyMs: 56 + Math.floor(rng() * 18), endpoint: "zebrad-mainnet:8232" },
      lwd: { status: "ok", latencyMs: 91, endpoint: "lightwalletd:9067" },
      tipHeight: 2_612_401,
      scanHeight: 2_612_389,
      scanLag: 12,
      walletBalance: 142.31 * 1e8,
      noteCount: 318,
      pendingSpends: 1,
      reorgDepthSeen24h: 1,
    },
    quotes: {
      source: "kraken",
      btcUsd: 96_842.31,
      zecUsd: 41.18,
      spreadBps: 50,
      ttlSecs: 30,
      observedAgo: 4,
      stale: false,
    },
    preflight: {
      lastRun: now - 3_600_000,
      passed: 18,
      total: 18,
      mainnetEnabled: true,
      keystoreMode: "0o600",
      strictMode: true,
    },
    process: {
      uptime: 11 * 86400 + 3 * 3600 + 22 * 60,
      memoryMb: 4218,
      memoryLimitMb: 6144,
      cpu: 12,
      version: "zwap-solverd 0.7.3 (feat/indexer-fsm @ ee459c6)",
      host: "prod-onevm-04.eu-fra-1",
    },
    db: {
      orderbookSize: 24.6,
      btcMetaSize: 8.1,
      zecWalletSize: 142.7,
      lastBackup: now - 1800 * 1000,
    },
  };
}

function buildOrders(rng, now) {
  const orders = [];
  for (let i = 0; i < 18; i++) {
    const dir = rng() > 0.5 ? "btc-to-zec" : "zec-to-btc";
    const status = rng() > 0.7 ? "matched" : rng() > 0.4 ? "open" : rng() > 0.2 ? "filled" : "expired";
    orders.push({
      id: "ord_" + hex(8, rng),
      direction: dir,
      amountSats: Math.floor((0.01 + rng() * 0.4) * 1e8),
      status,
      createdAt: now - rng() * 6 * 3600_000,
      counterparty: COUNTERPARTIES[Math.floor(rng() * COUNTERPARTIES.length)],
      quoteId: "q_" + hex(10, rng),
    });
  }
  return orders.sort((a, b) => b.createdAt - a.createdAt);
}

function buildLogs(rng, swaps, now) {
  const logs = [];
  const levels = ["info", "info", "info", "info", "debug", "warn", "info", "info"];
  const sources = ["fsm", "btc_watcher", "orchard_scan", "ws", "orderbook_poll", "quote", "db"];
  const templates = [
    (s) => ["info", "fsm", `${s.id} → ${STATE_LABELS[s.state]} (${s.state})`],
    (s) => ["info", "btc_watcher", `tip=928412 confirmations=2 lock_txid=${shortHash(s.lockTxid || "0".repeat(64))}`],
    (s) => ["debug", "orchard_scan", `scanned block 2612389 notes=3 nullifiers_seen=0`],
    (s) => ["info", "ws", `solver→orderbook frame[Phase0Initiator] swap=${s.id}`],
    (s) => ["info", "quote", `quote refresh kraken btc=96842.31 zec=41.18 spread=50bps`],
    (s) => ["warn", "btc_watcher", `mempool fee spike — economical 22→64 sat/vB`],
    (s) => ["debug", "fsm", `(role=${s.role}, state=${s.state}, event=tick) → no-op`],
  ];

  let t = now - 90_000;
  for (let i = 0; i < 80; i++) {
    const s = swaps[Math.floor(rng() * swaps.length)];
    const tpl = templates[Math.floor(rng() * templates.length)];
    const [lvl, src, msg] = tpl(s);
    logs.push({
      ts: t,
      level: lvl,
      source: src,
      msg,
      swapId: rng() > 0.4 ? s.id : null,
    });
    t += rng() * 1500;
  }
  return logs.sort((a, b) => b.ts - a.ts);
}

function buildSparkline(rng, n = 24, min = 0.4, max = 1.0) {
  const arr = [];
  let v = min + rng() * (max - min);
  for (let i = 0; i < n; i++) {
    v += (rng() - 0.5) * 0.15;
    v = Math.max(min, Math.min(max, v));
    arr.push(v);
  }
  return arr;
}

function buildAll(seed) {
  const rng = mulberry32(seed);
  const now = Date.now();
  const swaps = [];
  for (let i = 0; i < 26; i++) swaps.push(buildSwap(rng, i, now));
  const alerts = buildAlerts(rng, swaps, now);
  const health = buildHealth(rng, now);
  const orders = buildOrders(rng, now);
  const logs = buildLogs(rng, swaps, now);

  // Aggregates
  const active = swaps.filter((s) => !TERMINAL.has(s.state));
  const completed24h = swaps.filter((s) => s.state === "alice_redeemed").length + 14;
  const refunded24h = swaps.filter((s) => s.branch !== "happy").length + 2;
  const pnl24h = swaps.reduce((acc, s) => acc + (s.pnlUsd || 0), 0) + 142.18;

  return {
    swaps,
    alerts,
    health,
    orders,
    logs,
    spark: {
      throughput: buildSparkline(rng, 32, 0.2, 0.95),
      btcBalance: buildSparkline(rng, 32, 0.6, 0.95),
      zecBalance: buildSparkline(rng, 32, 0.5, 0.9),
      pnl: buildSparkline(rng, 32, 0.3, 0.85),
    },
    aggregate: {
      active: active.length,
      completed24h,
      refunded24h,
      pnl24h,
      avgFillSec: 412,
      btcUsd: 96_842.31,
      zecUsd: 41.18,
    },
    now,
  };
}

window.ZW = {
  FSM_STATES,
  STATE_LABELS,
  TERMINAL,
  HAPPY_PATH,
  REFUND_BRANCH,
  CLAIM_BRANCH,
  fmtSats,
  fmtZats,
  fmtUsd,
  fmtRelTime,
  fmtCountdown,
  shortHash,
  fmtInt,
  fmtMs,
  fmtDurationMs,
  fmtMaybe,
  isFiniteNumber,
  NA,
  buildAll,
};
