const { Op, fn, col, literal } = require("sequelize");
const {
  sequelize,
  SpotOrder,
  SpotTrade,
  Wallet,
  MarketPair,
  Asset,
  SymbolStats,
} = require("../models");

const {
  ValidationError,
  ConflictError,
  NotFoundError,
} = require("../utils/errors");
const { baseUrl } = require("../utils/helpers");

class SpotService {
  normalizeOrderType(orderType) {
    if (orderType === "stop-limit") return "stop_limit";
    return orderType;
  }

  denormalizeOrderType(type) {
    if (type === "stop_limit") return "stop-limit";
    return type;
  }

  toNum(v, fallback = 0) {
    const n = Number(v);
    return Number.isFinite(n) ? n : fallback;
  }

  round(v, dp = 12) {
    return Number(this.toNum(v).toFixed(dp));
  }

  pagination(query) {
    const page = Math.max(1, Number(query.page || 1));
    const limit = Math.max(1, Math.min(200, Number(query.limit || 20)));
    const offset = (page - 1) * limit;
    return { page, limit, offset };
  }

  async getSpotData(userId) {
    const [pairs, wallets] = await Promise.all([
  MarketPair.findAll({
    where: { type: "spot", status: "active" },
    include: [
      { model: Asset, as: "base" },
      { model: Asset, as: "quote" },
      { model: SymbolStats, as: "rate" },
    ],
  }),
  Wallet.findAll({
    where: { userId },
    include: [{ model: Asset, as: "coin" }],
  }),
]);

const walletMap = {};

wallets.forEach((w) => {
  const symbol = w.coin?.symbol;
  if (!symbol) return;

  walletMap[symbol] =
    Number(w.balance ?? 0) - Number(w.lockedBalance ?? 0);
});

return {
  pairs: pairs
    .map((p) => {
      const baseAsset = p?.base;
      const quoteAsset = p?.quote;
      const baseLogo = `${baseUrl}/uploads/assets/${baseAsset.logo}`
      const quoteLogo = `${baseUrl}/uploads/assets/${quoteAsset.logo}`
      if (!baseAsset || !quoteAsset) return null;
      if(baseAsset.symbol === quoteAsset.symbol) return null
      if (
        !(baseAsset.symbol in walletMap) ||
        !(quoteAsset.symbol in walletMap)
      ) {
        return null;
      }
   
      return {
        pair: p.name,
        symbol: p.symbol,
        base: baseAsset.symbol,
        quote: quoteAsset.symbol,
        baseName: baseAsset.name,
        quoteName: quoteAsset.name,
        baseLogo: baseLogo,
        quoteLogo: quoteLogo,
        price: Number(p.rate?.current_price ?? 0),
        low: Number(p.rate?.low24h ?? 0),
        high: Number(p.rate?.high24h ?? 0),
        volume: Number(p.rate?.volume24h ?? 0),
        change24h: Number(p.rate?.change_percent24h ?? 0),
      };
    })
    .filter(Boolean),
  wallets: walletMap,
};
  }

  async getMarketOrFail(symbol, transaction) {
    const market = await MarketPair.findOne({
      where: { symbol, status: "active" },
      transaction,
      lock: transaction ? transaction.LOCK.SHARE : undefined,
    });

    if (!market) throw new NotFoundError(`${symbol} market not found`);

    if (!market.baseAssetId || !market.quoteAssetId) {
      throw new ConflictError("Market is missing baseAssetId or quoteAssetId");
    }

    return market;
  }

  async getWalletOrFail(userId, coinId, symbol, transaction, lock = true) {
    const wallet = await Wallet.findOne({
      where: { userId, coinId, status: true },
      transaction,
      lock: lock && transaction ? transaction.LOCK.UPDATE : undefined,
    });

    if (!wallet) {
      throw new ConflictError(`Wallet not found for asset ${symbol}`);
    }

    return wallet;
  }

  getAvailable(wallet) {
    return this.toNum(wallet.balance) - this.toNum(wallet.lockedBalance);
  }

  async lockFundsForOrder({ userId,quotes, market, side, qty, price, transaction }) {
    
    const baseWallet = await this.getWalletOrFail(
      userId,
      market.baseAssetId,
      quotes?.base,
      transaction
    );
   
    const quoteWallet = await this.getWalletOrFail(
      userId,
      market.quoteAssetId,
      quotes?.quote,
      transaction
    );

    
    if (side === "buy") {
      if (!price || price <= 0) {
        throw new ValidationError("Price required to lock buy funds");
      }

      const requiredQuote = qty * price;
      const availableQuote = this.getAvailable(quoteWallet);

      if (availableQuote < requiredQuote) {
        throw new ValidationError("Insufficient quote wallet balance");
      }

      await quoteWallet.update(
        {
          lockedBalance: this.toNum(quoteWallet.lockedBalance) + requiredQuote,
        },
        { transaction }
      );

      return {
        lockedAsset: "quote",
        lockedAmount: requiredQuote,
        baseWallet,
        quoteWallet,
      };
    }

    if (side === "sell") {
      const availableBase = this.getAvailable(baseWallet);

      if (availableBase < qty) {
        throw new ValidationError("Insufficient base wallet balance");
      }

      await baseWallet.update(
        {
          lockedBalance: this.toNum(baseWallet.lockedBalance) + qty,
        },
        { transaction }
      );

      return {
        lockedAsset: "base",
        lockedAmount: qty,
        baseWallet,
        quoteWallet,
      };
    }

    throw new ValidationError("Invalid side");
  }

  async unlockRemainingForOrder({ order, market, transaction }) {
    const remainingQty = Math.max(
      0,
      this.toNum(order.qty) - this.toNum(order.qty_filled)
    );

    if (remainingQty <= 0) return;

    if (order.side === "buy") {
      const quoteWallet = await this.getWalletOrFail(
        order.user_id,
        market.quoteAssetId,
        transaction
      );

      const unlockAmount = remainingQty * this.toNum(order.price);

      await quoteWallet.update(
        {
          lockedBalance: Math.max(
            0,
            this.toNum(quoteWallet.lockedBalance) - unlockAmount
          ),
        },
        { transaction }
      );
    } else {
      const baseWallet = await this.getWalletOrFail(
        order.user_id,
        market.baseAssetId,
        transaction
      );

      await baseWallet.update(
        {
          lockedBalance: Math.max(
            0,
            this.toNum(baseWallet.lockedBalance) - remainingQty
          ),
        },
        { transaction }
      );
    }
  }

  async settleMatch({
    takerOrder,
    makerOrder,
    market,
    matchPrice,
    matchQty,
    transaction,
  }) {
    const quoteValue = matchPrice * matchQty;

    const takerBaseWallet = await this.getWalletOrFail(
      takerOrder.user_id,
      market.baseAssetId,
      transaction
    );
    const takerQuoteWallet = await this.getWalletOrFail(
      takerOrder.user_id,
      market.quoteAssetId,
      transaction
    );
    const makerBaseWallet = await this.getWalletOrFail(
      makerOrder.user_id,
      market.baseAssetId,
      transaction
    );
    const makerQuoteWallet = await this.getWalletOrFail(
      makerOrder.user_id,
      market.quoteAssetId,
      transaction
    );

    const buyerOrder = takerOrder.side === "buy" ? takerOrder : makerOrder;
    const sellerOrder = takerOrder.side === "sell" ? takerOrder : makerOrder;

    const buyerBaseWallet =
      buyerOrder.user_id === takerOrder.user_id ? takerBaseWallet : makerBaseWallet;
    const buyerQuoteWallet =
      buyerOrder.user_id === takerOrder.user_id ? takerQuoteWallet : makerQuoteWallet;

    const sellerBaseWallet =
      sellerOrder.user_id === takerOrder.user_id ? takerBaseWallet : makerBaseWallet;
    const sellerQuoteWallet =
      sellerOrder.user_id === takerOrder.user_id ? takerQuoteWallet : makerQuoteWallet;

    if (buyerOrder.side !== "buy") {
      throw new ConflictError("Buyer order side mismatch");
    }

    if (sellerOrder.side !== "sell") {
      throw new ConflictError("Seller order side mismatch");
    }

    await buyerQuoteWallet.update(
      {
        balance: this.toNum(buyerQuoteWallet.balance) - quoteValue,
        lockedBalance: Math.max(
          0,
          this.toNum(buyerQuoteWallet.lockedBalance) - quoteValue
        ),
      },
      { transaction }
    );

    await buyerBaseWallet.update(
      {
        balance: this.toNum(buyerBaseWallet.balance) + matchQty,
      },
      { transaction }
    );

    await sellerBaseWallet.update(
      {
        balance: this.toNum(sellerBaseWallet.balance) - matchQty,
        lockedBalance: Math.max(
          0,
          this.toNum(sellerBaseWallet.lockedBalance) - matchQty
        ),
      },
      { transaction }
    );

    await sellerQuoteWallet.update(
      {
        balance: this.toNum(sellerQuoteWallet.balance) + quoteValue,
      },
      { transaction }
    );

    const takerFilled = this.toNum(takerOrder.qty_filled) + matchQty;
    const makerFilled = this.toNum(makerOrder.qty_filled) + matchQty;

    const takerStatus =
      takerFilled >= this.toNum(takerOrder.qty) ? "filled" : "partially_filled";
    const makerStatus =
      makerFilled >= this.toNum(makerOrder.qty) ? "filled" : "partially_filled";

    await takerOrder.update(
      {
        qty_filled: takerFilled,
        status: takerStatus,
      },
      { transaction }
    );

    await makerOrder.update(
      {
        qty_filled: makerFilled,
        status: makerStatus,
      },
      { transaction }
    );

    const trade = await SpotTrade.create(
      {
        market_id: market.id,
        maker_order_id: makerOrder.id,
        taker_order_id: takerOrder.id,
        price: matchPrice,
        qty: matchQty,
        maker_user_id: makerOrder.user_id,
        taker_user_id: takerOrder.user_id,
        fee_asset_id: null,
        maker_fee: 0,
        taker_fee: 0,
      },
      { transaction }
    );

    return trade;
  }

  canOrdersMatch(takerOrder, makerOrder) {
    if (takerOrder.market_id !== makerOrder.market_id) return false;
    if (takerOrder.side === makerOrder.side) return false;
    if (!["new", "partially_filled"].includes(makerOrder.status)) return false;
    if (takerOrder.user_id === makerOrder.user_id) return false;

    const takerType = this.denormalizeOrderType(takerOrder.type);
    const makerType = this.denormalizeOrderType(makerOrder.type);

    if (!["limit", "market"].includes(takerType)) return false;
    if (!["limit"].includes(makerType)) return false;

    if (takerType === "market") return true;

    const takerPrice = this.toNum(takerOrder.price);
    const makerPrice = this.toNum(makerOrder.price);

    if (takerOrder.side === "buy") {
      return takerPrice >= makerPrice;
    }

    return takerPrice <= makerPrice;
  }

  async findMatchingOrders(takerOrder, transaction) {
    const oppositeSide = takerOrder.side === "buy" ? "sell" : "buy";

    const where = {
      market_id: takerOrder.market_id,
      side: oppositeSide,
      status: { [Op.in]: ["new", "partially_filled"] },
      type: "limit",
      user_id: { [Op.ne]: takerOrder.user_id },
    };

    const orderBy =
      takerOrder.side === "buy"
        ? [["price", "ASC"], ["created_at", "ASC"]]
        : [["price", "DESC"], ["created_at", "ASC"]];

    return SpotOrder.findAll({
      where,
      transaction,
      lock: transaction.LOCK.UPDATE,
      order: orderBy,
    });
  }

  async matchOrder(takerOrder, market, transaction) {
    const trades = [];
    const bookOrders = await this.findMatchingOrders(takerOrder, transaction);

    for (const makerOrder of bookOrders) {
      const refreshedTaker = await SpotOrder.findByPk(takerOrder.id, {
        transaction,
        lock: transaction.LOCK.UPDATE,
      });

      if (!refreshedTaker) break;
      if (refreshedTaker.status === "filled") break;

      if (!this.canOrdersMatch(refreshedTaker, makerOrder)) continue;

      const makerFresh = await SpotOrder.findByPk(makerOrder.id, {
        transaction,
        lock: transaction.LOCK.UPDATE,
      });

      if (!makerFresh) continue;
      if (!["new", "partially_filled"].includes(makerFresh.status)) continue;

      const takerRemaining =
        this.toNum(refreshedTaker.qty) - this.toNum(refreshedTaker.qty_filled);
      const makerRemaining =
        this.toNum(makerFresh.qty) - this.toNum(makerFresh.qty_filled);

      if (takerRemaining <= 0 || makerRemaining <= 0) continue;

      const matchQty = Math.min(takerRemaining, makerRemaining);
      const matchPrice = this.toNum(makerFresh.price);

      const trade = await this.settleMatch({
        takerOrder: refreshedTaker,
        makerOrder: makerFresh,
        market,
        matchPrice,
        matchQty,
        transaction,
      });

      trades.push(trade);
    }

    const finalOrder = await SpotOrder.findByPk(takerOrder.id, {
      transaction,
      lock: transaction.LOCK.UPDATE,
    });

    if (!finalOrder) {
      throw new NotFoundError("Order not found after matching");
    }

    return { order: finalOrder, trades };
  }

  async activateTriggeredStopOrders(marketId, lastTradePrice, transaction) {
    const stopOrders = await SpotOrder.findAll({
      where: {
        market_id: marketId,
        type: "stop_limit",
        status: "new",
        stop_price: { [Op.ne]: null },
      },
      transaction,
      lock: transaction.LOCK.UPDATE,
    });

    for (const order of stopOrders) {
      const stopPrice = this.toNum(order.stop_price);

      const shouldTrigger =
        (order.side === "buy" && lastTradePrice >= stopPrice) ||
        (order.side === "sell" && lastTradePrice <= stopPrice);

      if (!shouldTrigger) continue;

      await order.update(
        {
          type: "limit",
          stop_price: null,
        },
        { transaction }
      );
    }
  }

  async releaseBuyExcessReservation({ order, trades, market, userId, transaction }) {
    if (order.side !== "buy") return;

    const refreshed = await SpotOrder.findByPk(order.id, {
      transaction,
      lock: transaction.LOCK.UPDATE,
    });

    if (!refreshed) return;

    const filledQty = this.toNum(refreshed.qty_filled);
    const orderQty = this.toNum(refreshed.qty);

    if (filledQty < 0 || orderQty < 0) return;

    const reservedTotal = this.toNum(order.price) * orderQty;
    const actualSpent = trades.reduce(
      (sum, tr) => sum + this.toNum(tr.price) * this.toNum(tr.qty),
      0
    );
    const remainingQty = orderQty - filledQty;
    const stillReservedForOpen = remainingQty * this.toNum(order.price);
    const shouldRemainLocked = actualSpent + stillReservedForOpen;
    const excess = reservedTotal - shouldRemainLocked;

    if (excess <= 0) return;

    const quoteWallet = await this.getWalletOrFail(
      userId,
      market.quoteAssetId,
      transaction
    );

    await quoteWallet.update(
      {
        lockedBalance: Math.max(
          0,
          this.toNum(quoteWallet.lockedBalance) - excess
        ),
      },
      { transaction }
    );
  }

  async tradeSpot(userId, data) {
    const symbol = String(data.symbol || "").trim();
    const side = String(data.side || "").trim().toLowerCase();
    const orderType = String(data.orderType || "").trim().toLowerCase();
    const qty = this.toNum(data.amount);
    const price = data.price == null || data.price === "" ? null : this.toNum(data.price);
    const stopPrice =
      data.stopPrice == null || data.stopPrice === ""
        ? null
        : this.toNum(data.stopPrice);
    const clientOrderId = data.clientOrderId || null;

    if (!symbol) throw new ValidationError("Symbol is required");
    if (!["buy", "sell"].includes(side)) throw new ValidationError("Invalid side");
    if (!["market", "limit", "stop-limit"].includes(orderType)) {
      throw new ValidationError("Invalid order type");
    }
    if (!qty || qty <= 0) throw new ValidationError("Invalid amount");

    if ((orderType === "limit" || orderType === "stop-limit") && (!price || price <= 0)) {
      throw new ValidationError("Price is required");
    }

    if (orderType === "stop-limit" && (!stopPrice || stopPrice <= 0)) {
      throw new ValidationError("Stop price is required");
    }

    const q =  symbol.split("_");
    const quotes = {
      base: q[0],
      quote: q[1]
    }

    const t = await sequelize.transaction();
    
    try {
      const market = await this.getMarketOrFail(symbol, t);

      let lockPrice = price;

      if (orderType === "market") {
        const marketRefPrice = this.toNum(market.price || market.lastPrice || price);
        if (!marketRefPrice || marketRefPrice <= 0) {
          throw new ValidationError("Market price unavailable");
        }
        lockPrice = marketRefPrice;
      }
      
      
      await this.lockFundsForOrder({
        userId,
        quotes,
        market,
        side,
        qty,
        price: lockPrice,
        transaction: t,
      });

      

      const order = await SpotOrder.create(
        {
          user_id: userId,
          market_id: market.id,
          side,
          type: this.normalizeOrderType(orderType),
          price: orderType === "market" ? lockPrice : price,
          stop_price: orderType === "stop-limit" ? stopPrice : null,
          qty,
          qty_filled: 0,
          status: "new",
          client_order_id: clientOrderId,
        },
        { transaction: t }
      );

      

      let resultOrder = order;
      let trades = [];

      if (orderType === "market" || orderType === "limit") {
        const matched = await this.matchOrder(order, market, t);
        resultOrder = matched.order;
        trades = matched.trades;
      }

      if (orderType === "market" && this.toNum(resultOrder.qty_filled) <= 0) {
        await this.unlockRemainingForOrder({
          order: resultOrder,
          market,
          transaction: t,
        });

        await resultOrder.update({ status: "canceled" }, { transaction: t });

        await t.commit();

        return {
          order: resultOrder,
          trades: [],
          message: "No liquidity available for market order",
        };
      }

      await this.releaseBuyExcessReservation({
        order,
        trades,
        market,
        userId,
        transaction: t,
      });

      if (trades.length > 0) {
        const lastTrade = trades[trades.length - 1];
        await this.activateTriggeredStopOrders(
          market.id,
          this.toNum(lastTrade.price),
          t
        );

        // optional: update last traded price on market if your model has it
        if (Object.prototype.hasOwnProperty.call(market.dataValues, "lastPrice")) {
          await market.update(
            { lastPrice: this.toNum(lastTrade.price) },
            { transaction: t }
          );
        } else if (Object.prototype.hasOwnProperty.call(market.dataValues, "price")) {
          await market.update(
            { price: this.toNum(lastTrade.price) },
            { transaction: t }
          );
        }
      }

      await t.commit();

      return {
        order: resultOrder,
        trades,
        message:
          this.toNum(resultOrder.qty_filled) > 0
            ? "Order placed successfully"
            : "Order created and added to book",
      };
    } catch (e) {
      await t.rollback();
      throw e;
    }
  }

  async cancelSpotOrder(userId, orderId) {
    const t = await sequelize.transaction();

    try {
      const order = await SpotOrder.findOne({
        where: {
          id: orderId,
          user_id: userId,
        },
        transaction: t,
        lock: t.LOCK.UPDATE,
      });

      if (!order) throw new NotFoundError("Order not found");

      if (!["new", "partially_filled"].includes(order.status)) {
        throw new ValidationError("Only open orders can be cancelled");
      }

      const market = await MarketPair.findByPk(order.market_id, {
        transaction: t,
        lock: t.LOCK.SHARE,
      });

      if (!market) throw new NotFoundError("Market not found");

      await this.unlockRemainingForOrder({
        order,
        market,
        transaction: t,
      });

      await order.update({ status: "canceled" }, { transaction: t });

      await t.commit();

      return {
        order,
        message: "Order cancelled",
      };
    } catch (e) {
      await t.rollback();
      throw e;
    }
  }

  async getOpenSpotOrders(userId, query) {
    const { page, limit, offset } = this.pagination(query);

    let marketId = null;
    if (query.symbol) {
      const market = await this.getMarketOrFail(String(query.symbol).trim(), null);
      marketId = market.id;
    }

    const where = {
      user_id: userId,
      status: { [Op.in]: ["new", "partially_filled"] },
    };

    if (marketId) where.market_id = marketId;
    if (query.side) where.side = query.side;

    const { rows, count } = await SpotOrder.findAndCountAll({
      where,
      include: [
        {
          model: MarketPair,
          as: "market",
          attributes: ["id", "symbol"],
          required: false,
        },
      ],
      order: [["created_at", "DESC"]],
      limit,
      offset,
    });

    return {
      items: rows,
      pagination: {
        page,
        limit,
        total: count,
        pages: Math.ceil(count / limit),
      },
    };
  }

  async getSpotOrderHistory(userId, query) {
    const { page, limit, offset } = this.pagination(query);

    let marketId = null;
    if (query.symbol) {
      const market = await this.getMarketOrFail(String(query.symbol).trim(), null);
      marketId = market.id;
    }

    const where = {
      user_id: userId,
    };

    if (marketId) where.market_id = marketId;
    if (query.side) where.side = query.side;
    if (query.status) {
      where.status = query.status;
    } else {
      where.status = {
        [Op.in]: ["filled", "canceled", "rejected", "partially_filled", "new"],
      };
    }

    const { rows, count } = await SpotOrder.findAndCountAll({
      where,
      include: [
        {
          model: MarketPair,
          as: "market",
          attributes: ["id", "symbol"],
          required: false,
        },
      ],
      order: [["created_at", "DESC"]],
      limit,
      offset,
    });

    return {
      items: rows,
      pagination: {
        page,
        limit,
        total: count,
        pages: Math.ceil(count / limit),
      },
    };
  }

  async getSpotTrades(userId, query) {
    const { page, limit, offset } = this.pagination(query);

    let marketId = null;
    if (query.symbol) {
      const market = await this.getMarketOrFail(String(query.symbol).trim(), null);
      marketId = market.id;
    }

    const role = query.role || "any";

    const where = {};
    if (marketId) where.market_id = marketId;

    if (role === "maker") {
      where.maker_user_id = userId;
    } else if (role === "taker") {
      where.taker_user_id = userId;
    } else {
      where[Op.or] = [{ maker_user_id: userId }, { taker_user_id: userId }];
    }

    const { rows, count } = await SpotTrade.findAndCountAll({
      where,
      include: [
        {
          model: MarketPair,
          as: "market",
          attributes: ["id", "symbol"],
          required: false,
        },
      ],
      order: [["created_at", "DESC"]],
      limit,
      offset,
    });

    const items = rows.map((row) => {
      const json = row.toJSON();
      let roleLabel = "maker";

      if (json.taker_user_id === userId && json.maker_user_id === userId) {
        roleLabel = "maker_taker";
      } else if (json.taker_user_id === userId) {
        roleLabel = "taker";
      }

      return {
        ...json,
        role: roleLabel,
      };
    });

    return {
      items,
      pagination: {
        page,
        limit,
        total: count,
        pages: Math.ceil(count / limit),
      },
    };
  }

  async getSpotOrderBook(query) {
    const symbol = String(query.symbol || "").trim();
    const depth = Math.max(1, Math.min(500, Number(query.depth || 50)));

    if (!symbol) throw new ValidationError("Symbol is required");

    const market = await this.getMarketOrFail(symbol, null);

    const buyOrders = await SpotOrder.findAll({
      where: {
        market_id: market.id,
        side: "buy",
        type: "limit",
        status: { [Op.in]: ["new", "partially_filled"] },
      },
      attributes: [
        "price",
        [fn("SUM", literal("qty - qty_filled")), "qty"],
      ],
      group: ["price"],
      order: [["price", "DESC"]],
      limit: depth,
      raw: true,
    });

    const sellOrders = await SpotOrder.findAll({
      where: {
        market_id: market.id,
        side: "sell",
        type: "limit",
        status: { [Op.in]: ["new", "partially_filled"] },
      },
      attributes: [
        "price",
        [fn("SUM", literal("qty - qty_filled")), "qty"],
      ],
      group: ["price"],
      order: [["price", "ASC"]],
      limit: depth,
      raw: true,
    });

    const bids = buyOrders.map((o) => ({
      price: this.toNum(o.price),
      qty: this.toNum(o.qty),
      total: this.round(this.toNum(o.price) * this.toNum(o.qty)),
    }));

    const asks = sellOrders.map((o) => ({
      price: this.toNum(o.price),
      qty: this.toNum(o.qty),
      total: this.round(this.toNum(o.price) * this.toNum(o.qty)),
    }));

    const bestBid = bids.length ? bids[0].price : null;
    const bestAsk = asks.length ? asks[0].price : null;
    const spread =
      bestBid != null && bestAsk != null ? this.round(bestAsk - bestBid) : null;

    const recentTrades = await SpotTrade.findAll({
      where: { market_id: market.id },
      order: [["created_at", "DESC"]],
      limit: 20,
      raw: true,
    });

    return {
      market: {
        id: market.id,
        symbol: market.symbol,
      },
      bids,
      asks,
      stats: {
        bestBid,
        bestAsk,
        spread,
        lastTradePrice: recentTrades.length ? this.toNum(recentTrades[0].price) : null,
      },
      recentTrades: recentTrades.map((t) => ({
        id: t.id,
        price: this.toNum(t.price),
        qty: this.toNum(t.qty),
        createdAt: t.created_at,
      })),
    };
  }
}

module.exports = new SpotService();
