/**
 * CryptoMarketDataService.js
 *
 * Singleton crypto market service. Call cryptoMarketData.start() once at boot.
 * After that, cryptoMarketData.getTicker(symbol) is always available.
 *
 * Flow:
 *   start()
 *     → _getSymbols()            load coin symbols from DB
 *     → CryptoEngine init        register all coins
 *     → 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 and StockMarketDataService exactly —
 *   same singleton pattern, same poll/tick separation, same snapshot conventions.
 *
 * • Crypto is 24/7 — no session-closed guards needed, but the engine applies
 *   a soft activity rhythm by UTC hour (Asia / Europe / US / Late US).
 *
 * • Spreads are in basis points (bps), not ticks or pips. The snapshot exposes
 *   spreadBps, spreadPct, and spread (absolute) so consumers can choose.
 *
 * • Volume is tracked in both base coin units (volume) and USD (volumeUsd).
 *
 * • Liquidation trades are flagged in the tape (isLiquidation: true).
 */

"use strict";

const axios = require("axios");
const CryptoEngine = require("../engine/crypto/CryptoEngine.js");
const { MarketPair, SymbolStats, Asset } = require("../models/index.js");
const conversionService = require("./conversionService.js");

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

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

function convertSymbol(sym) {
  return `${t.symbol.split('_')[0]}USDT`.toUpperCase();
}

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

class CryptoMarketDataService {
  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;
    this._pollingTimes = 0;
    this._tickTimes = 0;
  }

  // ─── 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 = 20_000, tickMs = 10_000 } = {}) {
    if (this._started) {
      console.warn("[CryptoMarketDataService] Already started — skipping");
      return;
    }

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


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

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

    // ── 2. Build engine with all symbols ───────────────────────────────────
    const ticksPerDay = Math.round((24 * 60 * 60 * 1000) / tickMs); 
    this._engine = new CryptoEngine(symbolList, 42, { ticksPerDay });

    // ── 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, 1.0);
      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(`[CryptoMarketDataService] Running — pollMs=${pollMs} tickMs=${tickMs}`);
  }

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

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

  /**
   * Latest ticker for one symbol.
   * Always returns an object after start() — never undefined.
   *
   * @param   {string} symbol   e.g. "BTCUSDT"
   * @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: "spot" },
        include: [
          { model: SymbolStats, as: "rate" },
          { model: Asset, as: 'base', required: true },
          { model: Asset, as: 'quote', required: true }
        ],
      });

      return pairs
            .map(p => {
                if (!p.base?.symbol || !p.quote?.symbol) return null;

                // ✅ Build symbol from base + quote assets
                const symbol = `${p.base.symbol}${p.quote.symbol}`.toUpperCase();

                return {
                    symbol,                          // "BTCUSDT"
                    name:     p.name,
                    base:     p.base.symbol,         // "BTC"
                    quote:    p.quote.symbol,        // "USDT"
                    rate:
                        safeNum(p.rate?.current_price, 0) ||
                        safeNum(p.rate?.close,         0) ||
                        1.0,
                    fallback:
                        safeNum(p.rate?.close,         0) ||
                        safeNum(p.rate?.current_price, 0) ||
                        1.0,
                };
            })
            .filter(Boolean);

    } catch (err) {
      console.error("[CryptoMarketDataService] _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 freshSymbols = await this._getSymbols();

      // Find symbols that weren't in the engine before
      const existingSet = new Set(this._symbols.map(s => s.symbol));
      const newSymbols = freshSymbols.filter(s => !existingSet.has(s.symbol));

      if (newSymbols.length > 0) {
        console.log(`[CryptoMarketDataService] New symbols detected:`, newSymbols.map(s => s.symbol));

        // Register new coins in engine
        for (const asset of newSymbols) {
          const price = safeNum(asset.rate, 0) || safeNum(asset.fallback, 1.0);
          this._engine.updatePrice(asset.symbol, price); // initialises + seeds
        }

        // Update master symbol list
        this._symbols = freshSymbols;
      }

      // Also detect removed symbols (optional — just stop ticking them)
      const freshSet = new Set(freshSymbols.map(s => s.symbol));
      const removed = this._symbols.filter(s => !freshSet.has(s.symbol));
      if (removed.length > 0) {
        console.log(`[CryptoMarketDataService] Symbols removed:`, removed.map(s => s.symbol));
        this._symbols = freshSymbols; // stop ticking removed symbols
      }



      const res = await conversionService.updateCryptoPrices();
      let data = res?.prices ?? [];

      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.lastPrice, 0) || safeNum(d.price, 0) || safeNum(d.current_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/stock services
        /* const simPrice = safeNum(this._engine.assets[symbol]?.price, live);
        const blended = simPrice * 0.01 + live * 0.99; */

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

    } catch (err) {
      // Non-fatal: engine keeps running on last anchored prices
      console.warn("[CryptoMarketDataService] _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);

        // Change vs open (intraday baseline — consistent with stock service)
        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),

          // Crypto-specific spread fields (bps is the native unit)
          spread: quote.spread ?? 0,
          spreadBps: quote.spreadBps ?? 0,
          spreadPct: quote.spreadPct ?? 0,

          decimals: quote.decimals ?? 2,
          tickSize: quote.tickSize ?? 0.01,

          // ── Change ───────────────────────────────────────────────────────
          change: parseFloat(change.toFixed(8)),
          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,   // base coin units
          volumeUsd: ohlc.volumeUsd ?? 0,   // USD equivalent

          // ── Session (24/7 — soft UTC rhythm, never closed) ───────────────
          session,
          sessionLabel: session.label ?? "unknown",

          // ── Book + tape ──────────────────────────────────────────────────
          orderBook: book,
          trades: snap.trades ?? [],   // includes isLiquidation flag per trade

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

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

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