import logging
from power_switch import pybreaker
from structlog import get_logger
import exceptions

_breakers_map = {}


class Failsafe(object):
    """
    Utility class for creating circuit breakers.

    It is important to notice that every function decorated
    via failsafe must have a param named `key`. The value of
    this param is used to uniquely identify the circuit breaker
    instance. The callee must provide the value of this `key`.


    Basic usage::

        # Create a global failsafe instance
        failsafe = Failsafe(service_name='YourServiceName')

        # Use breaker decorator on external/dangerous calls
        @failsafe.breaker()
        def external_call(key=None):
            # perform dangerous operations


    To use ``redis`` for storing the circuit breaker state::

        failsafe = Failsafe(
            service_name='YourServiceName'
            redis_client=redis_client_object
        )


    A more advanced use case of breaker decorator::

        @failsafe.breaker(err_threshold=0.5, request_volume_window=10, reset_timeout=60)
        def external_call(key=None):
            # perform dangerous operations

    `err_threshold`: err_rate after which circuit open, this
                    value lies between 0 to 1
    `reset_timeout`: time in seconds before the circuit breaker
                   state changes to half open
    `request_volume_window` : box to determine, how many request are
                   taken into consideration for calculation
                   of current err_rate
    """

    def __init__(
        self,
        service_name,
        logger=None
    ):
        """
        :param service_name: str, name of the service
        :param redis_client: redis.Redis, if provided this circuit breaker uses
                redis for storing the state

        """
        self._service_name = service_name
        if logger:
            self.logger = logger
        else:
            log = logging.getLogger(__name__)
            console_handler = logging.StreamHandler()
            console_handler.setLevel(logging.INFO)
            console_handler_formatter = logging.Formatter(
                '[%(asctime)s] [%(levelname)-8s] [%(name)s:%(lineno)s] -- %('
                'message)s'
            )
            console_handler.setFormatter(console_handler_formatter)
            log.addHandler(console_handler)
            log.setLevel(logging.INFO)
            self.logger = get_logger().new()

    def breaker(self, err_threshold=0.3, request_volume_window=30, reset_timeout=5, exclude=None):
        """
        :param err_threshold: float, err_rate after which circuit open
        :param request_volume_window: int, box to determine, how many request
                                are taken into consideration for calculation
                                of current error_rate
        :param reset_timeout: int, timeout in seconds
        :param exclude: List[ClassVar[Exception]] (optional), list of exception
                        classes to be ignored by the circuit breaker. It
                        essentially means that these exceptions fill be
                        transparent to circuit breaker and will not be
                        counted towards failure
        """

        def breaker_decorator(method):
            def _breaker(*args, **kwargs):
                # Used as the key to the breaker object cache
                # This will raise `KeyError` in case the function has
                # no parameter with name key. This behaviour is intentional
                cache_key = kwargs['key']

                # Used while persisting the circuit breaker state into
                # the redis
                # ignore var as we are not using redis storage as of now
                namespace = self._get_namespace(cache_key)
                # limiting Maximum size of the dict holding
                # CircuitBreaker instances based on cache_key
                size_limit = 100000
                if len(_breakers_map) >= size_limit:
                    self.logger.error("Circuit Breaker instances limit of {}"
                                      " has been exhausted".format(size_limit),
                                      exc_info=True)
                    raise exceptions.MemoryLimitExhausted(400, "Maximum number {} of Circuit Breaker"
                                                               " instances Created, dictionary size full"
                                                          .format(size_limit))

                if not _breakers_map.get(cache_key):
                    # No circuit breaker was found for the given key
                    # Lets create a new circuit breaker instance
                    _breakers_map[cache_key] = \
                        self._new_circuit_breaker(
                            err_threshold=err_threshold,
                            request_volume_window=request_volume_window,
                            reset_timeout=reset_timeout,
                            svc_name=self._service_name,
                            namespace=namespace,
                            exclude=exclude or []
                        )

                breaker = _breakers_map[cache_key]
                return breaker.call(method, *args, **kwargs)

            return _breaker

        return breaker_decorator

    def _new_circuit_breaker(self, err_threshold, request_volume_window,
                             reset_timeout, svc_name, exclude, namespace):
        return pybreaker.CircuitBreaker(
            err_threshold=err_threshold,
            request_volume_window=request_volume_window,
            reset_timeout=reset_timeout,
            svc_name=svc_name,
            exclude=exclude or [],
            listeners=[]
        )

    def _get_namespace(self, suffix=''):
        return '{}-{}'.format(self._service_name, suffix)


class _StatsdMetricReporter(pybreaker.CircuitBreakerListener):
    """
    Internal class to report circuit stats to statsd broker. Implemented
    as a listener in ``pybreaker``
    """

    def __init__(self, *args, **kwargs):
        self._metric_client = kwargs.get('metric_client')
        self._key = kwargs.get('key', '')

    def state_change(self, cb, old_state, new_state):
        if isinstance(new_state, pybreaker.CircuitClosedState):
            self._metric_client.decr(self._key, 1)
        else:
            self._metric_client.incr(self._key, 1)


class _CircuitBreakerStateLogging(pybreaker.CircuitBreakerListener):

    def __init__(self, *args, **kwargs):
        self.namespace = kwargs.get('namespace', "")
        self.logger = kwargs.get('logger')

    def state_change(self, cb, old_state, new_state):
        self.logger.error("circuit breaker state changed",
                         old_state=str(old_state._name),
                         new_state=str(new_state._name),
                         namespace=self.namespace)
