const axios = require("axios");
const { Op, fn, col } = require("sequelize");
const { MarketPair, Asset, Price1m, SymbolStats, MarketStats } = require("../models");
const logger = require("../utils/logger");

/**
 * Slugify a coin name the same way CoinMarketCap does.
 * e.g. "Bitcoin Cash" -> "bitcoin-cash"
 */
function cmcSlugify(name = "") {
    return name.trim().toLowerCase().replace(/\s+/g, "-");
}

class CryptoService {

    // ─────────────────────────────────────────────
    //  PUBLIC ENTRY POINTS
    // ─────────────────────────────────────────────

    async getAssets() {
        const assets = await Asset.findAll({ where: { type: "crypto" } });
        return assets;
    }

    async getCryptoData() {
        const pairs = await MarketPair.findAll({ where: { type: "spot" } });
        await this.updateCryptoRates(pairs);
        await Promise.all(pairs.map(p => this.calculate24hStats(p.id)));
        return { message: "Crypto data updated", data: pairs };
    }

    async updateCryptoAssets() {
        const assets = await this.getAssets();
        await this.updateCryptoData(assets);
        return { message: "Crypto assets updated", data: assets };
    }

    async updateMarketStat() {

        const assets = await this.getAssets();

        const [marketStats, athAtl] = await Promise.all([
            this.updateMarketStats(assets),
            this.updateAthAtl()
        ]);

        return { message: "Crypto market stats updated", data: { marketStats, athAtl } };
    }

    // ─────────────────────────────────────────────
    //  PRICE FETCHING  (Binance → Coinbase → CMC)
    // ─────────────────────────────────────────────

    async updateCryptoRates(cryptoAssets) {
        try {
            if (!cryptoAssets?.length) return;

            const now = new Date();
            const priceRows = [];   // rows for Price1m  (OHLCV candles)
            const statsRows = [];   // rows for SymbolStats (full 24h stats)
            const filledPairIds = new Set();

            // symbol (e.g. "BTCUSDT") → pairId
            const assetMap = cryptoAssets.reduce((map, asset) => {
                const symbol = normalizeSymbol(asset.symbol);
                map[symbol] = asset.id;
                return map;
            }, {});

            // ── 1) Binance ──────────────────────────────────────────────────
            // /api/v3/ticker/24hr returns everything we need in one shot:
            // openPrice, highPrice, lowPrice, lastPrice, volume, quoteVolume,
            // priceChange, priceChangePercent, bidPrice, askPrice, weightedAvgPrice
            const wantedSymbols = new Set(
                cryptoAssets.map(a => normalizeSymbol(a.symbol))
            );

            try {
                const { data } = await axios.get(
                    "https://api.binance.com/api/v3/ticker/24hr"
                );

                for (const t of data) {
                    const symbol = String(t.symbol || "").toUpperCase();
                    const pairId = assetMap[symbol];
                    if (!wantedSymbols.has(symbol) || !pairId || filledPairIds.has(pairId)) continue;

                    const open24h = parseFloat(t.openPrice) || 0;
                    const high24h = parseFloat(t.highPrice) || 0;
                    const low24h = parseFloat(t.lowPrice) || 0;
                    const currentPrice = parseFloat(t.lastPrice) || 0;
                    const volume24h = parseFloat(t.volume) || 0;       // base asset volume
                    const quoteVolume24h = parseFloat(t.quoteVolume) || 0;       // quote (USD) volume
                    const change24h = parseFloat(t.priceChange) || 0;
                    const changePercent24h = parseFloat(t.priceChangePercent) || 0;
                    const bidPrice = parseFloat(t.bidPrice) || 0;
                    const askPrice = parseFloat(t.askPrice) || 0;
                    const weightedAvg = parseFloat(t.weightedAvgPrice) || 0;

                    // 1-minute candle row
                    priceRows.push({
                        pairId,
                        open: open24h,
                        high: high24h,
                        low: low24h,
                        close: currentPrice,
                        volume: volume24h,
                        change24h: change24h.toString(),
                        percChange: changePercent24h.toString(),
                        timestamp: now,
                    });

                    // Full stats row (everything the frontend needs)
                    statsRows.push({
                        pairId,
                        currentPrice,
                        open24h,
                        high24h,
                        low24h,
                        volume24h: quoteVolume24h,   // store USD volume for market-cap ratio
                        change24h,
                        changePercent24h,
                        bid: bidPrice,
                        ask: askPrice,
                        spread: parseFloat((askPrice - bidPrice).toFixed(10)),
                        weightedAvgPrice: weightedAvg,
                    });

                    filledPairIds.add(pairId);
                }
            } catch (e) {
                console.error("Binance fetch failed, falling back to Coinbase...", e.message);
            }

            // ── 2) Coinbase fallback ────────────────────────────────────────
            // Coinbase spot only gives a single price; we synthesise what we can.
            const missingCoinbase = cryptoAssets.filter(a => !filledPairIds.has(a.id));

            if (missingCoinbase.length) {
                try {
                    const responses = await Promise.allSettled(
                        missingCoinbase.map(asset => {
                            const [base, quote] = String(asset.symbol || "").toUpperCase().split("/");
                            return axios.get(
                                `https://api.coinbase.com/v2/prices/${base}-${quote || "USD"}/spot`
                            );
                        })
                    );

                    responses.forEach((result, i) => {
                        if (result.status !== "fulfilled") return;
                        const asset = missingCoinbase[i];
                        if (filledPairIds.has(asset.id)) return;

                        const price = parseFloat(result.value?.data?.data?.amount) || 0;
                        if (!price) return;

                        priceRows.push({
                            pairId: asset.id,
                            open: price,
                            high: price,
                            low: price,
                            close: price,
                            volume: 0,
                            change24h: "0",
                            percChange: "0",
                            timestamp: now,
                        });

                        statsRows.push({
                            pairId: asset.id,
                            currentPrice: price,
                            open24h: price,
                            high24h: price,
                            low24h: price,
                            volume24h: 0,
                            change24h: 0,
                            changePercent24h: 0,
                            bid: price,
                            ask: price,
                            spread: 0,
                            weightedAvgPrice: price,
                        });

                        filledPairIds.add(asset.id);
                    });
                } catch (e) {
                    console.warn("Coinbase fetch failed, falling back to CMC...", e.message);
                }
            }

            // ── 3) CoinMarketCap fallback ───────────────────────────────────
            // CMC /quotes/latest gives price, volume_24h, percent_change_24h,
            // market_cap, circulating_supply, total_supply, max_supply.
            const missingCMC = cryptoAssets.filter(a => !filledPairIds.has(a.id));

            if (missingCMC.length) {
                try {
                    const { normalized: cmcData } = await this.fetchCmcMulti({
                        symbols: missingCMC.map(a => normalizeSymbol(a.symbol)),
                        convert: "USD",
                    });

                    cmcData.forEach(p => {
                        const normalizedSymbol = String(p.symbol || "").toUpperCase();
                        const pairId = assetMap[normalizedSymbol];
                        if (!pairId || filledPairIds.has(pairId)) return;

                        const price = parseFloat(p.price) || 0;
                        const open24h = parseFloat(p.open24h) || price;
                        const high24h = parseFloat(p.high24h) || price;
                        const low24h = parseFloat(p.low24h) || price;
                        const vol = parseFloat(p.volume24h) || 0;
                        const pctChg = parseFloat(p.percentChange24h) || 0;
                        const chg24h = price - open24h;

                        priceRows.push({
                            pairId,
                            open: open24h,
                            high: high24h,
                            low: low24h,
                            close: price,
                            volume: vol,
                            change24h: chg24h.toString(),
                            percChange: pctChg.toString(),
                            timestamp: now,
                        });

                        statsRows.push({
                            pairId,
                            currentPrice: price,
                            open24h,
                            high24h,
                            low24h,
                            volume24h: vol,
                            change24h: chg24h,
                            changePercent24h: pctChg,
                            marketCap: parseFloat(p.marketCap) || 0,
                            fullyDilutedValuation: parseFloat(p.fullyDilutedValuation) || 0,
                            circulatingSupply: parseFloat(p.circulatingSupply) || 0,
                            totalSupply: parseFloat(p.totalSupply) || 0,
                            maxSupply: parseFloat(p.maxSupply) || null,
                            bid: price,
                            ask: price,
                            spread: 0,
                        });

                        filledPairIds.add(pairId);
                    });
                } catch (e) {
                    console.warn("CoinMarketCap fetch failed:", e.message);
                }
            }

            // ── Save ────────────────────────────────────────────────────────
            if (priceRows.length) {
                await Price1m.bulkCreate(priceRows);
            }

            if (statsRows.length) {
                // upsert on pairId so we don't create duplicate rows
                await Promise.all(statsRows.map(row => SymbolStats.upsert(row)));
            }

            console.log(`Updated ${priceRows.length} crypto prices at ${now.toISOString()}`);
            return { priceRows, statsRows };
        } catch (err) {
            console.error("Crypto update error:", err.message);
            return { error: err.message };
        }
    }

    // ─────────────────────────────────────────────
    //  24H AGGREGATES  (computed from Price1m)
    // ─────────────────────────────────────────────

    async calculateAll24hAggregates() {
        const pairs = await MarketPair.findAll({});
        await Promise.all(pairs.map(p => this.calculate24hStats(p.id)));
    }

    /**
     * Re-compute 24h stats from the Price1m table and upsert into SymbolStats.
     * This runs after updateCryptoRates to patch in any aggregated fields that
     * the ticker endpoint might not surface (e.g. volumeToMarketCap).
     */
    async calculate24hStats(pairId) {
        const since = new Date(Date.now() - 24 * 60 * 60 * 1000);

        const [aggregate, openCandle, latest] = await Promise.all([
            Price1m.findOne({
                attributes: [
                    [fn("MAX", col("high")), "high24h"],
                    [fn("MIN", col("low")), "low24h"],
                    [fn("SUM", col("volume")), "volume24h"],
                ],
                where: { pairId, timestamp: { [Op.gte]: since } },
                raw: true,
            }),
            Price1m.findOne({
                where: { pairId, timestamp: { [Op.gte]: since } },
                order: [["timestamp", "ASC"]],
                attributes: ["open"],
                raw: true,
            }),
            Price1m.findOne({
                where: { pairId },
                order: [["timestamp", "DESC"]],
                attributes: ["close"],
                raw: true,
            }),
        ]);

        if (!aggregate || !openCandle || !latest) return;

        const open24h = parseFloat(openCandle.open) || 0;
        const currentPrice = parseFloat(latest.close) || 0;
        const high24h = parseFloat(aggregate.high24h) || 0;
        const low24h = parseFloat(aggregate.low24h) || 0;
        const volume24h = parseFloat(aggregate.volume24h) || 0;
        const change24h = currentPrice - open24h;
        const changePercent24h = open24h ? (change24h / open24h) * 100 : 0;

        // Read existing stats so we can preserve marketCap / supply fields
        const existing = await SymbolStats.findOne({ where: { pairId }, raw: true });
        const marketCap = parseFloat(existing?.marketCap) || 0;

        // volumeToMarketCap: what the frontend computes as a ratio
        const volumeToMarketCap = marketCap > 0 ? volume24h / marketCap : 0;

        await SymbolStats.upsert({
            pairId,
            currentPrice,
            high24h,
            low24h,
            open24h,
            volume24h,
            change24h,
            changePercent24h,
            // preserve the fields already written by updateCryptoRates / CMC
            ...(existing?.marketCap != null && { marketCap: existing.marketCap }),
            ...(existing?.fullyDilutedValuation != null && { fullyDilutedValuation: existing.fullyDilutedValuation }),
            ...(existing?.circulatingSupply != null && { circulatingSupply: existing.circulatingSupply }),
            ...(existing?.totalSupply != null && { totalSupply: existing.totalSupply }),
            ...(existing?.maxSupply != null && { maxSupply: existing.maxSupply }),
            volumeToMarketCap,
        });
    }

    // ─────────────────────────────────────────────
    //  CMC HELPERS
    // ─────────────────────────────────────────────

    /**
     * Fetch latest quotes for multiple symbols from CoinMarketCap.
     * Returns a normalised array with all fields the frontend needs.
     */
    async fetchCmcMulti({ symbols, convert = "USD" }) {
        const res = await axios.get(
            "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest",
            {
                params: { symbol: symbols.join(","), convert },
                headers: { "X-CMC_PRO_API_KEY": process.env.COINMARKETCAP_API },
            }
        );

        const normalized = [];
        for (const sym of Object.keys(res.data.data)) {
            // CMC may return an array when symbol is ambiguous; take first entry
            const entries = Array.isArray(res.data.data[sym])
                ? res.data.data[sym]
                : [res.data.data[sym]];

            for (const coin of entries) {
                const q = coin.quote?.[convert] || {};
                normalized.push({
                    symbol: coin.symbol,
                    price: q.price,
                    open24h: q.open24h,
                    high24h: q.high24h,
                    low24h: q.low24h,
                    volume24h: q.volume_24h,
                    percentChange24h: q.percent_change_24h,
                    percentChange7d: q.percent_change_7d,
                    marketCap: q.market_cap,
                    fullyDilutedValuation: q.fully_diluted_market_cap,
                    circulatingSupply: coin.circulating_supply,
                    totalSupply: coin.total_supply,
                    maxSupply: coin.max_supply,
                });
            }
        }
        return { normalized };
    }

    // ─────────────────────────────────────────────
    //  ASSET METADATA (logos, descriptions, etc.)
    // ─────────────────────────────────────────────

    /**
     * Fetch asset metadata (logo, description, slug) from CMC /v2/cryptocurrency/info
     * and persist to the Asset table.
     */
    async updateCryptoData(assets) {
        const slugs = assets.map(s => s.symbol);

        try {
            const res = await axios.get(
                "https://pro-api.coinmarketcap.com/v2/cryptocurrency/info",
                {
                    params: { symbol: slugs.join(",") },
                    headers: { "X-CMC_PRO_API_KEY": process.env.COINMARKETCAP_API },
                }
            );

            if (!res.data?.data) return null;

            // Must be sequential (or Promise.all) — forEach + async is fire-and-forget
            const updates = await Promise.all(
                assets.map(async element => {
                    const coinArray =
                        res.data.data[element.symbol] ||
                        res.data.data[cmcSlugify(element.name)];
                    if (!coinArray?.length) return null;

                    const info = coinArray[0];
                    const patch = {
                        coinMarketId: info.id,
                        description: info.description,
                        slug: info.slug,
                    };

                    await element.update(patch);
                    return { symbol: element.symbol, ...patch };
                })
            );

            return { updates: updates.filter(Boolean), ApiCall: res.data };
        } catch (err) {
            console.error("updateCryptoData error:", err.message);
            return null;
        }
    }

    async updateMarketStats(assets) {
        const symbols = assets.map(a => a.symbol).join(",");

        try {
            const res = await axios.get(
                "https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest",
                {
                    params: { symbol: symbols },
                    headers: {
                        "X-CMC_PRO_API_KEY": process.env.COINMARKETCAP_API,
                    },
                }
            );

            const data = res.data?.data;
            const updatedCoin = [];
            if (!data) return null;

            const results = await Promise.all(
                assets.map(async (asset) => {
                    const coin = data[asset.symbol]?.[0];

                    if (!coin) return null;
                    updatedCoin.push({ "Symbol": asset.symbol });
                    const q = coin.quote?.USD;

                    const patch = {
                        pairId: asset.id,

                        volume24h: q.volume_24h,
                        marketCap: q.market_cap,

                        fullyDilutedValuation:
                            q.price * (coin.max_supply || 0),

                        volumeToMarketCap:
                            q.market_cap ? q.volume_24h / q.market_cap : 0,

                        ath: coin.ath,
                        atl: coin.atl,
                        athDate: coin.ath_date,
                        atlDate: coin.atl_date,
                        athChange: coin.ath_change_percentage,
                        atlChange: coin.atl_change_percentage,

                        changePercent1h: q.percent_change_1h,
                        changePercent7d: q.percent_change_7d,
                        changePercent24h: q.percent_change_24h,

                        cmcRank: coin.cmc_rank,

                        circulatingSupply: coin.circulating_supply,
                        totalSupply: coin.total_supply,
                        maxSupply: coin.max_supply,
                    };

                    // 🔥 UPSERT (insert or update)
                    const existing = await MarketStats.findOne({
                        where: { pairId: asset.id }
                    });

                     if (existing) {
                         await existing.update(patch);
                     } else {
                         await MarketStats.create(patch);
                     }
 
                    return { symbol: asset.symbol, ...patch };
                })
            );

            return {
                coins: updatedCoin,
               /*  updated: results.filter(Boolean),
                api: res.data,
                data: data, */
            };

        } catch (err) {
            logger.error("updateMarketStats error:", err);
            console.error("updateMarketStats error:", err.message);
            return null;
        }
    }

    async updateAthAtl() {
        const assets = await this.getAssets();

        try {
            // Step 1: Get CoinGecko coin list to map symbols → ids
            const { data: coinList } = await axios.get(
                "https://api.coingecko.com/api/v3/coins/list"
            );

            // Build symbol → coingecko id map (lowercase symbol)
            const symbolToId = {};
            for (const coin of coinList) {
                symbolToId[coin.symbol.toLowerCase()] = coin.id;
            }

            // Map our assets to coingecko ids
            const assetIdMap = {}; // coingeckoId → asset
            const geckoIds = [];

            for (const asset of assets) {
                const geckoId = symbolToId[asset.symbol.toLowerCase()];
                if (!geckoId) {
                    console.warn(`No CoinGecko ID found for ${asset.symbol}`);
                    continue;
                }
                assetIdMap[geckoId] = asset;
                geckoIds.push(geckoId);
            }

            if (!geckoIds.length) return null;

            // Step 2: Fetch ATH/ATL from /coins/markets (batched, max 250 per call)
            const BATCH_SIZE = 250;
            const batches = [];
            for (let i = 0; i < geckoIds.length; i += BATCH_SIZE) {
                batches.push(geckoIds.slice(i, i + BATCH_SIZE));
            }

            const results = [];

            for (const batch of batches) {
                const { data } = await axios.get(
                    "https://api.coingecko.com/api/v3/coins/markets",
                    {
                        params: {
                            vs_currency: "usd",
                            ids: batch.join(","),
                            per_page: BATCH_SIZE,
                            page: 1,
                        },
                        // Add key if you have CoinGecko Pro:
                        // headers: { "x-cg-pro-api-key": process.env.COINGECKO_API }
                    }
                );

                await Promise.all(
                    data.map(async (coin) => {
                        const asset = assetIdMap[coin.id];
                        if (!asset) return null;

                        const patch = {
                            ath: coin.ath,
                            atl: coin.atl,
                            athDate: coin.ath_date,
                            atlDate: coin.atl_date,
                            athChange: coin.ath_change_percentage,
                            atlChange: coin.atl_change_percentage,
                        };

                        await MarketStats.upsert({
                            pairId: asset.id,
                            ...patch,
                        });

                        results.push({ symbol: asset.symbol, ...patch });
                    })
                );

                // Free tier rate limit: 30 calls/min
                // Add delay between batches if you have many assets
                if (batches.length > 1) {
                    await new Promise(r => setTimeout(r, 2000));
                }
            }

            return { updated: results };

        } catch (err) {
            console.error("updateAthAtl error:", err.message);
            return null;
        }
    }



    /**
     * Fetch the full CMC listings and upsert rich stats into MarketStats.
     * This is the source of truth for supply, market cap, FDV, P/E etc.
     */
    async updateCryptoListing(assets) {
        const cryptoMap = {};
        assets.forEach(s => { cryptoMap[s.symbol] = s.id; });

        try {
            const res = await axios.get(
                "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest",
                {
                    params: { limit: 5000, convert: "USD" },
                    headers: { "X-CMC_PRO_API_KEY": process.env.COINMARKETCAP_API },
                }
            );

            if (!res.data?.data) return null;

            const mapUpdate = {};

            await Promise.all(
                assets.map(async element => {
                    const coinInfo = res.data.data.find(
                        c => c.symbol === element.symbol || c.slug === element.slug
                    );
                    if (!coinInfo) return;

                    const q = coinInfo.quote?.USD || {};
                    const id = cryptoMap[element.symbol];

                    const updateData = {
                        pairId: id,
                        currentPrice: q.price ?? 0,
                        open24h: q.open24h ?? 0,
                        high24h: q.high24h ?? 0,
                        low24h: q.low24h ?? 0,
                        volume24h: q.volume_24h ?? 0,
                        change24h: q.price_change_24h ?? 0,
                        changePercent24h: q.percent_change_24h ?? 0,
                        marketCap: q.market_cap ?? 0,
                        fullyDilutedValuation: q.fully_diluted_market_cap ?? 0,
                        circulatingSupply: coinInfo.circulating_supply ?? 0,
                        totalSupply: coinInfo.total_supply ?? 0,
                        maxSupply: coinInfo.max_supply ?? null,
                        volumeToMarketCap: (q.market_cap && q.volume_24h)
                            ? q.volume_24h / q.market_cap
                            : 0,
                        // 7d / 30d extras (stored in MarketStats for history pages)
                        percentChange7d: q.percent_change_7d ?? 0,
                        percentChange30d: q.percent_change_30d ?? 0,
                    };

                    await MarketStats.upsert(updateData);

                    // Keep Asset row in sync with CMC id/slug
                    await element.update(
                        { coinMarketId: coinInfo.id, slug: coinInfo.slug },
                        { where: { symbol: element.symbol } }
                    );

                    mapUpdate[element.symbol] = updateData;
                })
            );

            return { mapUpdate, ApiCall: res.data };
        } catch (err) {
            console.error("updateCryptoListing error:", err.message);
            return null;
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
//  HELPERS
// ─────────────────────────────────────────────────────────────────────────────

/** Strip "/" separators and upper-case so "BTC/USDT" → "BTCUSDT" */
function normalizeSymbol(raw = "") {
    return String(raw).replace(/[/_]/g, "").toUpperCase();
}

module.exports = new CryptoService();