import logging
from typing import Any, Dict, List, Optional, TYPE_CHECKING

import arrow

from estrade.candle_set import CandleSet
from estrade.candle import Candle
from estrade.mixins.ref_mixin import RefMixin
from estrade.mixins.candle_set_indicator_mixin import CandleSetIndicatorMixin
from estrade.epic import Epic
from estrade.tick import Tick
from estrade.trade import Trade
from estrade.exceptions import StrategyException

if TYPE_CHECKING:
    from estrade.market import Market

logger = logging.getLogger(__name__)


class Strategy(RefMixin):
    """
    This class define a strategy to apply on ticks/candles.t pu

    Arguments:
        epics: list of `estrade.Epic` instances
        meta: free of use dict of parameters you can re-use in your strategy methods
        ref: strategy name
        max_concurrent_trades: nb max of trades concurrently opened

    Define your custom strategies to define when to open/close trades. Your strategies
    are triggered by all new ticks received and all new candles created on the
    Epics attached to it.

    !!! example
        Example of a strategy that

         - open a BUY trade when the tick value is 1000
         - open a SELL trade when the last closed candle was red
         - close all opened BUY trades when the moving average on 50 periods is
         inferior to the current tick - 20
         - close all trades when the strategy result is > 200 when a new candle opens.

        ```python
        from estrade import CandleSet, Market, Provider, SimpleMovingAverage, Strategy

        class MyCustomStrategy(Strategy):
            def on_new_tick_opening_strategy(tick):
                if tick.value == 1000:
                    self.open_trade(
                        epic=tick.epic.ref,
                        quantity=2,
                        direction='SELL',
                        stop_absolute=1010,
                        limit_absolute=990,
                    )

            def on_new_candle_opening_strategy(candle_set):
                last_candle = candle_set.last_closed_candle
                # check if is defined because on first candle in candle set,
                # it will be none.
                if last_candle:
                    if last_candle.color == 'red':
                        self.open_trade(
                            epic=candle_set.epic.ref,
                            quantity=1,
                            direction='BUY',
                            stop_relative=30,
                        )

            def on_new_tick_closing_strategy(tick):
                mm50_15mn = self.get_indicator(
                    epic_ref='MY_EPIC',
                    timeframe='15minutes',
                    indicator_name='mm50'
                )
                # check if exists because for the 49 fist candles it will not be defined
                if mm50_15mn:
                    if mm50_15mn < (tick.value - 20):
                        self.close_all_buys()


            def on_new_candle_closing_strategy(tick):
                result = self.market.trade_manager.result(strategy=self)
                if result > 200:
                    self.close_all_trades()

        class MyProvider(Provider):
            # define your custom provider
            pass

        if __name__ == '__main__':
            market = Market()

            epic = Epic(ref='MY_EPIC', market=market)
            mm50 = SimpleMovingAverage(ref='mm50')
            candle_set = CandleSet(epic=epic, timeframe='15minutes', indicators=[mm50])

            strategy = MyCustomStrategy(market=market, epics=[epic])
            provider = MyProvider(market=market)

            market.run()

        ```


    """

    def __init__(
        self,
        market: 'Market',
        epics: List[Epic],
        meta: Optional[Dict[str, Any]] = None,
        ref: str = None,
        max_concurrent_trades: int = 1,
    ):
        logger.info('Init new strategy creation')
        self.market: Market = market
        self.market.strategies.append(self)
        # init class ref attribute
        RefMixin.__init__(self, ref)

        self.epics = epics
        self.meta = meta or {}
        self.max_concurrent_trades = max_concurrent_trades
        self.status = "active"

        self.paused_until: Optional[arrow.Arrow] = None
        self.stopped = False

        logger.info('New strategy created %s' % self.ref)

    ##################################################
    # EPICS
    ##################################################
    @property
    def epics(self) -> List[Epic]:
        """
        Return:
            Returns list of epics attached to strategy
        """
        return self._epics

    @epics.setter
    def epics(self, epics: List[Epic]) -> None:
        """
        Attach epics on the current strategy.

        Raises:
            StrategyExeption: if input is not a list of `estrade.Epic` instances.


        """
        self._epics = []

        if not isinstance(epics, list):
            raise StrategyException('Invalid epics on strategy creation')

        for epic in epics:
            if not isinstance(epic, Epic):
                raise StrategyException('Invalid Epic')
            logger.debug('Add epic %s to strategy %s' % (epic.ref, self.ref))
            self._epics.append(epic)
            epic.strategies.append(self)

    ##################################################
    # GETTERS
    # Helpers to fetch elements in strategy execution methods
    ##################################################
    def get_epic(self, epic_ref: str) -> Epic:
        """
        Getter to find epic in strategies epic from its ref.

        Arguments:
            epic_ref: ref of an Epic in the current strategy epics.

        Raises:
            StrategyException: if epic not found in current strategy epics

        Returns:
            Epic instance

        """
        for epic in self.epics:
            if epic.ref == epic_ref:
                return epic
        raise StrategyException(f'Epic with ref {epic_ref} not found')

    def get_candle_set(self, epic_ref: str, timeframe: str) -> CandleSet:
        """
        Getter to find a candle set from epic_ref and timeframe.

        Arguments:
            epic_ref: reference of an epic attached to this strategy
            timeframe: timeframe of the `estrade.CandleSet` to return

        Raises:
            StrategyException : if epic not found on market
            EpicException : if no candle set with this timeframe found on this epic

        Return:
            A candle set object, see `estrade.CandleSet`

        """
        return self.get_epic(epic_ref).get_candle_set(timeframe)

    def get_candle(
        self, epic_ref: str, timeframe: str, offset: int = 0
    ) -> Optional[Candle]:
        """
        Getter to find a candle by its offset in a candleset

        Arguments:
            epic_ref: reference of an epic attached to this strategy
            timeframe: timeframe of the `estrade.CandleSet` to return
            offset: index of the candle to retrieve

        !!! info "Offset usage"
            - 0: returns the currently opened candle on candleset
            - 1 : returns the last closed candle
            - etc.

        !!! tip
            Reserve usage of this getter when you need to find candles that are not the
            current candle or the last closed candle.

            It is recommendend that you call the `Strategy.get_candle_set` method and
            retrieve its `last_closed_candle`, `current_candle` for these cases.

        Raises:
            StrategyException : if epic not found on market
            EpicException : if no candle set with this timeframe found on this epic

        Return:
            A candle object, see `estrade.Candle`

        """
        offset = abs(offset) + 1
        cs = self.get_candle_set(epic_ref, timeframe)
        if len(cs.candles) >= offset:
            return cs.candles[offset * -1]
        return None

    def get_indicator(
        self, epic_ref: str, timeframe: str, indicator_name: str, offset: int = 0
    ) -> Optional[CandleSetIndicatorMixin]:
        """
        Getter to find a candle indicator value.

        Arguments:
            epic_ref: ref of an epic in current strategy epics
            timeframe: matching a candleset.timeframe in epic
            indicator_name: matching an existing indicator on candle_set
            offset: offset of the indicator

        Raise:
            StrategyException : if epic not found on market
            EpicException : if no candle set with this timeframe found on this epic

        Returns:
            Indicator value
        """
        candle = self.get_candle(epic_ref, timeframe, offset)
        if not candle:
            return None
        return candle.indicators.get(indicator_name)

    def get_trades(self, **kwargs) -> Optional[List[Trade]]:
        """
        Getter to find list of strategy trades.

        !!! seealso
            referer to `TradeManager.get_trades` to view the list of arguments

        Returns:
            List of trades attached to the current strategy
        """
        return self.market.trade_manager.get_trades(strategy=self, **kwargs)

    ##################################################
    # OPEN/CLOSE trade(s) helpers
    ##################################################
    def open_trade(self, **kwargs) -> None:
        """
        Use this method to open a trade.

        This method calls `estrade.trade_manager.TradeManager.open_trade` method
        and inject this strategy as an argument.

        As a result, a new `estrade.Trade` instance is created and attached to the
        TradeManager.

        for example, you can open a trade in your strategy event as following ::

        !!! example
            Example of trade open that you can use in opening strategies
            ```python
                from estrade import Strategy

                class MyCustomStrategy:
                    def on_new_tick_opening_strategy(self, tick):
                        if tick.value == 1000:
                            self.open_trade(
                                # mandatory params
                                epic=tick.epic.ref,
                                quantity=5,
                                direction='SELL',
                                # optional params
                                ref='my_trade_ref',
                                stop_relative=10,
                                limit_relative=20,
                                meta={'data': 'my_data'},
                            )
            ```
        """
        self.market.trade_manager.open_trade(strategy=self, **kwargs)

    def close_trade_by_ref(self, ref: str, **kwargs) -> None:
        """
        Use this method in your closing strategy methods to close a trade by its
        reference.

        !!! info
            Closing strategy method where it is advised to call this method are:

            - `Strategy.on_new_tick_closing_strategy`
            - `Strategy.on_new_candle_closing_strategy`

        !!! seealso
            This method calls `estrade.TradeManager.close_trade_by_ref` method
            and inject this strategy as an argument.

        Arguments:
            ref: existing `estrade.trade.Trade` ref code attached to this strategy on
                current Market.trade_manager
            kwargs: args to close a trade
                (see `estrade.TradeManager.close_trade_by_ref`)
        hello
        """
        self.market.trade_manager.close_trade_by_ref(ref=ref, strategy=self, **kwargs)

    def close_all_trades(self, *args, **kwargs) -> None:
        """
        Use this method in your closing strategy methods to close all opened trades
        for strategy

        !!! info
            Closing strategy method where it is advised to call this method are:

            - `Strategy.on_new_tick_closing_strategy`
            - `Strategy.on_new_candle_closing_strategy`

        !!! seealso
            This method calls `estrade.TradeManager.close_all_trades` method and inject
            this strategy as an argument.

        Arguments:
            args: args to close a trade (see `estrade.TradeManager.close_all_trades`)
            kwargs: args to close a trade (see `estrade.TradeManager.close_all_trades`)
        """
        kwargs.pop('strategy', None)
        self.market.trade_manager.close_all_trades(*args, strategy=self, **kwargs)

    def close_all_buys(self, *args, **kwargs) -> None:
        """
        Use this method in closing strategy methods to close all opened buys for
        strategy

        !!! info
            Closing strategy method where it is advised to call this method are:

            - `Strategy.on_new_tick_closing_strategy`
            - `Strategy.on_new_candle_closing_strategy`

        !!! seealso
            This method calls `estrade.TradeManager.close_all_buys` method and inject
            this strategy as an argument.

        Arguments:
            args: args to close a trade (see `estrade.TradeManager.close_all_buys`)
            kwargs: args to close a trade (see `estrade.TradeManager.close_all_buys`)
        """
        kwargs.pop('strategy', None)
        self.market.trade_manager.close_all_buys(*args, strategy=self, **kwargs)

    def close_all_sells(self, *args, **kwargs) -> None:
        """
        Use this method in closing strategy methods to close all opened sells for
        strategy

        !!! info
            Closing strategy method where it is advised to call this method are:

            - `Strategy.on_new_tick_closing_strategy`
            - `Strategy.on_new_candle_closing_strategy`

        !!! seealso
            This method calls `estrade.TradeManager.close_all_sells` method and inject
            this strategy as an argument.

        Arguments:
            args: args to close a trade (see `estrade.TradeManager.close_all_sells`)
            kwargs: args to close a trade (see `estrade.TradeManager.close_all_sells`)
        """
        kwargs.pop('strategy', None)
        self.market.trade_manager.close_all_sells(*args, strategy=self, **kwargs)

    ##################################################
    # Check if strategy method has to be applied
    ##################################################
    def _apply_closing_strategy(self):
        """
        Check if closing strategy must be applied.
        Closing strategy is applied if trades are opened.
        :return: <bool> True if closing strategy have to be applied, else False
        """
        if self.market.trade_manager.strategy_trades[self.ref]["opened"]:
            return True
        return False

    def _apply_opening_strategy(self):
        """
        Check if opening strategy must be applied.
        Opening strategy have to be applied if the number of open trades is inferior to
        the max concurrent trades
        :return: <bool> True if opening strategy have to be applied, else False
        """
        if (
            len(self.market.trade_manager.strategy_trades[self.ref]["opened"])
            < self.max_concurrent_trades
        ):
            return True
        return False

    ##################################################
    # EVENTS
    ##################################################
    def _is_strategy_paused(self, current_datetime: arrow.Arrow) -> bool:
        """
        Check if strategy is paused for a given tick datetime.
        """
        if self.paused_until is None:
            self.set_pause()

        if self.paused_until is None:
            return False
        elif self.paused_until <= current_datetime:
            logger.info('Un-pause strategy %s' % self.ref)
            self.paused_until = None
            return False
        return True

    def on_new_tick(self, tick: Tick) -> None:
        """
        Method triggered when a new tick is received by Market.
        This method check if opening and/or closing srategies have to be applied and
        then call the opening/closing
        methods.

        Arguments:
            tick: new tick instance received

        """
        logger.debug('check if any trade has to be open/closed on new tick')

        if self._apply_closing_strategy():
            logger.debug('apply closing strategy')
            self.on_new_tick_closing_strategy(tick)

            for candle_set in tick.epic.candle_sets:
                if candle_set.new_candle_opened:
                    self.on_new_candle_closing_strategy(candle_set=candle_set)

        if self.stopped is False:
            self.set_stop()
        if self.stopped is True:
            logger.warning('Strategy %s is stopped, it must not be called' % self.ref)
            return

        if self._is_strategy_paused(tick.datetime):
            logger.debug('Strategy %s is paused, does not apply tick.')
            return

        if self._apply_opening_strategy():
            logger.debug('apply opening strategy')
            self.on_new_tick_opening_strategy(tick)

            for candle_set in tick.epic.candle_sets:
                if candle_set.new_candle_opened:
                    logger.debug(
                        "call new candle opening strategy bc "
                        "a new candle was created in %s" % candle_set.timeframe
                    )
                    self.on_new_candle_opening_strategy(candle_set=candle_set)

    # def on_new_candle(self, candle):
    #     # TODO: check if need to be removed
    #     raise NotImplementedError('On new candle not implemented on Strategy')

    ##################################################
    # Strategy apply method to be implemented
    ##################################################
    def set_pause(self):
        """
        This method is called before every opening and closing strategy methods
        execution. Its purpose is to update the `self.paused_until` parameter
        with a datetime (as arrow.Arrow) representing the target date until no
        opening/closing strategy method will be called.

        When a tick with a datetime superior to the `self.paused_until` value will
        be received from provider, the strategy will be un-paused automatically (the
        `self.paused_until` value will be reset to `None`)

        !!! note:
            Setting the `self.paused_until` does not close trades automatically,
            if required you have to explicitly close trades.

        """
        pass

    def set_stop(self):
        """
        This method is called before every opening and closing strategy methods
        execution. Its purpose is to update the `self.stopped` parameter
        with `True` when the strategy have to be totally stopped.

        !!! note:
            When the `self.stopped` value is set to `True` the strategy is totally
            deactivated. No tick will every be sent again to this strategy.

        !!! note:
            If all strategy are stopped, the market run will automatically end.

        """
        pass

    def on_new_tick_opening_strategy(self, tick: Tick) -> None:
        """
        This method is called on every tick received from provider when the number of
        open trades for this strategy
        is inferior to this instance `max_concurrent_trades` value.

        Arguments:
            tick: new tick received
        """
        pass

    def on_new_tick_closing_strategy(self, tick: Tick) -> None:
        """
        Method is called on every tick received from provider when at least one trade is
        opened for this strategy.

        Arguments:
            tick: new tick received

        """
        pass

    def on_new_candle_opening_strategy(self, candle_set: CandleSet) -> None:
        """
        Method called every time a new candle is created when the number of opened
        trades for this strategy is inferior to this instance `max_concurrent_trades`
        value.

        Use this method to open trades with `Strategy.open_trade` method.

        Arguments:
            candle_set: candle set that generated a new candle

        """
        pass

    def on_new_candle_closing_strategy(self, candle_set: CandleSet) -> None:
        """
        Method called every time a new candle is created and at least one trade is
        opened for this strategy.

        Arguments:
            candle_set: candle set that generated a new candle

        """
        pass

    ##################################################
    # Reporting
    ##################################################
    @property
    def to_json(self) -> Dict[str, Any]:
        """
        Generate a dictionary of strategy stats for reporting
        """
        strategy_stats = {
            "ref": self.ref,
            "max_concurrent_trades": self.max_concurrent_trades,
            "profit_factor": None,
            "result": None,
            "nb_trades": 0,
            "nb_win": 0,
            "nb_losses": 0,
            **self.meta,
        }
        if self.market and self.market.trade_manager:
            strategy_stats["profit_factor"] = self.market.trade_manager.profit_factor(
                strategy=self
            )
            strategy_stats["result"] = self.market.trade_manager.result(strategy=self)
            strategy_stats["nb_trades"] = self.market.trade_manager.nb_trades(
                strategy=self
            )
            strategy_stats["nb_win"] = self.market.trade_manager.nb_win(strategy=self)
            strategy_stats["nb_losses"] = self.market.trade_manager.nb_losses(
                strategy=self
            )
            strategy_stats["max_drawdown"] = self.market.trade_manager.max_drowdown(
                strategy=self
            )

        return strategy_stats

    @property
    def json_headers(self) -> List[str]:
        """
        Generate header of a dictionary of strategy stats for reporting
        """
        headers = [
            "ref",
            "profit_factor",
            "result",
            "nb_trades",
            "nb_win",
            "nb_losses",
            "max_drawdown",
            "max_concurrent_trades",
        ]
        headers.extend(list(self.meta.keys()))
        return headers
