import BinanceApiClient from "../apis/binance/BinanceApiClient.js";
import APIClient from "../apiwrapper/APIClient.js";
import TradingPair from "../apiwrapper/assets/TradingPair.js";
import APIEvent from "../apiwrapper/event/APIEvent.js";
import APIEventType from "../apiwrapper/event/APIEventType.js";
import TransactionUpdateEvent from "../apiwrapper/event/TransactionUpdateEvent.js";
import Transaction from "../apiwrapper/transaction/Transaction.js";
import TransactionFill from "../apiwrapper/transaction/TransactionFill.js";
import TransactionPhase from "../apiwrapper/transaction/TransactionPhase.js";
import TransactionSettings from "../apiwrapper/transaction/TransactionSettings.js";
import Logger from "../logging/Logger.js";
import SerializableHelper from "../util/SerializableHelper.js";
import RestClient from "../webview/RestClient.js";
import WebViewDataProvider from "../webview/WebViewDataProvider.js";
import AssetDataCollector from "./AssetDataCollector.js";
import BotConfig from "./config/BotConfig.js";
import AccountInfo from "./data/account/AccountInfo.js";
import TradingBotRestDataProvider from "./TradingBotRestDataProvider.js";
import UnitHelper from "./util/UnitHelper.js";
import AbstractWorker from "./worker/AbstractWorker.js";

export default class TradingBot {
    /**
     * @type {BotConfig}
     */
    #config = null;

    /**
     * @type {Logger}
     */
    #logger = null;

    /**
     * @type {APIClient}
     */
    #api = null;

    /**
     * @type {AssetDataCollector}
     */
    #dataCollector = null;

    /**
     * @type {Transaction[]}
     */
    #transactions = [];

    /**
     * @type {Transaction[]}
     */
    #transactionHistory = [];

    /**
     * @type {AccountInfo}
     */
    #accountInfo = new AccountInfo();

    /**
     * @type {Map<string, AbstractWorker>}
     */
    #worker = new Map();

    /**
     * @type {WebViewDataProvider}
     */
    #restInterfaceBinding = null;

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

    /**
     * @type {Map<string, number>}
     */
    #orderBookUpdateTime = new Map();

    /**
     * @type {boolean}
     */
    #isRunning = false;

    /**
     * @type {boolean}
     */
    #isTradingStarted = false;

    /**
     * @type {boolean}
     */
    #isRampupTimeOver = false;

    /**
     * @type {number} time, when bot instance was started
     */
    #startTime = Number.MAX_VALUE;

    constructor(configPath, configName) {
        this.#config = SerializableHelper.load(configPath, configName, BotConfig);
        this.#logger = new Logger(this.#config.getLogDirectory());
        this.#dataCollector = new AssetDataCollector(this.#logger, this.#config);
        this.#api = new BinanceApiClient(this.#config.getApiConfig(), this.#logger);
        if(this.#config.isRestInterfaceEnabled()){
            this.#restInterfaceBinding = new TradingBotRestDataProvider(this);
            this.#restClient = new RestClient(this.#config.getRestInterfacePort());
            this.#restClient.start(this.#restInterfaceBinding);
        }
    }

    start() {
        if(this.#isRunning){
            this.#isTradingStarted = true;
            return;
        }

        this.#dataCollector.init();
        this.#api.registerAccountEventListener(this);
        this.#api.registerAssetEventListener(this);
        this.#api.subscribeBalanceChanges();
        this.#api.requestBalances();
        this.#api.requestAccountInformation();
        this.#loadTransactions();
        this.#loadWorker();
        this.#worker.forEach(worker => {
            this.#registerTradingPair(worker.getTradingPair());
            worker.init(this.#logger, this.#config.getDataDirectory());
        });
        this.#transactions.forEach(transaction => {
            const type = transaction.phase === (TransactionPhase.INITIAL || transaction.phase === TransactionPhase.BUY_IN_PROGRESS) ? "buy" : "sell";
            this.#api.requestTransactionUpdate(transaction, type);
        });

        this.#startTime = Date.now();
        this.#isRunning = true;
        this.#isTradingStarted = true;
        setTimeout((() => {
            this.#isRampupTimeOver = true;
        }).bind(this), UnitHelper.formatedDurationToMs(this.#config.getRampupTime()));
    }

    stop() {
        this.#isTradingStarted = false;
        this.#saveTransactions();
        this.#saveWorker();
    }

    /**
     * @returns {AbstractWorker[]} 
     */
    getWorker(){
        return this.#worker;
    }

    /**
     * @returns {boolean}
     */
    isTradingUp(){
        return this.#isTradingStarted;
    }

    /**
     * @returns {AccountInfo}
     */
    getAccountInfo() {
        return this.#accountInfo;
    }

    /**
     * @param {TradingPair} tradingPair 
     * @returns {Transaction[]}
     */
    getTransactionsByAsset(tradingPair) {
        return this.#transactions.filter(t => t.symbol.getKey().toLowerCase() == tradingPair.getKey().toLowerCase());
    }

    /**
     * @param {TradingPair} tradingPair 
     * @returns {Transaction[]}
     */
    getClosedTransactionsByAsset(tradingPair) {
        return this.#transactionHistory.filter(t => t.symbol.getKey().toLowerCase() == tradingPair.getKey().toLowerCase());
    }

    /**
     * @returns {AssetDataCollector}
     */
    getAssetDataCollector() {
        return this.#dataCollector;
    }

    /**
     * Add a new worker to the bot. If the id is allready in use, an error will be thrown.
     * @param {AbstractWorker} worker 
     */
    registerWorker(worker) {
        if (this.#worker.has(worker.getId())) {
            const e = new Error("A worker with following id allready exists! Worker id: " + worker.getId());
            this.#logger.error(e);
            throw e;
        }

        this.#worker.set(worker.getId(), worker);
        this.#saveWorker();

        if(this.#isRunning){
            worker.init(this.#logger, this.#config.getDataDirectory());
            this.#registerTradingPair(worker.getTradingPair());
        }
    }

    /**
     * @param {AbstractWorker} worker 
     */
    unregisterWorker(worker) {
        if (worker == null || !this.#worker.has(worker.getId())) {
            this.#logger.warn("Unable to unregister worker with id: " + (worker == null ? 'null' : worker.getId()));
            return;
        }

        // TODO what to do with open transactions ???
        this.#worker.delete(worker.getId());
        this.#saveWorker();
    }

    /**
     * @param {string} workerId 
     */
    unregisterWorkerById(workerId) {
        this.unregisterWorker(this.#worker.get(workerId));
    }

    /**
     * @param {APIEvent} accountUpdateEvent 
     */
    handleAccountUpdate(accountUpdateEvent) {
        switch (accountUpdateEvent.eventType) {
            case APIEventType.BALANCE_CHANGE:
                // TODO update changed balances
                break;
            case APIEventType.TRANSACTION_UPDATE:
                this.#updateTransaction(accountUpdateEvent);
                break;
            default:
                this.#logger.warn("Unknown account type event!", accountUpdateEvent);
        }
    }

    /**
     * @param {APIEvent} assetUpdateEvent 
     */
    handleAssetUpdate(assetUpdateEvent) {
        this.#dataCollector.pushAssetEvent(assetUpdateEvent);

        if(assetUpdateEvent.eventType == APIEventType.ORDER_BOOK_UPDATE 
            && (!this.#orderBookUpdateTime.has(assetUpdateEvent.tradingPair.getKey())
            || this.#orderBookUpdateTime.get(assetUpdateEvent.tradingPair.getKey()) < Date.now() - 120000)
        ){
            this.#api.requestOrderBook(assetUpdateEvent.tradingPair);
            this.#orderBookUpdateTime.set(assetUpdateEvent.tradingPair.getKey(), Date.now());
        }

        if (!this.#isTradingStarted || !this.#isRampupTimeOver) {
            return;
        }

        this.#worker.forEach(worker => {
            if (worker.getTradingPair().getKey() === assetUpdateEvent.tradingPair.getKey()) {
                worker.onUpdate(this.#dataCollector.getAssetData(worker.getTradingPair()), this.#transactions.filter(t => t.initiator = worker.getId()));
            }
        });
    }

    /**
     * @param {TransactionUpdateEvent} event 
     */
    #updateTransaction(event) {
        /**
         * @type {Transaction}
         */
        let transaction = null;
        let index = -1;

        this.#transactions.forEach((v, i) => {
            if (v.id == event.transactionId || v.buySettings.apiID == event.apiId || v.sellSettings.apiID == event.apiId) {
                transaction = v;
                index = i;
            }
        });

        if (transaction === null) {
            this.#logger.warn("Update event for unknown transaction!", event);
            return;
        }

        const nextTransactionPhase = TransactionPhase.getPhaseForStatus(transaction.phase, event.status, event.fills.length > 0);
        if (transaction.phase !== nextTransactionPhase) {
            if (TransactionPhase.BUY_DONE === nextTransactionPhase) {
                this.#calculateTradeValues(transaction.buySettings, event.fills);
            } else if (TransactionPhase.isFinalPhase(nextTransactionPhase)) {
                this.#calculateTradeValues(transaction.sellSettings, event.fills);
                transaction.result = (sellSettings.quantity * sellSettings.price) - (buySettings.quantity * buySettings.price)
                this.#transactionHistory.push(transaction);
                this.#transactions.splice(index, 1);
            }

            transaction.phase = nextTransactionPhase;
            this.#saveTransactions();
        }
    }

    /**
     * @param {TransactionSettings} taSettings 
     * @param {TransactionFill[]} fills 
     */
    #calculateTradeValues(taSettings, fills) {
        let priceSum = 0;
        let quantity = 0;

        fills.forEach(fill => {
            priceSum += fill.price * fill.quantity;
            quantity += fill.quantity;
        });

        taSettings.quantity = quantity;
        taSettings.price = priceSum / quantity;
    }

    /**
     * @param {TradingPair} tradingPair 
     */
    #registerTradingPair(tradingPair) {
        this.#api.subscribe24hTicker(tradingPair);
        this.#api.subscribeKLines(tradingPair);
        this.#api.subscribeTrades(tradingPair);
        this.#api.requestOrderBook(tradingPair);
        this.#api.subscribeOrderBookUpdates(tradingPair);

    }

    #loadTransactions() {
        try {
            this.#transactions = SerializableHelper.load(this.#config.getDataDirectory(), 'transactions.sjson', Transaction);
        } catch (e) {
            this.#logger.warn("Bot was unable to load any transactions. Assuming no active transactions existing!", e);
        }

        try {
            this.#transactionHistory = SerializableHelper.load(this.#config.getDataDirectory(), 'transactionHistory.sjson', Transaction);
        } catch (e) {
            this.#logger.warn("Bot was unable to load any transaction history. Assuming no history existing!", e);
        }
    }

    #saveTransactions() {
        SerializableHelper.save(this.#config.getDataDirectory(), 'transactions.sjson', this.#transactions);
        SerializableHelper.save(this.#config.getDataDirectory(), 'transactionHistory.sjson', this.#transactionHistory);
    }

    #loadWorker() {
        try {
            const workerArr = SerializableHelper.load(this.#config.getDataDirectory(), 'worker.sjson', AbstractWorker);
            this.#worker.clear();
            workerArr.forEach(w => this.#worker.set(w.getId(), w));
        } catch (e) {
            this.#logger.warn("Bot was unable to worker. Assuming no worker registered!", e);
        }
    }

    #saveWorker() {
        const workerArr = [];
        Array.from(this.#worker.values()).forEach(w => workerArr.push(w));
        SerializableHelper.save(this.#config.getDataDirectory(), 'worker.sjson', workerArr);
    }
}