# pragma pylint: disable=attribute-defined-outside-init

"""
This module load custom strategies
"""
import logging
import tempfile
from base64 import urlsafe_b64decode
from collections import OrderedDict
from inspect import getfullargspec
from pathlib import Path
from typing import Any, Dict, Optional

from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import IResolver
from freqtrade.strategy.interface import IStrategy


logger = logging.getLogger(__name__)


class StrategyResolver(IResolver):
    """
    This class contains the logic to load custom strategy class
    """
    object_type = IStrategy
    object_type_str = "Strategy"
    user_subdir = USERPATH_STRATEGIES
    initial_search_path = None

    @staticmethod
    def load_strategy(config: Dict[str, Any] = None) -> IStrategy:
        """
        Load the custom class from config parameter
        :param config: configuration dictionary or None
        """
        config = config or {}

        if not config.get('strategy'):
            raise OperationalException("No strategy set. Please use `--strategy` to specify "
                                       "the strategy class to use.")

        strategy_name = config['strategy']
        strategy: IStrategy = StrategyResolver._load_strategy(
            strategy_name, config=config,
            extra_dir=config.get('strategy_path'))

        # make sure ask_strategy dict is available
        if 'ask_strategy' not in config:
            config['ask_strategy'] = {}

        if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
            # Assign ticker_interval to timeframe to keep compatibility
            if 'timeframe' not in config:
                logger.warning(
                    "DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
                    )
                strategy.timeframe = strategy.ticker_interval

        # Set attributes
        # Check if we need to override configuration
        #             (Attribute name,                    default,     subkey)
        attributes = [("minimal_roi",                     {"0": 10.0}, None),
                      ("timeframe",                       None,        None),
                      ("stoploss",                        None,        None),
                      ("trailing_stop",                   None,        None),
                      ("trailing_stop_positive",          None,        None),
                      ("trailing_stop_positive_offset",   0.0,         None),
                      ("trailing_only_offset_is_reached", None,        None),
                      ("use_custom_stoploss",             None,        None),
                      ("process_only_new_candles",        None,        None),
                      ("order_types",                     None,        None),
                      ("order_time_in_force",             None,        None),
                      ("stake_currency",                  None,        None),
                      ("stake_amount",                    None,        None),
                      ("protections",                     None,        None),
                      ("startup_candle_count",            None,        None),
                      ("unfilledtimeout",                 None,        None),
                      ("use_sell_signal",                 True,        'ask_strategy'),
                      ("sell_profit_only",                False,       'ask_strategy'),
                      ("ignore_roi_if_buy_signal",        False,       'ask_strategy'),
                      ("sell_profit_offset",              0.0,         'ask_strategy'),
                      ("disable_dataframe_checks",        False,       None),
                      ("ignore_buying_expired_candle_after",  0,       'ask_strategy')
                      ]
        for attribute, default, subkey in attributes:
            if subkey:
                StrategyResolver._override_attribute_helper(strategy, config.get(subkey, {}),
                                                            attribute, default)
            else:
                StrategyResolver._override_attribute_helper(strategy, config,
                                                            attribute, default)

        # Loop this list again to have output combined
        for attribute, _, subkey in attributes:
            if subkey and attribute in config[subkey]:
                logger.info("Strategy using %s: %s", attribute, config[subkey][attribute])
            elif attribute in config:
                logger.info("Strategy using %s: %s", attribute, config[attribute])

        StrategyResolver._normalize_attributes(strategy)

        StrategyResolver._strategy_sanity_validations(strategy)
        return strategy

    @staticmethod
    def _override_attribute_helper(strategy, config: Dict[str, Any],
                                   attribute: str, default: Any):
        """
        Override attributes in the strategy.
        Prevalence:
        - Configuration
        - Strategy
        - default (if not None)
        """
        if attribute in config:
            setattr(strategy, attribute, config[attribute])
            logger.info("Override strategy '%s' with value in config file: %s.",
                        attribute, config[attribute])
        elif hasattr(strategy, attribute):
            val = getattr(strategy, attribute)
            # None's cannot exist in the config, so do not copy them
            if val is not None:
                config[attribute] = val
        # Explicitly check for None here as other "falsy" values are possible
        elif default is not None:
            setattr(strategy, attribute, default)
            config[attribute] = default

    @staticmethod
    def _normalize_attributes(strategy: IStrategy) -> IStrategy:
        """
        Normalize attributes to have the correct type.
        """
        # Assign deprecated variable - to not break users code relying on this.
        if hasattr(strategy, 'timeframe'):
            strategy.ticker_interval = strategy.timeframe

        # Sort and apply type conversions
        if hasattr(strategy, 'minimal_roi'):
            strategy.minimal_roi = OrderedDict(sorted(
                {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(),
                key=lambda t: t[0]))
        if hasattr(strategy, 'stoploss'):
            strategy.stoploss = float(strategy.stoploss)
        return strategy

    @staticmethod
    def _strategy_sanity_validations(strategy):
        if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
            raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
                              f"Order-types mapping is incomplete.")

        if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF):
            raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
                              f"Order-time-in-force mapping is incomplete.")

    @staticmethod
    def _load_strategy(strategy_name: str,
                       config: dict, extra_dir: Optional[str] = None) -> IStrategy:
        """
        Search and loads the specified strategy.
        :param strategy_name: name of the module to import
        :param config: configuration for the strategy
        :param extra_dir: additional directory to search for the given strategy
        :return: Strategy instance or None
        """

        abs_paths = StrategyResolver.build_search_paths(config,
                                                        user_subdir=USERPATH_STRATEGIES,
                                                        extra_dir=extra_dir)

        if ":" in strategy_name:
            logger.info("loading base64 encoded strategy")
            strat = strategy_name.split(":")

            if len(strat) == 2:
                temp = Path(tempfile.mkdtemp("freq", "strategy"))
                name = strat[0] + ".py"

                temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
                temp.joinpath("__init__.py").touch()

                strategy_name = strat[0]

                # register temp path with the bot
                abs_paths.insert(0, temp.resolve())

        strategy = StrategyResolver._load_object(paths=abs_paths,
                                                 object_name=strategy_name,
                                                 add_source=True,
                                                 kwargs={'config': config},
                                                 )
        if strategy:
            strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
            strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
            strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
            if any([x == 2 for x in [strategy._populate_fun_len,
                                     strategy._buy_fun_len,
                                     strategy._sell_fun_len]]):
                strategy.INTERFACE_VERSION = 1

            return strategy

        raise OperationalException(
            f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
            "or contains Python code errors."
        )
