/**
 * StockMarketDataService.js
 *
 * Singleton stock market service. Call stockMarketData.start() once at boot.
 * After that, stockMarketData.getTicker(symbol) is always available.
 *
 * Flow:
 *   start()
 *     → _getSymbols()            load stock symbols from DB
 *     → HybridEngine init        register all stocks
 *     → seed from DB prices      so engine never starts at fallback price
 *     → _pollRates()             fetch live rates, anchor engine (await before first tick)
 *     → _tick()                  first snapshot immediately
 *     → setInterval _pollRates   every pollMs
 *     → setInterval _tick        every tickMs
 *
 * DESIGN NOTES
 * ─────────────────────────────────────────────────────────────────────────────
 * • Mirrors ForexMarketDataService exactly — same singleton pattern, same
 *   poll/tick separation, same snapshot shape conventions.
 *
 * • We drive the tick loop ourselves (setInterval → _tick) rather than calling
 *   engine.start(), so poll and tick stay in sync and snapshots always reflect
 *   the latest engine state.
 *
 * • _pollRates() writes live prices to the engine via updatePrice() with the
 *   same 5/95 blend used by the forex service. The engine mean-reverts toward
 *   the blended price — no teleporting.
 */

"use strict";

const axios        = require("axios");
const HybridEngine = require("../engine/stock/StockEngine.js");
const { MarketPair, SymbolStats } = require("../models/index.js");

// ─────────────────────────────────────────────────────────────────────────────

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

// ─────────────────────────────────────────────────────────────────────────────

class StockMarketDataService {
  constructor() {
    this._engine    = null;
    this._symbols   = [];      // [{ symbol, name, rate, fallback }]
    this._snapshot  = {};      // { [SYMBOL]: ticker } — always populated after start()
    this._ratesPoll = null;
    this._tickTimer = null;
    this._polling   = false;   // prevents overlapping poll calls
    this._started   = false;
  }

  // ─── Startup / shutdown ────────────────────────────────────────────────────

  /**
   * Boot the service. Awaits real prices before first tick so
   * the engine never starts at the fallback price.
   *
   * @param {{ pollMs?: number, tickMs?: number }} opts
   */
  async start({ pollMs = 5_000, tickMs = 5_000 } = {}) {
    if (this._started) {
      console.warn("[StockMarketDataService] Already started — skipping");
      return;
    }

    // ── 1. Load symbols from DB ─────────────────────────────────────────────
    this._symbols = await this._getSymbols();

    if (!this._symbols.length) {
      console.warn("[StockMarketDataService] No stock symbols found in DB — nothing to start");
      return;
    }

    const symbolList = this._symbols.map(s => s.symbol);
    console.log(`[StockMarketDataService] Loaded ${symbolList.length} symbols: ${symbolList.join(", ")}`);

    // ── 2. Build engine with all symbols ───────────────────────────────────
    this._engine = new HybridEngine(symbolList, 42, { ticksPerDay: 100 });

    // ── 3. Seed engine with DB prices so no symbol starts at fallback ───────
    for (const asset of this._symbols) {
      const price = safeNum(asset.rate, 0) || safeNum(asset.fallback, 100);
      this._engine.updatePrice(asset.symbol, price);
    }

    // ── 4. Fetch live rates NOW and anchor engine before first tick ─────────
    await this._pollRates();

    // ── 5. First tick — snapshot immediately available ──────────────────────
    this._tick();

    // ── 6. Mark started before intervals so stop() is always safe ──────────
    this._started = true;

    // ── 7. Start intervals ──────────────────────────────────────────────────
    this._ratesPoll = setInterval(() => this._pollRates(), pollMs);
    this._tickTimer = setInterval(() => this._tick(),      tickMs);

    console.log(`[StockMarketDataService] Running — pollMs=${pollMs} tickMs=${tickMs}`);
  }

  stop() {
    clearInterval(this._ratesPoll);
    clearInterval(this._tickTimer);
    this._ratesPoll = null;
    this._tickTimer = null;
    this._started   = false;
    console.log("[StockMarketDataService] Stopped");
  }

  // ─── Public getters ────────────────────────────────────────────────────────

  /**
   * Latest ticker for one symbol.
   * Always returns an object after start() — never undefined.
   *
   * @param   {string} symbol   e.g. "AAPL"
   * @returns {object|null}
   */
  getTicker(symbol) {
    return this._snapshot[String(symbol ?? "").toUpperCase()] ?? null;
  }

  /**
   * All tickers as an array.
   * @returns {object[]}
   */
  getAllTickers() {
    return Object.values(this._snapshot);
  }

  /**
   * All tickers as a map { [SYMBOL]: ticker }.
   * @returns {object}
   */
  getAllTickersMap() {
    return { ...this._snapshot };
  }

  // ─── DB ───────────────────────────────────────────────────────────────────

  async _getSymbols() {
    try {
      const pairs = await MarketPair.findAll({
        where:   { type: "stock" },
        include: [{ model: SymbolStats, as: "rate" }],
      });

      return pairs
        .map(p => ({
          symbol:   String(p.symbol ?? "").toUpperCase(),
          name:     p.name,
          // Prefer current_price; fall back to last close; default 100 so the
          // engine always starts at a non-zero price
          rate:
            safeNum(p.rate?.current_price, 0) ||
            safeNum(p.rate?.close,         0) ||
            100,
          fallback:
            safeNum(p.rate?.close,         0) ||
            safeNum(p.rate?.current_price, 0) ||
            100,
        }))
        .filter(p => p.symbol);

    } catch (err) {
      console.error("[StockMarketDataService] _getSymbols failed:", err.message);
      return [];
    }
  }

  // ─── Rate poll ────────────────────────────────────────────────────────────

  async _pollRates() {
    // Skip if a previous poll is still in flight
    if (this._polling) return;
    this._polling = true;

    try {
      const res  = await axios.get("http://localhosts:5001/api/v1/latest/rates/type/stock", {
        timeout: 4_000,
      });

      const data = res?.data?.data?.rates ?? res?.data?.rates ?? [];

      for (const d of data) {
        const symbol = String(d.symbol ?? d.pair ?? "").toUpperCase();
        if (!symbol || !this._engine?.states[symbol]) continue;

        // Accept price, mid, or close from the API response
        const live = safeNum(d.price, 0) || safeNum(d.mid, 0) || safeNum(d.close, 0);
        if (live <= 0) continue;

        // Blend: 5% sim weight, 95% live — same anchor ratio as forex service
        const simPrice = safeNum(this._engine.assets[symbol]?.price, live);
        const blended  = simPrice * 0.05 + live * 0.95;

        this._engine.updatePrice(symbol, blended);
      }

    } catch (err) {
      // Non-fatal: engine keeps running on last anchored prices
      console.warn("[StockMarketDataService] _pollRates failed:", err.message);
    } finally {
      this._polling = false;
    }
  }

  // ─── Engine tick ──────────────────────────────────────────────────────────

  _tick() {
    const eng = this._engine;
    if (!eng) return;

    for (const asset of this._symbols) {
      const sym = asset.symbol;

      // Skip symbols that haven't been initialised yet (updatePrice not called)
      if (!eng.states[sym]?._initialised) continue;

      try {
        const snap = eng.tick(sym, { drift: true, book: true, trades: true });

        const quote   = snap.quote     ?? {};
        const ohlc    = snap.asset     ?? {};
        const book    = snap.orderBook ?? { bids: [], asks: [] };
        const session = snap.session   ?? {};

        const price = safeNum(quote.mid ?? ohlc.price, 0);

        // Session flags — mirrors forex service naming convention
        const isMarketOpen = session.label === "morning" || session.label === "afternoon";
        const isPreMarket  = session.label === "pre-market";
        const isAfterHours = session.label === "after-hours";
        const isClosed     = session.label === "closed";
        const isOpenBell   = session.label === "open-bell";
        const isCloseBell  = session.label === "close-bell";

        // Change vs open (intraday baseline — same approach as forex uses session baseline)
        const open      = safeNum(ohlc.open, price);
        const change    = open > 0 ? price - open : 0;
        const changePct = open > 0 ? (change / open) * 100 : 0;

        this._snapshot[sym] = {
          symbol: sym,

          // ── Price ────────────────────────────────────────────────────────
          price,
          bid:         safeNum(quote.bid, 0),
          ask:         safeNum(quote.ask, 0),
          mid:         safeNum(quote.mid, price),
          spread:      quote.spread      ?? 0,
          spreadPct:   quote.spreadPct   ?? 0,
          spreadTicks: quote.spreadTicks ?? 0,
          tickSize:    eng.states[sym]?.tickSize ?? 0.01,

          // ── Change ───────────────────────────────────────────────────────
          change:        parseFloat(change.toFixed(4)),
          changePct:     parseFloat(changePct.toFixed(4)),
          percentChange: parseFloat(changePct.toFixed(4)),
          isUp:          change > 0,
          isDown:        change < 0,

          // ── OHLCV ────────────────────────────────────────────────────────
          open:   safeNum(ohlc.open,  price),
          high:   safeNum(ohlc.high,  price),
          low:    safeNum(ohlc.low,   price),
          close:  safeNum(ohlc.close, price),
          volume: ohlc.volume ?? 0,

          // ── Session ──────────────────────────────────────────────────────
          session,
          isMarketOpen,
          isPreMarket,
          isAfterHours,
          isClosed,
          isOpenBell,
          isCloseBell,

          // ── Book + tape ──────────────────────────────────────────────────
          orderBook: book,
          trades:    snap.trades ?? [],

          // ── Meta ─────────────────────────────────────────────────────────
          updatedAt: Date.now(),
        };

      } catch (err) {
        console.error(`[StockMarketDataService] tick failed for ${sym}:`, err.message);
      }
    }
  }
}

// Export singleton — one engine, shared across the whole process
module.exports = new StockMarketDataService();