import { MainClient, WebsocketClient } from "binance";
import APIClient from "../../apiwrapper/APIClient.js";
import Logger from "../../logging/Logger.js";
import BinanceApiConfig from "./BinanceApiConfig.js";
import BinanceWSEvent from "./BinanceWSEvent.js";
import BalanceChangeEvent from "../../apiwrapper/event/BalanceChangeEvent.js";
import KLineEvent from "../../apiwrapper/event/KLineEvent.js";
import TradingPairs from "../../apiwrapper/assets/TraidingPairs.js";
import Ticker24hEvent from "../../apiwrapper/event/Ticker24hEvent.js";
import OrderBookUpdateEvent from "../../apiwrapper/event/OrderBookUpdateEvent.js";
import OrderBookEntry from "../../apiwrapper/orderbook/OrderBookEntry.js";
import TradingPair from "../../apiwrapper/assets/TradingPair.js";
import MarketType from "../../apiwrapper/market/MarketType.js";
import TradeEvent from "../../apiwrapper/event/TradeEvent.js";
import Transaction from "../../apiwrapper/transaction/Transaction.js";
import TransactionUpdateEvent from "../../apiwrapper/event/TransactionUpdateEvent.js";
import TransactionFill from "../../apiwrapper/transaction/TransactionFill.js";
import TransactionStatus from "../../apiwrapper/transaction/TransactionStatus.js";

export default class BinanceApiClient extends APIClient {
    /**
     * @type {WebsocketClient}
     */
    #wsClient = null;

    /**
     * @type {MainClient}
     */
    #restClient = null;

    /**
     * @param {BinanceApiConfig} config 
     * @param {Logger} logger
     */
    constructor(config, logger) {
        super(config, logger);
        this.#wsClient = new WebsocketClient(config.getWSClientConfig());
        this.#restClient = new MainClient(config.getRestClientConfig());
        this.#wsClient.on('formattedMessage', this.#wsMessageHandler.bind(this));
        this.#wsClient.on('open', this.#connectionOpened.bind(this));
        this.#wsClient.on('reconnecting', this.#reconnected.bind(this));
        this.#wsClient.on('error', this.#handleWSError.bind(this));
    }

    async #connectionOpened(msg) {
        this.getLogger().info("Connection to binance web socket api!", msg);
    }

    async #reconnected(msg) {
        this.getLogger().info("Reconnected to binance web socket api!", msg);
    }

    async #handleWSError(e) {
        if (msg instanceof Error) {
            this.getLogger().error("Error on WS!", msg);
        } else {
            this.getLogger().error("Error on WS!", msg.error.message, msg.error.error);
        }
    }

    #wsMessageHandler(data) {
        if (!data.eventType) {
            this.getLogger().warn("Websocket message is not an event:", data);
            return;
        }

        switch (data.eventType) {
            case BinanceWSEvent.ACCOUNT_BALANCE_CHANGE:
                this.#fireAccountBalanceChangeEvent(data);
                break;
            case BinanceWSEvent.KLINE_EVENT:
                this.#fireKLineEvent(data);
                break;
            case BinanceWSEvent.TICKER_24H_EVENT:
                this.#fire24hTickerEvent(data);
                break;
            case BinanceWSEvent.BOOK_DEPTH_UPDATE_EVENT:
                this.#fireOrderBookUpdateEvent(data);
                break;
            case BinanceWSEvent.TRADE_EVENT:
                this.#fireTradeEvent(data);
                break;
            default:
                this.getLogger().warn("Unknown event type!", data);
        }
    }

    async #fireTradeEvent(data){
        const tradingPair = TradingPairs.fromString(data.symbol);

        if (tradingPair == null) {
            this.getLogger().warn("Trade for unknown symbol: " + data.symbol);
            return;
        }

        const event = new TradeEvent(tradingPair);
        event.price = data.price;
        event.quantity = data.quantity;

        this.dispatchAssetEvent(event);
    }

    async #fireOrderBookUpdateEvent(data) {
        const tradingPair = TradingPairs.fromString(data.symbol);

        if (tradingPair == null) {
            this.getLogger().warn("Order book update for unknown symbol: " + data.symbol);
            return;
        }

        const event = new OrderBookUpdateEvent(tradingPair);
        event.updateId = data.lastUpdateId;
        if(data.bids && data.asks){
            data.bids.forEach(b => event.bids.push(new OrderBookEntry(b[0], b[1])));
            data.asks.forEach(a => event.asks.push(new OrderBookEntry(a[0], a[1])));
            event.isReset = true;
        } else {
            data.bidDepthDelta.forEach(b => event.bids.push(new OrderBookEntry(b.price, b.quantity)));
            data.askDepthDelta .forEach(a => event.asks.push(new OrderBookEntry(a.price, a.quantity)));
        }


        this.dispatchAssetEvent(event);
    }

    async #fire24hTickerEvent(data) {
        const tradingPair = TradingPairs.fromString(data.symbol);

        if (tradingPair == null) {
            this.getLogger().warn("24h ticker for unknown symbol: " + data.symbol);
            return;
        }

        const event = new Ticker24hEvent(tradingPair);
        event.priceChange = data.priceChange;
        event.priceChangePercent = data.priceChangePercent;
        event.weightedAveragePrice = data.weightedAveragePrice;
        event.bestBidPrice = data.bestBid;
        event.bestBidQuantity = data.bestBidQuantity;
        event.bestAskPrice = data.bestAskPrice;
        event.bestAskQuantity = data.bestAskQuantity;
        event.high = data.high;
        event.low = data.low;
        event.trades = data.trades;

        this.dispatchAssetEvent(event);
    }

    async #fireKLineEvent(data) {
        const tradingPair = TradingPairs.fromString(data.symbol);

        if (tradingPair == null) {
            this.getLogger().warn("Kline for unknown symbol: " + data.symbol);
            return;
        }

        const event = new KLineEvent(tradingPair, data.kline.high, data.kline.low, data.kline.open, data.kline.close);
        event.isClosed = data.kline.final;
        event.startTime = data.kline.startTime;
        event.endTime = data.kline.endTime;

        this.dispatchAssetEvent(event);
    }

    /**
     * @param {any} data 
     */
    async #fireAccountBalanceChangeEvent(data) {
        const event = new BalanceChangeEvent();
        data.balances.forEach(balanceUpdate => {
            event.setChangedBalance(balanceUpdate.asset, balanceUpdate.availableBalance);
        });

        this.dispatchAccountEvent(event);
    }

    /**
     * @param {TradingPair} tradingPair 
     */
    subscribeOrderBookUpdates(tradingPair) {
        this.requestOrderBook(tradingPair);
        this.#wsClient.subscribeDiffBookDepth(tradingPair.getKey(), 100, MarketType.SPOT)
    }

    /**
     * @param {TradingPair} tradingPair 
     */
    subscribeKLines(tradingPair) {
        this.#wsClient.subscribeKlines(tradingPair.getKey(), "1m", MarketType.SPOT);
    }

    /**
     * @param {TradingPair} tradingPair 
     */
    subscribe24hTicker(tradingPair) {
        this.#wsClient.subscribeSymbol24hrTicker(tradingPair.getKey(), MarketType.SPOT);
    }

    /**
     * @param {TradingPair} tradingPair
     */
    subscribeTrades(tradingPair) {
        this.#wsClient.subscribeTrades(tradingPair.getKey(), MarketType.SPOT);
    }

    subscribeBalanceChanges() {
        this.#wsClient.subscribeSpotUserDataStream();
    }

    /**
     * @param {TradingPair} tradingPair 
     */
    requestOrderBook(tradingPair) {
        this.#restClient.getOrderBook({ "symbol": tradingPair.getKey(), "limit": 10 })
            .then(resp => {
                resp.symbol = tradingPair.getKey();
                this.#fireOrderBookUpdateEvent(resp);
            })
            .catch(e => {
                this.getLogger().error("Unable to get order book information from binance rest api!", e);
            });
    }

    requestBalances() {
        if (this.getApiConfig().isTestNet()) {
            return;
        }

        this.#restClient.getBalances().then(resp => {
            const event = new BalanceChangeEvent();
            resp.forEach(coinInfo => {
                if (coinInfo.free > 0) {
                    event.setChangedBalance(coinInfo.coin, coinInfo.free);
                }
            });

            this.dispatchAccountEvent(event);
        }).catch(e => {
            this.getLogger().error("Unable to get balances from binance rest api!", e);
        });
    }

    requestAccountInformation() {
        this.#restClient.getAccountInformation().then(resp => {
            // TODO
        }).catch(e => {
            this.getLogger().error("Unable to get Account information from binance rest api!", e);
        });
    }

    /**
     * @param {Transaction} transaction 
     */
    requestBuy(transaction){
        this.#restClient.submitNewOrder({
            "symbol": transaction.symbol.getKey(),
            "side": "BUY", 
            "type": "LIMIT", 
            "price": transaction.buySettings.oderPrice, 
            "quantity" : transaction.buySettings.orderQuantity, 
            "newClientOrderId": transaction.id, 
            "timeInForce": "GTC",
            "timestamp": Date.now()
        }).then(resp => {
            transaction.buySettings.apiID = resp.orderId;
        }).catch(e => {
            this.getLogger().error("Unable to create buy order for " + transaction.id, {code: e.code, msg: e.message, url: e.requestUrl});
            const event = new TransactionUpdateEvent(transaction.id, transaction.buySettings.apiID, TransactionStatus.CANCELED)
            this.dispatchAccountEvent(event);
        });
    }

    /**
     * @param {Transaction} transaction 
     */
    requestSell(transaction){
        this.#restClient.submitNewOrder({
            "symbol": transaction.symbol.getKey(),
            "side": "SELL", 
            "type": "LIMIT", 
            "price": transaction.sellSettings.oderPrice, 
            "quantity" : transaction.sellSettings.orderQuantity, 
            "newClientOrderId": transaction.id, 
            "timeInForce": "GTC",
            "timestamp": Date.now()
        }).then(resp => {
            transaction.sellSettings.apiID = resp.orderId;
        }).catch(e => {
            this.getLogger().error("Unable to create sell order for " + transaction.id, {code: e.code, msg: e.message, url: e.requestUrl});
            const event = new TransactionUpdateEvent(transaction.id, transaction.sellSettings.apiID, TransactionStatus.CANCELED)
            this.dispatchAccountEvent(event);
        });
    }

    /**
     * @param {Transaction} transaction 
     */
    requestBuyCancel(transaction){
        this.#restClient.cancelOrder({
            "symbol": transaction.symbol.getKey(),
            "orderId": transaction.buySettings.apiID,
            "origClientOrderId": transaction.id
        }).then(resp =>{
            this.requestTransactionUpdate(transaction, "buy");
        }).catch(e => {
            this.getLogger().error("Unable to cancle buy order for " + transaction.id, {code: e.code, msg: e.message, url: e.requestUrl});
        });
    }

    /**
     * @param {Transaction} transaction 
     */
    requestSellCancel(transaction){
        this.#restClient.cancelOrder({
            "symbol": transaction.symbol.getKey(),
            "orderId": transaction.sellSettings.apiID,
            "origClientOrderId": transaction.id
        }).then(resp =>{
            this.requestTransactionUpdate(transaction, "sell");
        }).catch(e => {
            this.getLogger().error("Unable to cancle sell order for " + transaction.id, {code: e.code, msg: e.message, url: e.requestUrl});
        });
    }

    /**
     * @param {Transaction} transaction 
     * @param {"buy" | "sell"} type
     */
    requestTransactionUpdate(transaction, type = "buy"){
        const id = type == "buy" ? transaction.buySettings.apiID : transaction.sellSettings.apiID;

        this.#restClient.getOrder({
            "symbol": transaction.symbol.getKey(),
            "orderId": id,
            "origClientOrderId": transaction.id
        }).then(resp => {
            const event = new TransactionUpdateEvent(resp.clientOrderId, resp.orderId, resp.status, resp.updateTime);
            this.#requestTransactionFills(transaction, type, event);
        }).catch(e => {
            this.getLogger().error("Unable to get order status for " + transaction.id, {code: e.code, msg: e.message, url: e.requestUrl});
        });
    }

    /**
     * @param {Transaction} transaction 
     * @param {"buy" | "sell"} type
     * @param {TransactionUpdateEvent} event
     */
    #requestTransactionFills(transaction, type, event){
        const id = type == "buy" ? transaction.buySettings.apiID : transaction.sellSettings.apiID;

        this.#restClient.getAccountTradeList({
            "symbol": transaction.symbol.getKey(),
            "orderId": id,
        }).then(resp => {
            resp.forEach(trade => event.fills.push(new TransactionFill(trade.price, trade.qty)));
            this.dispatchAccountEvent(event);
        }).catch(e => {
            this.getLogger().error("Unable to get order fills for " + transaction.id, {code: e.code, msg: e.message, url: e.requestUrl});
        });
    }
}