// src/realtime/wsMarket.js
const { WebSocketServer } = require("ws");

function safeSend(ws, obj) {
  if (ws.readyState !== ws.OPEN) return;
  ws.send(JSON.stringify(obj));
}

function normSymbols(arr) {
  return (Array.isArray(arr) ? arr : [])
    .map(s => String(s).trim().toUpperCase())
    .filter(Boolean);
}

function normChannels(arr) {
  const allowed = new Set(["ticker", "orderbook"]);
  return (Array.isArray(arr) ? arr : [])
    .map(c => String(c).trim().toLowerCase())
    .filter(c => allowed.has(c));
}

/**
 * Very small in-memory market generator (replace later with real feed)
 */
function round(n, dp = 2) {
  const m = 10 ** dp;
  return Math.round(n * m) / m;
}
function seedPrice(symbol) {
  let seed = 0;
  for (const ch of symbol) seed += ch.charCodeAt(0);
  const open = round(50 + (seed % 400), 2);
  const price = round(open + (Math.random() - 0.5) * 2, 2);
  return { open, price };
}
function createBook(mid) {
  const levels = 10, spread = 0.02, step = 0.05;
  const bestBid = round(mid - spread / 2, 2);
  const bestAsk = round(mid + spread / 2, 2);
  const bids = [], asks = [];
  for (let i = 0; i < levels; i++) {
    bids.push({ p: round(bestBid - i * step, 2), q: Math.floor(1 + Math.random() * 200) });
    asks.push({ p: round(bestAsk + i * step, 2), q: Math.floor(1 + Math.random() * 200) });
  }
  return { bids, asks };
}
function applyBookDelta(book) {
  const pick = (arr) => Math.floor(Math.random() * arr.length);
  const bidUpdates = [];
  const askUpdates = [];

  for (let i = 0; i < 3; i++) {
    const b = book.bids[pick(book.bids)];
    const a = book.asks[pick(book.asks)];

    const newBQ = Math.max(0, b.q + Math.floor((Math.random() - 0.5) * 60));
    const newAQ = Math.max(0, a.q + Math.floor((Math.random() - 0.5) * 60));

    bidUpdates.push({ p: b.p, q: newBQ });
    askUpdates.push({ p: a.p, q: newAQ });
  }

  const apply = (side, updates, isBid) => {
    const map = new Map(side.map(l => [l.p, l.q]));
    for (const u of updates) {
      if (u.q === 0) map.delete(u.p);
      else map.set(u.p, u.q);
    }
    const arr = [...map.entries()].map(([p, q]) => ({ p: Number(p), q: Number(q) }));
    arr.sort((x, y) => (isBid ? y.p - x.p : x.p - y.p));
    return arr.slice(0, 10);
  };

  book.bids = apply(book.bids, bidUpdates, true);
  book.asks = apply(book.asks, askUpdates, false);

  return { bidUpdates, askUpdates };
}

/**
 * WS setup:
 * - path: /ws/market
 * - origin allowlist (like your cors)
 * - protocol:
 *   client -> {type:"subscribe", symbols:["AAPL"], channels:["ticker","orderbook"]}
 *   client -> {type:"unsubscribe", symbols:["AAPL"], channels:["orderbook"]}
 *   server -> {type:"ticker", ...}
 *   server -> {type:"orderbook_snapshot", ...}
 *   server -> {type:"orderbook_delta", ...}
 */
function attachMarketWS(httpServer, { allowedOrigins = [] } = {}) {
  const wss = new WebSocketServer({ server: httpServer, path: "/ws/market" });

  // per-client subscriptions: Map(symbol -> Set(channels))
  function newSubs() {
    return new Map();
  }

  // in-memory symbol states
  const market = new Map(); // symbol -> {open, price, book}

  function ensureSymbol(symbol) {
    if (market.has(symbol)) return;
    const { open, price } = seedPrice(symbol);
    market.set(symbol, { open, price, book: createBook(price) });
  }

  // Origin check
  wss.on("headers", (headers, req) => {
    const origin = req.headers.origin;
    if (origin && allowedOrigins.length && !allowedOrigins.includes(origin)) {
      // ws doesn't give us a clean "reject" here; we terminate on connection event
    }
  });

  wss.on("connection", (ws, req) => {
    const origin = req.headers.origin;

    if (origin && allowedOrigins.length && !allowedOrigins.includes(origin)) {
      ws.close(1008, "Origin not allowed");
      return;
    }

    ws.subs = newSubs();
    ws.isAlive = true;

    ws.on("pong", () => (ws.isAlive = true));

    safeSend(ws, { type: "welcome", serverTime: Date.now(), path: "/ws/market" });

    ws.on("message", (buf) => {
      let msg;
      try {
        msg = JSON.parse(buf.toString());
      } catch {
        return safeSend(ws, { type: "error", message: "Invalid JSON" });
      }

      const type = String(msg?.type || "");

      if (type === "ping") return safeSend(ws, { type: "pong", ts: msg?.ts ?? Date.now() });

      if (type === "subscribe" || type === "unsubscribe") {
        const symbols = normSymbols(msg.symbols);
        const channels = normChannels(msg.channels);

        if (!symbols.length || !channels.length) {
          return safeSend(ws, { type: "error", message: "symbols & channels required" });
        }

        for (const sym of symbols) ensureSymbol(sym);

        if (type === "subscribe") {
          for (const sym of symbols) {
            if (!ws.subs.has(sym)) ws.subs.set(sym, new Set());
            const set = ws.subs.get(sym);
            channels.forEach((c) => set.add(c));
          }

          safeSend(ws, { type: "subscribed", symbols, channels });

          // immediate snapshot(s)
          for (const sym of symbols) {
            const st = market.get(sym);
            const set = ws.subs.get(sym);

            if (set.has("ticker")) {
              const change = round(st.price - st.open, 2);
              const changePct = st.open ? round((change / st.open) * 100, 4) : 0;
              safeSend(ws, { type: "ticker", symbol: sym, ts: Date.now(), price: st.price, change, changePct });
            }
            if (set.has("orderbook")) {
              safeSend(ws, { type: "orderbook_snapshot", symbol: sym, ts: Date.now(), bids: st.book.bids, asks: st.book.asks });
            }
          }
        } else {
          for (const sym of symbols) {
            const set = ws.subs.get(sym);
            if (!set) continue;
            channels.forEach((c) => set.delete(c));
            if (set.size === 0) ws.subs.delete(sym);
          }
          safeSend(ws, { type: "unsubscribed", symbols, channels });
        }

        return;
      }

      safeSend(ws, { type: "error", message: `Unknown type: ${type}` });
    });
  });

  // Broadcast loop (filtered per-client)
  const intervalMs = 250;
  const loop = setInterval(() => {
    const ts = Date.now();

    // update market state
    for (const [sym, st] of market.entries()) {
      const drift = (Math.random() - 0.5) * 0.2;
      st.price = round(Math.max(0.01, st.price + drift), 2);
      st._delta = applyBookDelta(st.book);
      st._ts = ts;
    }

    // push updates to each client only for their subs
    wss.clients.forEach((ws) => {
      if (ws.readyState !== ws.OPEN) return;

      for (const [sym, channelSet] of ws.subs.entries()) {
        const st = market.get(sym);
        if (!st) continue;

        if (channelSet.has("ticker")) {
          const change = round(st.price - st.open, 2);
          const changePct = st.open ? round((change / st.open) * 100, 4) : 0;
          safeSend(ws, { type: "ticker", symbol: sym, ts, price: st.price, change, changePct });
        }

        if (channelSet.has("orderbook")) {
          const sendSnapshot = Math.random() < 0.03;
          if (sendSnapshot) {
            safeSend(ws, { type: "orderbook_snapshot", symbol: sym, ts, bids: st.book.bids, asks: st.book.asks });
          } else {
            safeSend(ws, {
              type: "orderbook_delta",
              symbol: sym,
              ts,
              bids: st._delta?.bidUpdates ?? [],
              asks: st._delta?.askUpdates ?? [],
            });
          }
        }
      }
    });
  }, intervalMs);

  // Heartbeat
  const hb = setInterval(() => {
    wss.clients.forEach((ws) => {
      if (ws.isAlive === false) return ws.terminate();
      ws.isAlive = false;
      ws.ping();
    });
  }, 20000);

  return {
    wss,
    close: () => {
      clearInterval(loop);
      clearInterval(hb);
      wss.close();
    },
  };
}

module.exports = { attachMarketWS };