/* 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 — etDayHour / instrumentKey / dedupeEvents /
   aggregatePositions / optionExpired and friends — now lives in
   src/positions-core.js, loaded as a plain <script> just before this file so the
   API can require() the very same definitions. They are globals here exactly as
   before; nothing downstream changed. */


// ─── OHLC candle generation
function generateCandles(sym, days = 60) {
  let t = TICKERS.find(x => x.sym === sym);
  if (!t) {
    // Unknown ticker → deterministic synthetic params from the symbol hash, so
    // the chart fallback always renders SOMETHING (labeled "Synthesized")
    // instead of a blank "no data" panel.
    let h = 0;
    for (const ch of String(sym)) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
    t = { sym, base: 20 + (h % 280), vol: 1 + (h % 30) / 10, drift: (((h >> 4) % 21) - 10) / 50 };
  }
  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.
    // basisDefaulted: the entry price was NEVER captured — a forwarded/image/portal
    // message, or a trade opened before tracking — so there is no real cost basis and
    // the `|| 1` below is only a placeholder. A price-derived P&L must NOT be booked
    // against it: dividing a $260 exit by 1 invents +25,900%. See the pnl guard below.
    const basisDefaulted = p.avgPrice == null && !(opens[0] && opens[0].price) && !p.entryPrice;
    const basis = p.avgPrice != null ? p.avgPrice : (opens[0]?.price || p.entryPrice || 1);

    // A REAL exit (close/cut) is authoritative — that fill IS the exit, even if a
    // later stray mark exists. A position that merely LAPSED at expiry never got
    // one, so its exit is the last price the analyst actually put in front of
    // members: a trim fill, or any price update ("-> n"), whichever came LAST.
    // Owner's rule — a published price update is a point a member could have taken
    // profit at, so it counts the same as a trim. Automated running-P&L cards count
    // too (they're posted to the channel); `cardConflict` below flags the case where
    // a manual update contradicts a card so it can be reviewed rather than trusted.
    // 1. A real close/cut that STATES a price — that fill is the exit, full stop.
    // 2. Otherwise (a price-less exit alert, or a lapse at expiry) the last price
    //    actually published on the position: a trim fill or any price update.
    // Note a close/cut with no price of its own must fall through to 2, not be
    // treated as "no evidence" — plenty of app exit alerts carry no fill.
    // An open/add price is the COST BASIS, never an exit — everything else that
    // states a number ("position update @ 8.5", "-> 2.10", a trim fill) is a price
    // the analyst published while holding, and so is a point a member could have
    // exited at.
    // On a FILL (close/cut/trim) the realized number is the fill price; on a HOLD
    // update it's the stated mark. Don't let a restated entry on a hold ("bought at
    // .08, it's .02 now") be read as the exit — the .02 is.
    const priced = (e) => (["close", "cut", "trim"].includes(e.action))
      ? (e.price ?? e.currentPrice ?? null)
      : (e.currentPrice ?? e.price ?? null);
    const isExitEvidence = (e) =>
      e.currentPrice != null || (e.price != null && e.action !== "open" && e.action !== "add");
    // Take the FINAL close/cut — never an earlier one. If that last exit states a
    // fill, it is the exit. If it doesn't (a bare "Closed SPY here", a portal-logged
    // close), fall through to the newest price published on the position. Reaching
    // back to an older *priced* close would ignore the trims that came after it and
    // book a worse number than the analyst actually got.
    const lastCloseCut = [...p.events].reverse().find(e => e.action === "close" || e.action === "cut");
    const lastEvidence = [...p.events].reverse().find(isExitEvidence);
    const exitFrom = (lastCloseCut && priced(lastCloseCut) != null)
      ? priced(lastCloseCut)
      : (lastEvidence ? priced(lastEvidence) : null);

    // 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);

    // Plenty of exits state the RESULT instead of a price — "85% gains im taking
    // profit now", "Out of DELL -25%". That's the analyst's own figure for the
    // trade, so when there's no price to work from we score it directly rather than
    // calling the trade unknown. A price still wins when we have one: it's exact,
    // and it stays consistent with how every other trade is measured.
    const statedExit = [...p.events].reverse().find(e =>
      e.realizedPct != null && ["trim", "cut", "close", "expire"].includes(e.action));
    const statedPct = statedExit ? Number(statedExit.realizedPct) : null;

    // A price-derived P&L needs a REAL cost basis. When the entry was never captured
    // (basisDefaulted), an exit price divided by the placeholder 1 invents absurd
    // figures — a $260 exit reads as +25,900%, a $10 share exit as +900%. So a priced
    // score is only trusted when there's a real basis; otherwise fall back to the
    // analyst's own stated result, and failing that treat it as unpriced. This is the
    // same rule already applied when there's no exit at all (below).
    const priceScoreable = exitFrom != null && !basisDefaulted;

    // With neither a trustworthy price nor a stated result, the outcome is genuinely
    // UNKNOWN. Booking it at entry (0.0%) would claim a scratch on what is usually a
    // total loss; booking it at 0 (-100%) would claim a loss we cannot evidence. So it
    // is scored as unpriced and left OUT of win-rate / averages until confirmed.
    const unpriced = !priceScoreable && statedPct == null;
    const pnlFrom = priceScoreable ? "price" : statedPct != null ? "stated" : null;
    const pnlPct = priceScoreable
      ? (basis ? (exitFrom - basis) / basis * sign : 0)
      : statedPct != null ? statedPct : 0;
    // Show a matching exit figure so "basis → exit" still reads, derived from the
    // analyst's stated result when that's all we have.
    const exit = priceScoreable ? exitFrom
      : (statedPct != null && basis != null ? basis * (1 + statedPct * sign) : basis);

    // Did an automated card and a manual update disagree materially about the same
    // position? Informational only — surfaced so a discrepancy can be dismissed or
    // corrected, never used to silently pick one over the other.
    const isCard = (e) => String(e.raw || "").startsWith("[image card]");
    const marks = p.events.filter(e => e.currentPrice != null);
    const lastCard = [...marks].reverse().find(isCard);
    const lastManual = [...marks].reverse().find(e => !isCard(e));
    const cardConflict = !!(lastCard && lastManual && lastCard.currentPrice
      && Math.abs(lastManual.currentPrice - lastCard.currentPrice) / lastCard.currentPrice > 0.05);

    return {
      ...p,
      basis,
      exit,
      unpriced,
      pnlFrom,        // "price" | "stated" | null — how this trade's P&L was derived
      cardConflict,
      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, dedupeEvents,
  pick, between, rng, mulberry32, randInt,
});
