import asyncio
import logging
import time
from functools import wraps
from typing import Any, Callable, Optional, TypeVar, cast, overload

from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
from freqtrade.mixins import LoggingMixin


logger = logging.getLogger(__name__)
__logging_mixin = None


def _reset_logging_mixin():
    """
    Reset global logging mixin - used in tests only.
    """
    global __logging_mixin
    __logging_mixin = LoggingMixin(logger)


def _get_logging_mixin():
    # Logging-mixin to cache kucoin responses
    # Only to be used in retrier
    global __logging_mixin
    if not __logging_mixin:
        __logging_mixin = LoggingMixin(logger)
    return __logging_mixin


# Maximum default retry count.
# Functions are always called RETRY_COUNT + 1 times (for the original call)
API_RETRY_COUNT = 4
API_FETCH_ORDER_RETRY_COUNT = 5

BAD_EXCHANGES = {
    "bitmex": "Various reasons.",
    "phemex": "Does not provide history.",
    "probit": "Requires additional, regular calls to `signIn()`.",
    "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
}

MAP_EXCHANGE_CHILDCLASS = {
    'binanceus': 'binance',
    'binanceje': 'binance',
    'binanceusdm': 'binance',
    'okex': 'okx',
    'gate': 'gateio',
}

SUPPORTED_EXCHANGES = [
    'binance',
    'bittrex',
    'ftx',
    'gateio',
    'huobi',
    'kraken',
    'okx',
]

EXCHANGE_HAS_REQUIRED = [
    # Required / private
    'fetchOrder',
    'cancelOrder',
    'createOrder',
    'fetchBalance',

    # Public endpoints
    'fetchOHLCV',
]

EXCHANGE_HAS_OPTIONAL = [
    # Private
    'fetchMyTrades',  # Trades for order - fee detection
    'createLimitOrder', 'createMarketOrder',  # Either OR for orders
    # 'setLeverage',  # Margin/Futures trading
    # 'setMarginMode',  # Margin/Futures trading
    # 'fetchFundingHistory', # Futures trading
    # Public
    'fetchOrderBook', 'fetchL2OrderBook', 'fetchTicker',  # OR for pricing
    'fetchTickers',  # For volumepairlist?
    'fetchTrades',  # Downloading trades data
    # 'fetchFundingRateHistory',  # Futures trading
    # 'fetchPositions',  # Futures trading
    # 'fetchLeverageTiers',  # Futures initialization
    # 'fetchMarketLeverageTiers',  # Futures initialization
]


def remove_credentials(config) -> None:
    """
    Removes exchange keys from the configuration and specifies dry-run
    Used for backtesting / hyperopt / edge and utils.
    Modifies the input dict!
    """
    if config.get('dry_run', False):
        config['exchange']['key'] = ''
        config['exchange']['secret'] = ''
        config['exchange']['password'] = ''
        config['exchange']['uid'] = ''


def calculate_backoff(retrycount, max_retries):
    """
    Calculate backoff
    """
    return (max_retries - retrycount) ** 2 + 1


def retrier_async(f):
    async def wrapper(*args, **kwargs):
        count = kwargs.pop('count', API_RETRY_COUNT)
        kucoin = args[0].name == "KuCoin"  # Check if the exchange is KuCoin.
        try:
            return await f(*args, **kwargs)
        except TemporaryError as ex:
            msg = f'{f.__name__}() returned exception: "{ex}". '
            if count > 0:
                msg += f'Retrying still for {count} times.'
                count -= 1
                kwargs['count'] = count
                if isinstance(ex, DDosProtection):
                    if kucoin and "429000" in str(ex):
                        # Temporary fix for 429000 error on kucoin
                        # see https://github.com/freqtrade/freqtrade/issues/5700 for details.
                        _get_logging_mixin().log_once(
                            f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. "
                            f"{count} tries left before giving up", logmethod=logger.warning)
                        # Reset msg to avoid logging too many times.
                        msg = ''
                    else:
                        backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
                        logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
                        await asyncio.sleep(backoff_delay)
                if msg:
                    logger.warning(msg)
                return await wrapper(*args, **kwargs)
            else:
                logger.warning(msg + 'Giving up.')
                raise ex
    return wrapper


F = TypeVar('F', bound=Callable[..., Any])


# Type shenanigans
@overload
def retrier(_func: F) -> F:
    ...


@overload
def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]:
    ...


def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT):
    def decorator(f: F) -> F:
        @wraps(f)
        def wrapper(*args, **kwargs):
            count = kwargs.pop('count', retries)
            try:
                return f(*args, **kwargs)
            except (TemporaryError, RetryableOrderError) as ex:
                msg = f'{f.__name__}() returned exception: "{ex}". '
                if count > 0:
                    logger.warning(msg + f'Retrying still for {count} times.')
                    count -= 1
                    kwargs.update({'count': count})
                    if isinstance(ex, (DDosProtection, RetryableOrderError)):
                        # increasing backoff
                        backoff_delay = calculate_backoff(count + 1, retries)
                        logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
                        time.sleep(backoff_delay)
                    return wrapper(*args, **kwargs)
                else:
                    logger.warning(msg + 'Giving up.')
                    raise ex
        return cast(F, wrapper)
    # Support both @retrier and @retrier(retries=2) syntax
    if _func is None:
        return decorator
    else:
        return decorator(_func)
