const ws = require("ws");
const { SymbolStats, Asset, Coin } = require("../models");
const { mapPrice } = require("../utils/helpers");

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


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

function sendRes(ws, payload) {
    if (ws.readyState !== ws.OPEN) return
    ws.send(JSON.stringify(payload))
}

function broadcast(ws, payload) {
    for (const client of ws.clients) {
        if (client.readyState !== WebSocket.OPEN) continue;

        client.send(JSON.stringify(payload));
    }
}

async function loadAllSymbols() {
    const rows = await SymbolStats.findAll({
        include: [
            { model: Asset, as: "asset" },
            { model: Coin, as: "coin" },
        ],
    });
    return rows.map((s) => mapPrice(s));
}


const attachWsServerws = (server, opts = {}) => {

    const { allowedOrigins = [] } = opts;
    const wss = new ws.WebSocketServer({
        server,
        path: '/ws',
        maxPayload: 1024 * 1024,
    })

    function newSubs() {
        return new Map();
    }

    const market = new Map();
    let allSymbolsArr = [];
    (async () => {
        try {
            allSymbolsArr = await loadAllSymbols();
            for (const s of allSymbolsArr) {
                market.set(s.symbol, { ...s, _ts: Date.now() });
            }
            console.log("[ws] loaded symbols:", allSymbolsArr.length);
        } catch (e) {
            console.error("[ws] failed to load symbols", e);
        }
    })()



    function checkSymbol(symbol) {
        const found = allSymbolsArr.find((x) => x.symbol === symbol);
        return (found.length === 0)
    }
    function ensureSymbol(symbol) {
        if (market.has(symbol)) return;
        const found = allSymbolsArr.find((x) => x.symbol === symbol);
        if (!found) return;

        market.set(symbol, {

        });
    }


    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 doesn't give us a clean "reject" here; we terminate on connection event
        }

        ws.subs = newSubs()
        ws.isAlive = true,
            ws.on("pong", () => (ws.isAlive = true))
        sendRes(ws, { message: "welcome", time: new Date() })

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

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

            if (type === "ping") return sendRes(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 sendRes(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));
                    }

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

                    // immediate snapshot(s)
                    for (const sym of symbols) {
                        const st = market.get(sym);
                        if (!st) continue;
                        sendRes(ws, { type: "symbol_stats", symbol: sym, data: st, ts: Date.now() });
                    }
                } 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);
                    }
                    sendRes(ws, { type: "unsubscribed", symbols, channels });
                }
                return;
            }
            sendRes(ws, { type: "error", message: `Unknown type: ${type}` });
        })
        ws.on("error", console.error)
    })

    const intervalMs = 250;
    const loop = setInterval(() => {
        const ts = Date.now();

        // 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('stocks')) {
                    sendRes(ws, { update: "stocks", message: "sentNewStocks", time: ts })
                }

                if (channelSet.has('symbol_stats')) {
                    sendRes(ws, { update: "symbol_stats", message: "sent New Symbols", time: ts })
                }
            }
        });
    }, intervalMs);

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

    function broadcastCall(data) {
        broadcast(wss, { type: 'cons_gotten', data: data });
    }

    /* function broadcastToChannel(channel, payload) {
        for (const [sym, channelSet] of ws.subs.entries()) {
            const st = market.get(sym);
            if (!st) continue;
            if (channelSet.has(channel)) {
             broadcast(wss, { channel, data: payload });
            }
 
        }
    } */

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

module.exports = {
    attachWsServerws
}