#  Drakkar-Software OctoBot
#  Copyright (c) Drakkar-Software, All rights reserved.
#
#  This library is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 3.0 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library.
import logging
import os
import shutil
import traceback
import logging.config as config

import sys
import async_channel.channels as channel_instances
import async_channel.enums as channel_enums

import octobot_commons.enums as enums
import octobot_commons.constants as commons_constants
import octobot_commons.logging as common_logging
import octobot_commons.channels_name as channels_name
import octobot_commons.pretty_printer as pretty_printer

import octobot_evaluators.evaluators.channel as evaluator_channels

import octobot_trading.exchanges as trading_exchanges
import octobot_trading.enums as trading_enums

import octobot.constants as constants

BOT_CHANNEL_LOGGER = None
LOGGER_PRIORITY_LEVEL = channel_enums.ChannelConsumerPriorityLevels.OPTIONAL.value


def _log_uncaught_exceptions(ex_cls, ex, tb):
    logging.exception("".join(traceback.format_tb(tb)))
    logging.exception("{0}: {1}".format(ex_cls, ex))


def init_logger():
    try:
        if not os.path.exists(constants.LOGS_FOLDER):
            os.mkdir(constants.LOGS_FOLDER)
        _load_logger_config()
        # overwrite BOT_CHANNEL_LOGGER to apply global logging configuration
        global BOT_CHANNEL_LOGGER
        BOT_CHANNEL_LOGGER = common_logging.get_logger("OctoBot Channel")
    except KeyError:
        print(
            "Impossible to start OctoBot: the logging configuration can't be found in '"
            + constants.LOGGING_CONFIG_FILE
            + "' please make sure you are running OctoBot from its root directory."
        )
        os._exit(-1)

    logger = logging.getLogger("OctoBot Launcher")

    try:
        # Force new log file creation not to log at the previous one's end.
        logger.parent.handlers[1].doRollover()
    except PermissionError:
        print(
            "Impossible to start OctoBot: the logging file is locked, this is probably due to another running "
            "OctoBot instance."
        )
        os._exit(-1)

    sys.excepthook = _log_uncaught_exceptions
    return logger


def _load_logger_config():
    try:
        # use local logging file to allow users to customize the log level
        if not os.path.isfile(constants.USER_LOCAL_LOGGING_CONFIG_FILE):
            if not os.path.exists(commons_constants.USER_FOLDER):
                os.mkdir(commons_constants.USER_FOLDER)
            shutil.copyfile(constants.LOGGING_CONFIG_FILE, constants.USER_LOCAL_LOGGING_CONFIG_FILE)
        config.fileConfig(constants.USER_LOCAL_LOGGING_CONFIG_FILE)
    except Exception as ex:
        config.fileConfig(constants.LOGGING_CONFIG_FILE)
        logging.getLogger("Logging Configuration").warning(f"Impossible to initialize local logging configuration file,"
                                                           f" using default one. {ex}")


async def init_exchange_chan_logger(exchange_id):
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.TICKER_CHANNEL.value,
                                     exchange_id).new_consumer(
        ticker_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.MINI_TICKER_CHANNEL.value,
                                     exchange_id).new_consumer(
        mini_ticker_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.RECENT_TRADES_CHANNEL.value,
                                     exchange_id).new_consumer(
        recent_trades_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.ORDER_BOOK_CHANNEL.value,
                                     exchange_id).new_consumer(
        order_book_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.ORDER_BOOK_TICKER_CHANNEL.value,
                                     exchange_id).new_consumer(
        order_book_ticker_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value,
                                     exchange_id).new_consumer(
        kline_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value,
                                     exchange_id).new_consumer(
        ohlcv_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.BALANCE_CHANNEL.value,
                                     exchange_id).new_consumer(
        balance_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.BALANCE_PROFITABILITY_CHANNEL.value,
                                     exchange_id).new_consumer(
        balance_profitability_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.TRADES_CHANNEL.value,
                                     exchange_id).new_consumer(
        trades_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.LIQUIDATIONS_CHANNEL.value,
                                     exchange_id).new_consumer(
        liquidations_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.POSITIONS_CHANNEL.value,
                                     exchange_id).new_consumer(
        positions_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.ORDERS_CHANNEL.value,
                                     exchange_id).new_consumer(
        orders_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value,
                                     exchange_id).new_consumer(
        mark_price_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await trading_exchanges.get_chan(channels_name.OctoBotTradingChannelsName.FUNDING_CHANNEL.value,
                                     exchange_id).new_consumer(
        funding_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )


async def init_evaluator_chan_logger(matrix_id: str):
    await evaluator_channels.get_chan(channels_name.OctoBotEvaluatorsChannelsName.MATRIX_CHANNEL.value,
                                      matrix_id).new_consumer(
        matrix_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await evaluator_channels.get_chan(channels_name.OctoBotEvaluatorsChannelsName.EVALUATORS_CHANNEL.value,
                                      matrix_id).new_consumer(
        evaluators_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )


async def init_octobot_chan_logger(bot_id: str):
    await channel_instances.get_chan_at_id(constants.OCTOBOT_CHANNEL, bot_id).new_consumer(
        octobot_channel_callback,
        priority_level=LOGGER_PRIORITY_LEVEL,
        bot_id=bot_id,
        subject=[enums.OctoBotChannelSubjects.NOTIFICATION.value, enums.OctoBotChannelSubjects.ERROR.value]
    )


async def ticker_callback(
    exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, ticker
):
    BOT_CHANNEL_LOGGER.debug(
        f"TICKER : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || TICKER = {ticker}"
    )


async def mini_ticker_callback(
    exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mini_ticker
):
    BOT_CHANNEL_LOGGER.debug(
        f"MINI TICKER : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || MINI TICKER = {mini_ticker}"
    )


async def order_book_callback(
    exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, asks, bids
):
    BOT_CHANNEL_LOGGER.debug(
        f"ORDERBOOK : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || ASKS = {len(asks)} orders || BIDS = {len(bids)} orders"
    )


async def order_book_ticker_callback(
    exchange: str,
    exchange_id: str,
    cryptocurrency: str,
    symbol: str,
    ask_quantity,
    ask_price,
    bid_quantity,
    bid_price,
):
    BOT_CHANNEL_LOGGER.debug(
        f"ORDERBOOK TICKER : EXCHANGE = {exchange} || SYMBOL = {symbol} "
        f"|| ASK PRICE / QUANTIY = {ask_price} / {ask_quantity}"
        f"|| BID PRICE / QUANTIY = {bid_price} / {bid_quantity}"
    )


async def ohlcv_callback(
    exchange: str,
    exchange_id: str,
    cryptocurrency: str,
    symbol: str,
    time_frame,
    candle,
):
    BOT_CHANNEL_LOGGER.debug(
        f"OHLCV : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| TIME FRAME = {time_frame} || CANDLE = {candle}"
    )


async def recent_trades_callback(
    exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, recent_trades
):
    BOT_CHANNEL_LOGGER.debug(
        f"RECENT TRADES : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || 10 first RECENT TRADES = {recent_trades[:10]}"
    )


async def liquidations_callback(
    exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, liquidations
):
    BOT_CHANNEL_LOGGER.debug(
        f"LIQUIDATIONS : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || LIQUIDATIONS = {liquidations}"
    )


async def kline_callback(
    exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, time_frame, kline
):
    BOT_CHANNEL_LOGGER.debug(
        f"KLINE : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| TIME FRAME = {time_frame} || KLINE = {kline}"
    )


async def mark_price_callback(
    exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mark_price
):
    BOT_CHANNEL_LOGGER.info(
        f"MARK PRICE : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || MARK PRICE = {mark_price}"
    )


async def balance_callback(exchange: str, exchange_id: str, balance):
    BOT_CHANNEL_LOGGER.info(f"BALANCE : EXCHANGE = {exchange} || BALANCE = {balance}")


async def balance_profitability_callback(
    exchange: str,
    exchange_id: str,
    profitability,
    profitability_percent,
    market_profitability_percent,
    initial_portfolio_current_profitability,
):
    BOT_CHANNEL_LOGGER.info(
        f"BALANCE PROFITABILITY : EXCHANGE = {exchange} || PROFITABILITY = "
        f"{pretty_printer.portfolio_profitability_pretty_print(profitability, profitability_percent, 'USDT')}"
    )


async def trades_callback(
    exchange: str,
    exchange_id: str,
    cryptocurrency: str,
    symbol: str,
    trade: dict,
    old_trade: bool,
):
    BOT_CHANNEL_LOGGER.info(
        f"TRADES : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| TRADE = {trade} "
        f"|| OLD_TRADE = {old_trade}"
    )


async def orders_callback(
    exchange: str,
    exchange_id: str,
    cryptocurrency: str,
    symbol: str,
    order: dict,
    is_new: bool,
    is_from_bot: bool,
):
    order_string = f"ORDERS : EXCHANGE = {exchange} || SYMBOL = {symbol} || " \
                   f"{pretty_printer.open_order_pretty_printer(exchange, order)} || " \
                   f"status = {order.get(trading_enums.ExchangeConstantsOrderColumns.STATUS.value, None)} || " \
                   f"CREATED = {is_new} || FROM_BOT = {is_from_bot}"
    BOT_CHANNEL_LOGGER.info(order_string)


async def positions_callback(
    exchange: str,
    exchange_id: str,
    cryptocurrency: str,
    symbol: str,
    position,
    is_closed: bool,
    is_updated: bool,
    is_liquidated: bool,
    is_from_bot: bool,
):
    BOT_CHANNEL_LOGGER.info(
        f"POSITIONS : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || POSITIONS = {position}"
        f"|| CLOSED = {is_closed} || UPDATED = {is_updated} || LIQUIDATED = {is_liquidated} "
        f"|| FROM_BOT = {is_from_bot}"
    )


async def funding_callback(
    exchange: str,
    exchange_id: str,
    cryptocurrency: str,
    symbol: str,
    funding_rate,
    next_funding_time,
    timestamp,
):
    BOT_CHANNEL_LOGGER.info(
        f"FUNDING : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| RATE = {str(funding_rate)}"
        f"|| NEXT TIME = {str(next_funding_time)} || TIMESTAMP = {str(timestamp)}"
    )


async def matrix_callback(
    matrix_id,
    evaluator_name,
    evaluator_type,
    eval_note,
    eval_note_type,
    exchange_name,
    cryptocurrency,
    symbol,
    time_frame,
):
    BOT_CHANNEL_LOGGER.debug(
        f"MATRIX : EXCHANGE = {exchange_name} || "
        f"EVALUATOR = {evaluator_name} || EVALUATOR_TYPE = {evaluator_type} || "
        f"CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} || TF = {time_frame} "
        f"|| NOTE = {eval_note} [MATRIX id = {matrix_id}] "
    )


async def evaluators_callback(
    matrix_id,
    evaluator_name,
    evaluator_type,
    exchange_name,
    cryptocurrency,
    symbol,
    time_frame,
    data,
):
    BOT_CHANNEL_LOGGER.debug(
        f"EVALUATORS : EXCHANGE = {exchange_name} || "
        f"EVALUATOR = {evaluator_name} || EVALUATOR_TYPE = {evaluator_type} || "
        f"CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} || TF = {time_frame} "
        f"|| DATA = {data} [MATRIX id = {matrix_id}] "
    )


async def octobot_channel_callback(
        bot_id: str,
        subject: str,
        action: str,
        data: dict
):
    BOT_CHANNEL_LOGGER.debug(
        f"OCTOBOT_CHANNEL : SUBJECT = {subject} || ACTION = {action} || DATA = {data} "
    )
