"""
Defines a transport client. A transport client is an
abstract http client
"""
import collections
import inspect
import json
import logging
import time
from urlparse import urlparse

import gtransport._utils as _utils
from gtransport._executor import basic_executor
from gtransport.constants import ERROR_THRESHOLD, REQUEST_VOLUME_WINDOW, RESET_TIMEOUT

LOGGER = logging.getLogger(__name__)


class TransportClient(object):
    """
    Represents an abstract bounded http client. We call it bounded
    because it will append a few configured headers with every request
    it sends. This gives convenience to pass headers like `request-id`,
    which is used to trace a request or a header like `service`, used
    to identify the service making the request.

    @todo: update this docstrings
    """

    _executor = None
    _provider = None

    def __init__(self, **kwargs):
        self._executor = kwargs.get('executor') or basic_executor
        self._provider = kwargs['provider']
        self._extra_headers = kwargs.get('extra_headers') or {}

    def update_extra_headers(self, retain=True, **headers):
        """
        Updates any extra headers to be appended to the request

        @param retain: bool, optional flag to determine if any
        old headers present are to be preserved. Defaults to True
        """
        headers = headers or {}
        if not bool(retain):
            self._extra_headers = {}
        self._extra_headers.update(**headers)

    def _get_service_name(self):
        svc_name = self._extra_headers.get('Service') or 'default_svc'
        return svc_name

    @staticmethod
    def _generate_key(url):
        """
        Generates a unique key for failsafe instance
        @param url: str, url includes method, scheme and netloc
        generated by urlparse lib
        url = 'https.get.xyz.example.com'
        Lets say we are in this file,
        promotions/services/espina.py
        and if we make a call like
            def recommend_products(user_id, **kwargs):
                gtransport.get(url)
        then,
        func_name = recommend_products
        module_name = services.espina
        @return: str, unique key formed
        key = https.get.xyz.example.com.services.espina.recommend_products
        """
        frame = inspect.currentframe()
        request_source = frame.f_back.f_back.f_back
        code_object = request_source.f_code
        module_name = "module_name"
        try:
            module_name = inspect.getmodule(request_source).__name__
        except AttributeError:
            LOGGER.warning("FailSafe: Using default module name - %s", module_name)

        func_name = code_object.co_name
        LOGGER.info("func_name:{} and module_name:{}".format(func_name, module_name))
        separator = '::'
        key = separator.join((url, module_name, func_name))
        return key

    @staticmethod
    def _parse_url(method, url):
        """
        Make a URL, which will contribute for forming unique key
        unique key will be used to make failsafe instance
        @param method: str, the HTTP verb
        @param url: str, the URL
            method => get
            url => 'https://xyz.example.com/report/archive/rt'
        Sample:
            for above args
            scheme = 'https'
            netloc = 'xyz.example.com'
        @return: str, URL formed with provided params
            url = 'https.get.xyz.example.com'
        """
        parsed_url = urlparse(url)
        separator = '::'
        url = separator.join((parsed_url.scheme, method, parsed_url.netloc))
        return url

    def send(self, method, url, provider=None, **kwargs):
        """
        Makes an HTTP request to the external service
        @param method: str, the HTTP verb
        @param url: str, the URL
        @param provider: Any, the low level http client, any
        object with a method: `def request(method, url, **kwargs)`,
        will do.
        @param kwargs:
        @return: same as the response from the `provider`
        """
        # Put any additional headers to be forwarded
        headers = kwargs.get('headers') or {}
        self._decorate_headers(headers)
        kwargs['headers'] = headers
        provider = provider or self._provider
        svc_name = self._get_service_name()
        parsed_url = self._parse_url(method, url)
        unique_key = kwargs.pop('key', self._generate_key(parsed_url))
        # popping out these keys is must as we cant pass these to
        # request library
        err_threshold = kwargs.pop('err_threshold', ERROR_THRESHOLD)
        request_volume_window = kwargs.pop(
            'request_volume_window', REQUEST_VOLUME_WINDOW
        )
        reset_timeout = kwargs.pop('reset_timeout', RESET_TIMEOUT)
        LOGGER.info('FailSafe key is {} for service {}'.format(unique_key, svc_name))
        return self._executor(
            func=lambda: self._request(provider, method, url, **kwargs),
            key=unique_key,
            svc_name=svc_name,
            err_threshold=err_threshold,
            request_volume_window=request_volume_window,
            reset_timeout=reset_timeout,
        )

    def _decorate_headers(self, headers):
        """Appends any additional headers to the request"""
        for name, value in self._extra_headers.items():
            headers.setdefault(name, str(value))

    @staticmethod
    def _request(provider, method, url, **kwargs):
        """
        Invokes the `request(...)` method of the given
        provider and returns the response. It also logs the
        request and response objects based on the logging level.
        """
        # dict holding contextual information to be logged
        debug = collections.OrderedDict(method=method, url=url, headers=kwargs.get('headers'))
        # Holds the result of the http call
        result = None
        LOGGER.info(
            'Upstream Request: %s'
            % json.dumps(debug, default=lambda x: repr(x))
        )
        start_time = time.time()
        try:
            result = provider.request(method, url, **kwargs)
            return result
        except Exception as ex:
            debug['exception_type'] = type(ex).__name__
            raise ex
        finally:
            end_time = time.time()
            debug.update(
                response_status=result.status_code if result else None,
                request_elapsed_time=result.elapsed.total_seconds() if result else None,
                time_taken=str(end_time - start_time)[:8],
            )

            # If debug log is enabled add request and response body
            if LOGGER.isEnabledFor(logging.DEBUG):
                debug.update(request_body=kwargs.get('data') or kwargs.get('json'))
                debug.update(response_body=result.text if result else None)
                debug.update(response_headers=result.headers if result else None)

            LOGGER.info(
                'Upstream Response: %s'
                % json.dumps(debug, default=lambda x: repr(x))
            )


class TransportClientSupplier(object):
    """A factory for the TransportClient"""

    def __init__(self, **kwargs):
        self._kw = kwargs or {}

    def __call__(self, *args, **kwargs):
        """
        Returns a new TransportClient bounded with the given context
        """
        return TransportClient(**self._kw)
