const RNG = require("../RNG.js");

const SESSIONS = [
  [0.00,  4.00, "closed",      0.10, 6.0],
  [4.00,  9.50, "pre-market",  0.40, 2.5],
  [9.50, 10.00, "open-bell",   2.60, 2.0],
  [10.00, 12.0, "morning",     1.00, 1.0],
  [12.00, 13.5, "lunch",       0.60, 1.2],
  [13.50, 15.5, "afternoon",   0.85, 1.0],
  [15.50, 16.0, "close-bell",  2.20, 1.7],
  [16.00, 20.0, "after-hours", 0.25, 3.5],
  [20.00, 24.0, "closed",      0.10, 6.0],
];

function getSession(nowMs, tzOffsetHours = -5) {
  const d = new Date(nowMs + tzOffsetHours * 3_600_000);
  const t = d.getUTCHours() + d.getUTCMinutes() / 60 + d.getUTCSeconds() / 3600;
  for (const [start, end, label, volMult, spreadMult] of SESSIONS) {
    if (t >= start && t < end) return { label, volMult, spreadMult };
  }
  return { label: "closed", volMult: 0.10, spreadMult: 6.0 };
}

// ─────────────────────────────────────────────────────────────────────────────
// ── Price helpers ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────

function autoTickSize(price) {
  if (price < 1)   return 0.0001;
  if (price < 5)   return 0.01;
  if (price < 50)  return 0.01;
  if (price < 500) return 0.01;
  return 0.01;
}

function roundTick(price, tickSize) {
  return Math.round(price / tickSize) * tickSize;
}

function fmtPrice(price, tickSize) {
  const decimals = tickSize < 0.01 ? 4 : tickSize < 0.1 ? 2 : 0;
  return parseFloat(price.toFixed(decimals));
}

function safeRate(value, fallback = 0) {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? n : fallback;
}

// ─────────────────────────────────────────────────────────────────────────────
// ── Symbol profiles (optional per-symbol overrides) ───────────────────────────
// Extend this map to hard-code known profiles; any unknown symbol gets defaults.
// ─────────────────────────────────────────────────────────────────────────────

const SYMBOL_PROFILES = {
  // High-cap tech — moderate vol, tight spread
  AAPL:  { dailyVol: 0.013, reversionSpeed: 0.04, jumpProb: 0.003, jumpStdLog: 0.008 },
  MSFT:  { dailyVol: 0.012, reversionSpeed: 0.04, jumpProb: 0.003, jumpStdLog: 0.007 },
  GOOGL: { dailyVol: 0.014, reversionSpeed: 0.04, jumpProb: 0.004, jumpStdLog: 0.009 },
  AMZN:  { dailyVol: 0.016, reversionSpeed: 0.04, jumpProb: 0.004, jumpStdLog: 0.010 },
  META:  { dailyVol: 0.018, reversionSpeed: 0.04, jumpProb: 0.005, jumpStdLog: 0.012 },
  NVDA:  { dailyVol: 0.025, reversionSpeed: 0.05, jumpProb: 0.006, jumpStdLog: 0.015 },
  // High-vol momentum names
  TSLA:  { dailyVol: 0.032, reversionSpeed: 0.03, jumpProb: 0.007, jumpStdLog: 0.020 },
  COIN:  { dailyVol: 0.040, reversionSpeed: 0.03, jumpProb: 0.008, jumpStdLog: 0.025 },
  // ETFs — lower vol, mean-revert faster
  SPY:   { dailyVol: 0.010, reversionSpeed: 0.06, jumpProb: 0.002, jumpStdLog: 0.006 },
  QQQ:   { dailyVol: 0.012, reversionSpeed: 0.05, jumpProb: 0.002, jumpStdLog: 0.007 },
  // Financials
  JPM:   { dailyVol: 0.014, reversionSpeed: 0.04, jumpProb: 0.003, jumpStdLog: 0.009 },
  GS:    { dailyVol: 0.015, reversionSpeed: 0.04, jumpProb: 0.003, jumpStdLog: 0.010 },
};

function getSymbolProfile(symbol) {
  return SYMBOL_PROFILES[symbol] ?? {
    dailyVol:       0.015,
    reversionSpeed: 0.04,
    jumpProb:       0.003,
    jumpStdLog:     0.008,
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// ── HybridEngine (Node.js-native, no React deps) ──────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────

const EXCHANGES = ["N", "Q", "P", "C"]; // NYSE / Nasdaq / ARCA / CBOE

class StockEngine {
  constructor(symbols = [], seed = 42, options = {}) {
    this._rng  = new RNG(seed);
    this._opts = {
      tzOffsetHours: options.tzOffsetHours ?? -5,
      ticksPerDay:   options.ticksPerDay   ?? 100,
      maxBookLevels: options.maxBookLevels ?? 10,
      maxTrades:     options.maxTrades     ?? 50,
      ...options,
    };

    this.states       = {};
    this.assets       = {};
    this.quotes       = {};
    this.orderBooks   = {};
    this.recentTrades = {};
    this.sessions     = {};

    const syms = Array.isArray(symbols) ? symbols : [symbols];
    for (const s of syms) if (s) this._registerSymbol(s);
  }

  // ── Public ──────────────────────────────────────────────────────────────

  updatePrice(symbol, price) {
    const p = Math.max(Number(price) || 0.01, 0.0001);

    if (!this.states[symbol] || !this.states[symbol]._initialised) {
      this._initSymbol(symbol, p);
      return;
    }

    const st = this.states[symbol];
    st.fairValue = p;

    const logDrift = Math.abs(Math.log(p / Math.max(st.price, 1e-6)));
    if (logDrift > 0.01) {
      st.garchVar = Math.min(st.garchVar * 1.3, st.baseTickVol * st.baseTickVol * 9);
    }
  }

  tick(symbol, opts = { drift: true, book: true, trades: true }) {
    if (!this.states[symbol] || !this.states[symbol]._initialised) {
      this._initSymbol(symbol, 100);
    }

    const st      = this.states[symbol];
    const nowMs   = Date.now();
    const session = getSession(nowMs, this._opts.tzOffsetHours);
    this.sessions[symbol] = session;

    if (opts.drift  !== false) this._evolvePrice(symbol, st, session);
    this._computeQuotes(symbol, st, session);
    if (opts.book   !== false) this._buildOrderBook(symbol, st, session);
    if (opts.trades !== false) this._generateTrades(symbol, st, session);

    this.assets[symbol] = {
      price:  fmtPrice(st.price, st.tickSize),
      open:   fmtPrice(st.open,  st.tickSize),
      high:   fmtPrice(st.high,  st.tickSize),
      low:    fmtPrice(st.low,   st.tickSize),
      close:  fmtPrice(st.close, st.tickSize),
      volume: Math.round(st.volume),
    };

    return this._snapshot(symbol);
  }

  resetCandle(symbol) {
    const st = this.states[symbol];
    if (!st) return;
    st.open = st.price; st.high = st.price;
    st.low  = st.price; st.close = st.price;
    st.volume = 0;
  }

  // ── Price evolution ──────────────────────────────────────────────────────

  _evolvePrice(symbol, st, session) {
    const rng = this._rng;

    // GARCH(1,1): σ²_t = ω·σ²_LR + α·ε²_{t-1} + β·σ²_{t-1}
    const longRunVar = st.baseTickVol * st.baseTickVol;
    const rawVar =
      st.garchOmega * longRunVar +
      st.garchAlpha * (st.prevLogRet * st.prevLogRet) +
      st.garchBeta  * st.garchVar;

    st.garchVar = Math.min(Math.max(rawVar, 0.0625 * longRunVar), 25 * longRunVar);

    const tickVol   = Math.sqrt(st.garchVar) * session.volMult;
    const tickDrift = -0.5 * st.garchVar; // Itô correction

    // OU mean reversion
    const logGap       = Math.log(Math.max(st.fairValue, 1e-6)) - st.logPrice;
    const reversionRaw = st.reversionSpeed * logGap;
    const reversionPull= Math.max(-0.02, Math.min(0.02, reversionRaw));

    // Momentum
    st.momentumEMA =
      st.momentumDecay * st.momentumEMA +
      (1 - st.momentumDecay) * st.prevLogRet;
    const rawMomentum  = st.momentumWeight * st.momentumEMA;
    const momentumPull = Math.max(-st.momentumCap, Math.min(st.momentumCap, rawMomentum));

    // Fat-tailed shock
    const shock = rng.fattailNormal(0, tickVol, 4, 6);

    // Jump diffusion (earnings / news)
    let jumpContrib = 0;
    if (rng.next() < st.jumpProb * session.volMult) {
      jumpContrib = rng.normal(st.jumpMeanLog, st.jumpStdLog * session.volMult);
    }

    const logRet = tickDrift + reversionPull + momentumPull + shock + jumpContrib;

    st.logPrice   += logRet;
    st.price       = roundTick(Math.exp(st.logPrice), st.tickSize);
    st.price       = Math.max(st.price, st.tickSize);
    st.prevLogRet  = logRet;

    if (st.price > st.high) st.high = st.price;
    if (st.price < st.low)  st.low  = st.price;
    st.close = st.price;
  }

  // ── Quotes ───────────────────────────────────────────────────────────────

  _computeQuotes(symbol, st, session) {
    const rng = this._rng;

    const volFactor  = Math.sqrt(st.garchVar) / st.baseTickVol;
    const rawSpread  = st.tickSize * Math.max(1, 1.5 * volFactor) * session.spreadMult;

    const noise      = rng.normal(0, st.tickSize * 0.3);
    const mid        = st.price + noise;
    const halfSpread = rawSpread / 2;

    const bid = roundTick(mid - halfSpread, st.tickSize);
    const ask = roundTick(mid + halfSpread, st.tickSize);

    st.bid = Math.max(bid, st.tickSize);
    st.ask = Math.max(ask, st.bid + st.tickSize);

    this.quotes[symbol] = {
      bid:         fmtPrice(st.bid, st.tickSize),
      ask:         fmtPrice(st.ask, st.tickSize),
      mid:         fmtPrice((st.bid + st.ask) / 2, st.tickSize),
      spread:      parseFloat((st.ask - st.bid).toFixed(4)),
      spreadPct:   parseFloat(((st.ask - st.bid) / st.bid * 100).toFixed(4)),
      spreadTicks: Math.round((st.ask - st.bid) / st.tickSize),
    };
  }

  // ── Order book ───────────────────────────────────────────────────────────

  _buildOrderBook(symbol, st, session) {
    const rng    = this._rng;
    const levels = this._opts.maxBookLevels;
    const tick   = st.tickSize;

    const sessionScale = session.volMult < 0.5 ? 0.3 : 1.0;
    const priceScale   = st.price < 10 ? 5 : st.price < 50 ? 2 : st.price < 200 ? 1 : 0.5;
    const baseSize     = Math.max(100, Math.round(500 * priceScale * sessionScale));

    const bids = [];
    const asks = [];

    for (let i = 0; i < levels; i++) {
      const bidPrice = roundTick(st.bid - i * tick, tick);
      const askPrice = roundTick(st.ask + i * tick, tick);

      const decayFactor = Math.exp(-i * 0.35);
      const bidSize     = Math.max(1, Math.round(baseSize * decayFactor * rng.logNormal(0, 0.4)));
      const askSize     = Math.max(1, Math.round(baseSize * decayFactor * rng.logNormal(0, 0.4)));

      const bidDisplay  = rng.next() < 0.05 ? 100 : bidSize;
      const askDisplay  = rng.next() < 0.05 ? 100 : askSize;

      if (bidPrice > 0) {
        bids.push({
          price:     fmtPrice(bidPrice, tick),
          size:      bidDisplay,
          totalSize: bidSize,
          isIceberg: bidDisplay !== bidSize,
          orders:    rng.int(1, 8),
        });
      }
      asks.push({
        price:     fmtPrice(askPrice, tick),
        size:      askDisplay,
        totalSize: askSize,
        isIceberg: askDisplay !== askSize,
        orders:    rng.int(1, 8),
      });
    }

    this.orderBooks[symbol] = { bids, asks };
  }

  // ── Trade tape ───────────────────────────────────────────────────────────

  _generateTrades(symbol, st, session) {
    const rng       = this._rng;
    const maxTrades = this._opts.maxTrades;

    const lambdaBase = session.volMult < 0.5 ? 1.5 : 4;
    const numTrades  = rng.poisson(lambdaBase);

    if (!this.recentTrades[symbol]) this.recentTrades[symbol] = [];
    const tape = this.recentTrades[symbol];

    for (let i = 0; i < numTrades; i++) {
      const bullBias  = 0.5 + Math.max(-0.3, Math.min(0.3, st.momentumEMA * 20));
      const isBuy     = rng.next() < bullBias;
      const basePrice = isBuy ? st.ask : st.bid;
      const execPrice = roundTick(
        basePrice + rng.normal(0, st.tickSize * 0.15),
        st.tickSize,
      );

      const isBlock = rng.next() < 0.02;
      const size    = isBlock
        ? rng.int(1_000, 10_000)
        : Math.max(1, Math.round(rng.logNormal(Math.log(200), 0.9)));

      tape.unshift({
        price:    fmtPrice(Math.max(execPrice, st.tickSize), st.tickSize),
        size,
        side:     isBuy ? "buy" : "sell",
        isBlock,
        ts:       Date.now() - rng.int(0, 4_900),
        exchange: rng.pick(EXCHANGES),
      });

      st.volume += size;
    }

    if (tape.length > maxTrades) tape.length = maxTrades;
  }

  // ── Init / snapshot ──────────────────────────────────────────────────────

  _registerSymbol(symbol) {
    this.states[symbol]       = { _initialised: false };
    this.assets[symbol]       = { price: 0, open: 0, high: 0, low: 0, close: 0, volume: 0 };
    this.quotes[symbol]       = { bid: 0, ask: 0, mid: 0, spread: 0, spreadPct: 0, spreadTicks: 1 };
    this.orderBooks[symbol]   = { bids: [], asks: [] };
    this.recentTrades[symbol] = [];
    this.sessions[symbol]     = getSession(Date.now(), this._opts.tzOffsetHours);
  }

  _initSymbol(symbol, price) {
    const p           = Math.max(Number(price) || 100, 0.0001);
    const profile     = getSymbolProfile(symbol);
    const ticksPerDay = this._opts.ticksPerDay;
    const dailyVol    = profile.dailyVol;
    const baseTickVol = dailyVol / Math.sqrt(ticksPerDay);
    const tickSize    = autoTickSize(p);

    const st = {
      _initialised: true,
      price:        p,
      logPrice:     Math.log(p),
      fairValue:    p,
      prevLogRet:   0,

      tickSize,
      lotSize: 1,

      garchVar:   baseTickVol * baseTickVol,
      garchOmega: 0.10,
      garchAlpha: 0.08,
      garchBeta:  0.85,
      baseTickVol,

      momentumEMA:    0,
      momentumDecay:  0.80,
      momentumWeight: 0.06,
      momentumCap:    baseTickVol * 1.0,

      reversionSpeed: profile.reversionSpeed,

      jumpProb:    profile.jumpProb,
      jumpMeanLog: 0,
      jumpStdLog:  profile.jumpStdLog,

      lastTradeTs: Date.now(),
      open: p, high: p, low: p, close: p, volume: 0,
      bid: 0, ask: 0,
    };

    this.states[symbol]       = st;
    this.assets[symbol]       = { price: p, open: p, high: p, low: p, close: p, volume: 0 };
    this.quotes[symbol]       = { bid: 0, ask: 0, mid: p, spread: 0, spreadPct: 0, spreadTicks: 1 };
    this.orderBooks[symbol]   = { bids: [], asks: [] };
    this.recentTrades[symbol] = [];
    this.sessions[symbol]     = getSession(Date.now(), this._opts.tzOffsetHours);
  }

  _snapshot(symbol) {
    return {
      symbol,
      session:   this.sessions[symbol],
      asset:     this.assets[symbol],
      quote:     this.quotes[symbol],
      orderBook: this.orderBooks[symbol],
      trades:    this.recentTrades[symbol],
    };
  }
}

module.exports = StockEngine;