/* data.jsx — Mock data layer.
   Real tickers, fictional analyst handles, parsed events that read like real Discord posts.
   Deterministic via seeded RNG so the UI is stable across reloads.
*/

// ─── Deterministic RNG
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;
  };
}
const rng = mulberry32(20250508);
const pick = (arr) => arr[Math.floor(rng() * arr.length)];
const pickWeighted = (entries) => {
  const total = entries.reduce((s, [, w]) => s + w, 0);
  let r = rng() * total;
  for (const [v, w] of entries) { r -= w; if (r <= 0) return v; }
  return entries[0][0];
};
const between = (lo, hi) => lo + rng() * (hi - lo);
const randInt = (lo, hi) => Math.floor(between(lo, hi + 1));

// ─── Analyst handles (fictional)
const ANALYSTS = [
  { handle: "AURELIUS",  color: "var(--a-1)", channels: ["signals", "analysis"], events: 169, priority: "core" },
  { handle: "VANTAGE",   color: "var(--a-2)", channels: ["signals", "analysis"], events: 164, priority: "core" },
  { handle: "RHEA",      color: "var(--a-3)", channels: ["signals"],             events: 96,  priority: "core" },
  { handle: "BLACKBOX",  color: "var(--a-4)", channels: ["analysis"],            events: 80,  priority: "core" },
  { handle: "CLUTCH",    color: "var(--a-5)", channels: ["signals"],             events: 41,  priority: "secondary" },
  { handle: "TIDE",      color: "var(--a-6)", channels: ["analysis", "chat"],    events: 23,  priority: "secondary" },
  { handle: "COOP",      color: "var(--a-7)", channels: ["chat"],                events: 14,  priority: "watchlist" },
  { handle: "NORTHSTAR", color: "var(--a-1)", channels: ["chat"],                events: 8,   priority: "watchlist" },
  { handle: "FLINT",     color: "var(--a-3)", channels: ["chat"],                events: 5,   priority: "watchlist" },
];

// ─── Tickers (real)
const TICKERS = [
  { sym: "SPY",  name: "SPDR S&P 500 ETF",          base: 568, vol: 1.1, drift:  0.10 },
  { sym: "QQQ",  name: "Invesco QQQ Trust",         base: 482, vol: 1.4, drift:  0.18 },
  { sym: "NVDA", name: "NVIDIA Corporation",        base: 122, vol: 3.2, drift:  0.30 },
  { sym: "AAPL", name: "Apple Inc.",                base: 217, vol: 1.6, drift:  0.04 },
  { sym: "TSLA", name: "Tesla, Inc.",               base: 251, vol: 4.1, drift: -0.05 },
  { sym: "AMD",  name: "Advanced Micro Devices",    base: 156, vol: 3.4, drift:  0.10 },
  { sym: "MU",   name: "Micron Technology",         base:  92, vol: 2.6, drift:  0.20 },
  { sym: "SNAP", name: "Snap Inc.",                 base:  10.6, vol: 2.8, drift: -0.30 },
  { sym: "AVGO", name: "Broadcom Inc.",             base: 168, vol: 1.9, drift:  0.10 },
  { sym: "AOSL", name: "Alpha & Omega Semiconductor", base: 24, vol: 3.2, drift: -0.08 },
  { sym: "NBIS", name: "Nebius Group N.V.",         base:  37, vol: 4.2, drift:  0.35 },
  { sym: "CRWV", name: "CoreWeave, Inc.",           base:  68, vol: 5.0, drift:  0.45 },
  { sym: "PENG", name: "Penguin Solutions, Inc.",   base:  18, vol: 2.6, drift:  0.05 },
  { sym: "RDDT", name: "Reddit, Inc.",              base: 161, vol: 3.6, drift:  0.20 },
  { sym: "TTD",  name: "The Trade Desk",            base:  73, vol: 2.4, drift: -0.10 },
  { sym: "PLTR", name: "Palantir Technologies",     base:  41, vol: 3.0, drift:  0.18 },
  { sym: "IBM",  name: "International Business Machines", base: 244, vol: 1.1, drift: 0.05 },
];

// ─── Sample raw Discord messages (template snippets, lightly seeded)
const RAW_TEMPLATES = {
  open: [
    "Bought ${sym} ${cn} {price} fill — starter position",
    "Opening ${sym} ${cn} @ {price}, looks coiled on the 1hr",
    "Took ${sym} ${cn} @ {price} on the breakout, watching VWAP",
    "Adding ${sym} ${cn} {price} — earnings setup",
    "BOUGHT ${sym} ${cn} {price} FILL",
    "Sized up ${sym} ${cn} @ {price}, structure is clean",
  ],
  add: [
    "${sym} ${cn} adds at {price}, still constructive",
    "Added more ${sym} ${cn} @ {price}, conviction up",
    "Loading ${sym} ${cn} @ {price} — flag on the daily",
    "Topped up ${sym} ${cn} {price}, building runners",
  ],
  trim: [
    "Trimmed half ${sym} ${cn} @ {price}, holding runners",
    "Taking a small trim on ${sym} ${cn} — earnings tomorrow",
    "${sym} ${cn} 1/3 off at {price}, locking in",
    "Trimming ${sym} ${cn} ~25% at {price} — extended",
    "Off some ${sym} ${cn} @ {price} (½ off)",
  ],
  cut: [
    "Cutting ${sym} ${cn} at {price}, structure broke",
    "Stopped ${sym} ${cn} @ {price} — invalid",
    "Out of ${sym} ${cn} {price}, didn't follow through",
  ],
  close: [
    "Closed ${sym} ${cn} @ {price}, full exit",
    "Out of ${sym} ${cn} {price} — full cover",
    "Sold rest of ${sym} ${cn} {price}, calling it",
    "Done ${sym} ${cn} @ {price}",
  ],
  hold: [
    "Still in ${sym} ${cn}, holding runners through close",
    "Holding ${sym} ${cn}, no add no trim",
    "Sitting on ${sym} ${cn} — letting it work",
  ],
  watch: [
    "${sym} on watch above {price}, will alert on trigger",
    "Eyes on ${sym} — looking for a bid near {price}",
    "Watching ${sym} into earnings, no position",
  ],
  expire: [
    "${sym} ${cn} expiring worthless — that's a goose egg",
    "Let ${sym} ${cn} expire, theta won",
  ],
};

// ─── Construct contract string like "6/18 12C" or "6/18/26 45P"
function makeContract(sym, dir, basePrice) {
  // 1/3 chance shares only, 2/3 options
  if (rng() < 0.15) return null; // shares
  const month = pick(["5/15", "5/22", "6/6", "6/18", "6/18/26", "7/3", "7/17"]);
  const otmShift = (rng() - 0.4) * 0.15 * basePrice;
  const strike = Math.round(basePrice + otmShift);
  const right = dir === "long" ? "C" : "P";
  return `${month} ${strike}${right}`;
}

// ─── Generate events
function generateEvents(count = 420) {
  const events = [];
  const now = Date.UTC(2026, 4, 21, 16, 0, 0); // May 21 2026 4pm
  const SPAN_MS = 1000 * 60 * 60 * 24 * 30; // 30 days

  const actionWeights = [
    ["open", 18], ["add", 8], ["trim", 14], ["hold", 6],
    ["cut", 4], ["close", 10], ["watch", 6], ["expire", 2],
  ];

  for (let i = 0; i < count; i++) {
    const analyst = pickWeighted([
      ["AURELIUS", 28], ["VANTAGE", 26], ["RHEA", 14], ["BLACKBOX", 12],
      ["CLUTCH", 6], ["TIDE", 4], ["COOP", 3], ["NORTHSTAR", 2], ["FLINT", 2],
    ]);
    const aMeta = ANALYSTS.find(a => a.handle === analyst);

    // Bias ticker distribution
    const ticker = pickWeighted([
      ["SPY", 12], ["QQQ", 8], ["NVDA", 14], ["TSLA", 8], ["AMD", 8], ["MU", 8],
      ["SNAP", 6], ["AVGO", 6], ["AOSL", 6], ["NBIS", 8], ["CRWV", 6], ["PENG", 5],
      ["RDDT", 6], ["TTD", 4], ["PLTR", 5], ["IBM", 3], ["AAPL", 6],
    ]);
    const t = TICKERS.find(x => x.sym === ticker);

    const action = pickWeighted(actionWeights);
    const direction = rng() < 0.78 ? "long" : "short";
    const price = Number((t.base * between(0.01, 0.06)).toFixed(2)); // mock option premium
    const contract = (action === "watch") ? null : makeContract(ticker, direction, t.base);
    const ts = now - rng() * SPAN_MS;

    const channelCategory = pick(aMeta.channels);
    const channel = channelCategory === "signals" ? "#alerts-signals" :
                    channelCategory === "analysis" ? "#desk-analysis" : "#chat-lounge";

    // Confidence depends on action + channel
    let confidence = "high";
    if (channelCategory === "chat") confidence = pickWeighted([["high",1],["medium",4],["low",3]]);
    else if (action === "watch" || action === "hold") confidence = pickWeighted([["high",2],["medium",4],["low",1]]);
    else confidence = pickWeighted([["high", 5], ["medium", 3], ["low", 1]]);

    // Generate raw note
    const tmpl = pick(RAW_TEMPLATES[action] || RAW_TEMPLATES.hold);
    const symToken = rng() < 0.7 ? "$" + ticker : ticker;
    const cnToken = contract || "shares";
    const rawNote = tmpl
      .replace("${sym}", symToken)
      .replace("${cn}", cnToken)
      .replace("{price}", "$" + price.toFixed(2));

    // Parser reasoning (which words mapped where)
    const reasoning = makeReasoning(action, rawNote);

    // Structured instrument fields (parsed from the contract string)
    const instrument = contract ? "option" : "shares";
    let strike = null, expiry = null, optionType = null;
    if (contract) {
      const m = contract.match(/^([\d/]+)\s+(\d+(?:\.\d+)?)([CP])$/);
      if (m) { expiry = m[1]; strike = Number(m[2]); optionType = m[3] === "P" ? "put" : "call"; }
    }
    // Mock current mark for in-position events (the "-> n" value update)
    const currentPrice = (contract && ["open","add","hold","trim"].includes(action))
      ? Number((price * between(0.3, 2.6)).toFixed(2)) : null;
    // Mock a stated average on some adds ("new avg is …")
    const avgPrice = (action === "add" && contract && rng() < 0.5)
      ? Number((price * between(0.85, 1.25)).toFixed(2)) : null;

    events.push({
      id: "evt_" + i.toString(36).padStart(4, "0"),
      ts,
      analyst,
      ticker,
      action,
      direction,
      contract,
      instrument,
      strike,
      expiry,
      optionType,
      price,
      currentPrice,
      avgPrice,
      confidence,
      raw: rawNote,
      channel,
      channelCategory,
      reasoning,
      messageId: "msg_" + (i*7 + 1000031).toString(36),
    });
  }
  events.sort((a,b) => b.ts - a.ts);
  return events;
}

function makeReasoning(action, raw) {
  const triggers = {
    open: ["bought", "opening", "took", "starter", "fill", "entry"],
    add: ["adding", "added", "loading", "topped up", "conviction"],
    trim: ["trim", "trimmed", "1/3 off", "½", "locking", "half"],
    cut: ["cutting", "stopped", "out of", "invalid", "broke"],
    close: ["closed", "sold rest", "full cover", "done", "exit"],
    hold: ["holding", "still in", "sitting on", "runners"],
    watch: ["watch", "watching", "eyes on", "above", "trigger"],
    expire: ["expiring", "expire", "theta", "worthless"],
  };
  const matched = (triggers[action] || []).filter(t => raw.toLowerCase().includes(t.toLowerCase()));
  return {
    matched,
    summary: `Tagged as ${action.toUpperCase()} — matched ${matched.length} keyword${matched.length===1?"":"s"} (${matched.slice(0,3).map(m=>`"${m}"`).join(", ")}). Direction inferred from contract suffix and language polarity.`,
  };
}

// ─── Position aggregation
function aggregatePositions(events) {
  const all = [];               // every position cycle (open or historical)
  const current = new Map();    // baseKey → the active cycle for that contract
  const cycles = new Map();     // baseKey → how many cycles seen (for unique keys)
  const isClosed = (p) => p && (p.status === "closed" || p.status === "expired");
  // sort ascending for state machine
  const asc = [...events].sort((a,b) => a.ts - b.ts);
  for (const e of asc) {
    if (e.action === "watch") continue; // watchlist ideas are not positions
    let baseKey = `${e.analyst}|${e.ticker}|${e.contract || "shares"}`;

    // An exit/adjust that doesn't restate the contract (e.g. "Trimming $CENX @ 3")
    // attaches to the analyst's most recent STILL-OPEN cycle for this ticker.
    if (!e.contract && e.action !== "open" && e.action !== "watch") {
      let best = null;
      for (const p of current.values()) {
        if (p.analyst === e.analyst && p.ticker === e.ticker && !isClosed(p)) {
          if (!best || p.updatedTs > best.updatedTs) best = p;
        }
      }
      if (best) baseKey = best.baseKey;
    }

    let p = current.get(baseKey);
    // A new entry on a contract whose previous cycle is closed starts a FRESH
    // position — it must not inherit the closed cycle's mark/avg/events.
    if (isClosed(p) && (e.action === "open" || e.action === "add")) p = null;

    // Classify by position state, not by the analyst's wording: a buy on a
    // contract with NO live position of the same instrument is an OPEN; a buy
    // when one already exists is an ADD. (So "adding $QCOM 270C" with nothing
    // open yet shows as OPEN, and a true add shows as ADD.)
    const isBuy = e.action === "open" || e.action === "add";
    const effAction = isBuy ? (p && !isClosed(p) ? "add" : "open") : e.action;

    if (!p) {
      const n = (cycles.get(baseKey) || 0) + 1;
      cycles.set(baseKey, n);
      p = {
        key: `${baseKey}#${n}`, baseKey,
        analyst: e.analyst, ticker: e.ticker, contract: e.contract,
        instrument: e.instrument || (e.contract ? "option" : "shares"),
        strike: e.strike ?? null, expiry: e.expiry ?? null, optionType: e.optionType ?? null,
        direction: e.direction,
        entryPrice: null, lastPrice: e.price, markPrice: e.currentPrice ?? null,
        avgPrice: e.avgPrice ?? null,
        entryTs: null, updatedTs: e.ts,
        lastAction: effAction, lastConfidence: e.confidence, lastNote: e.raw,
        events: [],
        status: "open", // open | partial | closed | expired
      };
      current.set(baseKey, p);
      all.push(p);
    }
    // Latest mark (the "-> n" current value) and stated average (cost basis)
    if (e.currentPrice != null) p.markPrice = e.currentPrice;
    if (e.avgPrice != null) p.avgPrice = e.avgPrice;
    if (p.entryPrice == null && e.price != null && ["open", "add", "hold"].includes(e.action)) {
      p.entryPrice = e.price; p.entryTs = e.ts;
    }
    // Fill in option details if a later event carries them and the position lacks them
    if ((!p.instrument || p.instrument === "shares") && e.instrument === "option") p.instrument = "option";
    if (!p.contract && e.contract) p.contract = e.contract;
    if (p.strike == null && e.strike != null) p.strike = e.strike;
    if (!p.expiry && e.expiry) p.expiry = e.expiry;
    if (!p.optionType && e.optionType) p.optionType = e.optionType;

    p.events.push(isBuy ? { ...e, action: effAction } : e);
    p.updatedTs = e.ts;
    p.lastAction = effAction;
    p.lastConfidence = e.confidence;
    p.lastNote = e.raw;
    p.lastPrice = e.price;
    if (["open","add"].includes(e.action)) {
      if (!p.entryPrice) { p.entryPrice = e.price; p.entryTs = e.ts; }
      p.status = "open";
    }
    if (e.action === "trim") p.status = "partial";
    if (e.action === "close" || e.action === "cut") p.status = "closed";
    if (e.action === "expire") p.status = "expired";
  }
  return all;
}

// ─── OHLC candle generation
function generateCandles(sym, days = 60) {
  const t = TICKERS.find(x => x.sym === sym);
  if (!t) return [];
  const candles = [];
  let price = t.base * (1 - t.drift * 0.4); // start lower so trend ends near base
  const startMs = Date.UTC(2026, 4, 21) - days * 86400000;
  const symRng = mulberry32(sym.charCodeAt(0) * 7919 + sym.length * 131);
  for (let i = 0; i < days; i++) {
    const ts = startMs + i * 86400000;
    const driftStep = (t.drift / days) * t.base;
    const noise = (symRng() - 0.5) * t.vol * 1.6;
    const open = price;
    const close = price + driftStep + noise;
    const hi = Math.max(open, close) + symRng() * t.vol * 0.8;
    const lo = Math.min(open, close) - symRng() * t.vol * 0.8;
    candles.push({ t: ts, o: open, h: hi, l: lo, c: close });
    price = close;
  }
  return candles;
}

// ─── Channels (Discord-style)
const CHANNELS = [
  { id: "1198451224561", name: "signals-alerts",    category: "signals",  msgs: 412 },
  { id: "1198451224562", name: "signal-confirmed",  category: "signals",  msgs: 188 },
  { id: "1198451224563", name: "options-flow",      category: "signals",  msgs: 144 },
  { id: "1198451224564", name: "desk-analysis",     category: "analysis", msgs: 326 },
  { id: "1198451224565", name: "macro-thread",      category: "analysis", msgs: 102 },
  { id: "1198451224566", name: "premarket-prep",    category: "analysis", msgs: 220 },
  { id: "1198451224567", name: "chat-lounge",       category: "chat",     msgs: 1820 },
  { id: "1198451224568", name: "off-topic",         category: "chat",     msgs: 940 },
  { id: "1198451224569", name: "screener-bots",     category: "ignore",   msgs: 6210 },
];

// ─── Closed/historical recaps (computed from events)
function buildRecaps(events) {
  const positions = aggregatePositions(events);
  const closed = positions.filter(p => p.status === "closed" || p.status === "expired");
  return closed.map(p => {
    const opens = p.events.filter(e => e.action === "open" || e.action === "add");
    const exits = p.events.filter(e => e.action === "close" || e.action === "cut" || e.action === "trim");
    // Cost basis = the analyst's stated average if given, else the first entry.
    const basis = p.avgPrice != null ? p.avgPrice : (opens[0]?.price || p.entryPrice || 1);
    // Exit value = the last exit's fill ("@"), else its mark ("-> n"), else the
    // latest mark on the position.
    const lastExit = exits[exits.length - 1];
    const exit = (lastExit && (lastExit.price ?? lastExit.currentPrice)) ?? p.markPrice ?? p.lastPrice ?? basis;
    // Premium P&L: a bought option (call OR put) gains when its premium rises, so
    // don't flip by direction for options — only shares use long/short.
    const sign = p.instrument === "option" ? 1 : (p.direction === "long" ? 1 : -1);
    const pnlPct = basis ? (exit - basis) / basis * sign : 0;
    return {
      ...p,
      basis,
      exit,
      pnlPct,
      pnlMult: 1 + pnlPct,
      opens, exits,
    };
  }).sort((a,b) => b.updatedTs - a.updatedTs);
}

// Expose
const ALL_EVENTS = generateEvents(420);
const ALL_POSITIONS = aggregatePositions(ALL_EVENTS);
const ALL_RECAPS = buildRecaps(ALL_EVENTS);

Object.assign(window, {
  ANALYSTS, TICKERS, CHANNELS,
  ALL_EVENTS, ALL_POSITIONS, ALL_RECAPS,
  generateEvents, aggregatePositions, generateCandles, buildRecaps,
  pick, between, rng, mulberry32, randInt,
});
