import asyncio
import math
from abc import ABC, abstractmethod
from asyncio import Lock
from datetime import datetime
from logging import Logger
from typing import Union
from decimal import Decimal

import ccxt
from ccxt.base.exchange import Exchange
from functional import seq

from neobabix.models.ccxt import CurrencyInfo, PrecisionField
from neobabix.notifications.notification import Notification
from neobabix.strategies.strategy import Actions


class Playbook(ABC):
    __name__ = 'Neobabix Playbook'

    """
        This is the flow for playbooks.
        
        Instantiated > Acquire Lock > Play > Entry > After Entry > Exit > After Exit > Release Lock > Destructured 
    """

    """
        Supported Exchanges:
            - Bybit
    """

    def __init__(self, action: Actions, exchange: Exchange, trade_lock: Lock, logger: Logger, symbol: str,
                 timeframe: str, notification: Notification, recursive: bool = False, leverage: int = None,
                 ohlcv: dict = None):
        self.action = action
        if self.action != Actions.LONG and self.action != Actions.SHORT:
            raise NotImplementedError('Supported actions are LONG and SHORT')

        self.exchange = exchange
        self.trade_lock = trade_lock
        self.logger = logger
        self.recursive = recursive
        self.symbol = symbol
        self.timeframe = timeframe
        self.leverage = leverage
        self.notification = notification
        self.ohlcv = ohlcv

        # Currency Info
        market_info = seq(self.exchange.fetch_markets()) \
            .filter(lambda x: x.get('symbol') == symbol) \
            .map(lambda x: CurrencyInfo(**x)) \
            .to_list()
        if len(market_info) == 0:
            raise NotImplementedError('Market info is not implemented in this exchange, bailing')
        self.currency_info: CurrencyInfo = market_info[0]

        # Orders
        self.order_entry = {}
        self.order_exit = {}
        self.order_stop = {}

        self.execution_start_time = datetime.utcnow()

    async def play(self):
        # Acquire lock immediately
        if not self.trade_lock.locked():
            self.info('Acquiring trade lock')
            await self.trade_lock.acquire()

        await self.entry()
        await self.after_entry()
        await self.exit()
        await self.after_exit()

        if not self.recursive:
            await self.release_trade_lock()

    @abstractmethod
    async def entry(self):
        pass

    @abstractmethod
    async def after_entry(self):
        pass

    @abstractmethod
    async def exit(self):
        pass

    @abstractmethod
    async def after_exit(self):
        pass

    @property
    def current_price(self):
        return float(self.ohlcv.get('closes')[-1])

    @property
    def entry_price(self):
        if self.order_entry is None:
            return None
        return float(self.order_entry.get('price'))

    @property
    def price_precision(self) -> int:
        if type(self.currency_info.precision.price) == int:
            return self.currency_info.precision.price

        sp = str(self.currency_info.precision.price).split('.')
        if len(sp) != 2:
            raise ValueError('Unrecognized precision given from the exchange')

        return len(sp[1])

    @property
    def amount_precision(self) -> int:
        return int(self.currency_info.precision.amount)

    @property
    def min_price(self) -> Decimal:
        return Decimal(self.currency_info.limits.price.min)

    @property
    def max_price(self) -> Decimal:
        return Decimal(self.currency_info.limits.price.max)

    @property
    def min_amount(self) -> Decimal:
        if not self.currency_info.limits.amount.min:
            return Decimal(0)
        return Decimal(self.currency_info.limits.amount.min)

    @property
    def max_amount(self) -> Decimal:
        if not self.currency_info.limits.amount.max:
            return Decimal(10**9)
        return Decimal(self.currency_info.limits.amount.max)

    @property
    @abstractmethod
    def exit_price(self):
        return None

    @property
    @abstractmethod
    def stop_price(self):
        return None

    @property
    @abstractmethod
    def stop_action_price(self):
        return None

    @staticmethod
    def round_decimals_down(number: float, decimals: int = 8):
        if not isinstance(decimals, int):
            raise TypeError("decimal places must be an integer")
        elif decimals < 0:
            raise ValueError("decimal places has to be 0 or more")
        elif decimals == 0:
            return math.floor(number)

        factor = 10 ** decimals
        return math.floor(number * factor) / factor

    async def notify(self, message):
        # TODO: Notification mechanism to notify for significant events
        pass

    async def sleep(self, interval_in_seconds):
        await asyncio.sleep(interval_in_seconds)

    def info(self, message):
        self.logger.info(f'{self.__name__}: {message}')

    def debug(self, message):
        self.logger.debug(f'{self.__name__}: {message}')

    async def release_trade_lock(self):
        self.info('Releasing trade lock')
        self.trade_lock.release()

    async def get_latest_candle(self):
        ohlcv = self.exchange.fetch_ohlcv(symbol=self.symbol,
                                          timeframe=self.timeframe,
                                          limit=1)

        return ohlcv[0]

    async def get_ticker(self):
        return self.exchange.fetch_ticker(symbol=self.symbol)

    async def set_leverage(self, leverage: int):
        normalized_symbol = None
        post_name = get_name = None

        if type(self.exchange) == ccxt.bybit:
            post_name = 'userPostLeverageSave'
            get_name = 'userGetLeverage'
            normalized_symbol = self.symbol.replace('/', '')
        if not post_name or not get_name or not normalized_symbol:
            raise NotImplementedError('Unsupported exchange')

        # Get leverage
        method = getattr(self.exchange, get_name)
        response = method()
        if response.get('result').get(normalized_symbol).get('leverage') == leverage:
            return response

        # Post leverage
        method = getattr(self.exchange, post_name)
        response = method(params={
            'symbol': normalized_symbol,
            'leverage': leverage
        })

        if response.get('ret_code') != 0 or response.get('ret_msg') != 'ok':
            raise AssertionError('Got error message while setting leverage')

        return response

    async def poll_results(self):
        self.info(f'Starting to poll for order IDs f{self.order_exit.get("id")} and f{self.order_stop.get("id")}')

        def _poll_orders(exit_order_id, stop_order_id, symbol) -> Union[dict, bool]:
            exit_order = self.exchange.fetch_order(id=exit_order_id,
                                                   symbol=symbol)
            stop_order = self.exchange.fetch_order(id=stop_order_id,
                                                   symbol=symbol)

            exit_order_status = exit_order.get('status')
            stop_order_status = stop_order.get('status')

            ended_statuses = ['closed', 'canceled']

            if exit_order_status in ended_statuses or stop_order_status in ended_statuses:
                return {
                    'stop_order': stop_order,
                    'exit_order': exit_order
                }

            return False

        while True:
            self.info(f'Going to poll for order ids: {self.order_exit.get("id")} {self.order_stop.get("id")}')
            result = _poll_orders(exit_order_id=self.order_exit.get('id'),
                                  stop_order_id=self.order_stop.get('id'),
                                  symbol=self.symbol)
            if result is False:
                self.info('Exit and Stop orders are still open, sleeping..')
                await asyncio.sleep(60)
                continue
            else:
                self.info(f'Got either closed or canceled on one of the order, breaking out..')
                break

        return result

    async def cancel_order(self, order_id):
        result = self.exchange.cancel_order(id=order_id,
                                            symbol=self.symbol)
        return result

    async def limit_buy_order(self, price, amount):
        order = self.exchange.create_limit_buy_order(symbol=self.symbol,
                                                     amount=amount,
                                                     price=price)
        return order

    async def market_buy_order(self, amount):
        if not self.exchange.has['createMarketOrder']:
            raise AttributeError('The selected exchange does not support market orders')

        order = self.exchange.create_market_buy_order(symbol=self.symbol,
                                                      amount=amount)

        return order

    async def market_sell_order(self, amount):
        if not self.exchange.has['createMarketOrder']:
            raise AttributeError('The selected exchange does not support market orders')

        order = self.exchange.create_market_sell_order(symbol=self.symbol,
                                                       amount=amount)

        return order

    async def market_stop_sell_order(self, amount):
        if not self.exchange.has['createMarketOrder']:
            raise AttributeError('The selected exchange does not support market orders')

        order = self.exchange.create_order(symbol=self.symbol,
                                           tyoe='market',
                                           side='sell',
                                           amount=amount)

        return order

    async def market_stop_buy_order(self, amount):
        if not self.exchange.has['createMarketOrder']:
            raise AttributeError('The selected exchange does not support market orders')

        order = self.exchange.create_order(symbol=self.symbol,
                                           type='market',
                                           side='buy',
                                           amount=amount)

        return order

    async def limit_buy_order(self, amount, price):
        order = self.exchange.create_order(symbol=self.symbol,
                                           type='limit',
                                           side='buy',
                                           amount=amount,
                                           price=price)
        return order

    async def limit_sell_order(self, amount, price):
        order = self.exchange.create_order(symbol=self.symbol,
                                           type='limit',
                                           side='sell',
                                           amount=amount,
                                           price=price)
        return order

    async def limit_stop_order(self, side, amount, stop_price, price, base_price):
        if type(self.exchange) != ccxt.bybit:
            raise NotImplementedError('Unsupported exchange')

        method_name = None
        if type(self.exchange) == ccxt.bybit:
            method_name = 'openapiPostStopOrderCreate'
        if not method_name:
            raise NotImplementedError('The exchange does not support stop orders')

        normalized_symbol = None
        if type(self.exchange) == ccxt.bybit:
            normalized_symbol = self.symbol.replace('/', '')
        if not normalized_symbol:
            raise NotImplementedError('Unsupported exchange')

        method = getattr(self.exchange, method_name)
        order = method(params={
            'side': side,
            'symbol': normalized_symbol,
            'order_type': 'Limit',
            'qty': amount,
            'price': price,
            'stop_px': stop_price,
            'base_price': base_price,
            'close_on_trigger': True,
            'time_in_force': 'GoodTillCancel'
        })
        order.update({
            'id': order.get('result').get('stop_order_id'),
            'price': order.get('result').get('price')
        })

        return order

    async def limit_stop_sell_order(self, amount, stop_price, stop_action_price, base_price):
        return await self.limit_stop_order(side='Sell',
                                           amount=amount,
                                           stop_price=stop_price,
                                           price=stop_action_price,
                                           base_price=base_price)

    async def limit_stop_buy_order(self, amount, stop_price, stop_action_price, base_price):
        return await self.limit_stop_order(side='Buy',
                                           amount=amount,
                                           stop_price=stop_price,
                                           price=stop_action_price,
                                           base_price=base_price)

    async def get_order(self, order_id: str):
        return self.exchange.fetch_order(id=order_id,
                                         symbol=self.symbol)
