# encoding: utf-8

import logging_helper
from ._exceptions import APIError, APIMissing, APIUnresponsive
from .api_support_cache import SupportAPIDualCache

logging = logging_helper.setup_logging()


class APISupportLayer(object):

    """ Base class that provides a common interface to versioned HTTP / WS API's.
        Create one Versioned API support per host:port combination.
    """

    NAME = u''  # You should set this if using caching!

    APIS = []  # List of api modules to make available as part of this support layer (must be populated by subclass)

    DEFAULT_PORT = 443  # Assume HTTPS unless told otherwise
    PATH_PREFIX = u''

    # Custom Exceptions
    API_ERROR = APIError
    API_MISSING = APIMissing
    API_UNRESPONSIVE = APIUnresponsive

    def __init__(self,
                 host,
                 port=None,
                 path_prefix=None,
                 active_api_versions=None,
                 api_parameter_overrides=None):
        """
        :param host:
        :param port:
        :param path_prefix:
        :param active_api_versions:     Dictionary of API name to version number.
                                        This will override the latest and fallback
                                        versions.
        :param api_parameter_overrides: A dict of key value pairs where the key is the api name
                                        to override the host of and the value is the new host.
        """

        if port is None:
            try:
                host, port = host.split(u':')
            except ValueError:
                port = self.DEFAULT_PORT

        if path_prefix is None:
            path_prefix = self.PATH_PREFIX

        # API Root components
        self._host = host
        self._port = port
        self._path_prefix = path_prefix

        self._apis = {}
        self._api_versions = {}
        self._api_mappings = {}
        self._active_api_versions = active_api_versions if active_api_versions else {}

        # Setup cache
        self._cache = SupportAPIDualCache(api_name=self.NAME if self.NAME else self.__class__.__name__)

        self.api_parameters = dict(api_root=self.api_root(),
                                   cache=self._cache,
                                   cache_host=self._host)
        self._api_parameter_overrides = api_parameter_overrides if api_parameter_overrides else {}

        self._load_api_mappings()

    def api_root(self,
                 host_override=None,
                 port_override=None):
        api_parts = [u'{host}:{port}'.format(host=self._host if host_override is None else host_override,
                                             port=self._port if port_override is None else port_override)]

        if self._path_prefix:
            api_parts.append(self._path_prefix)

        return u'/'.join(api_parts)

    def _load_api_mappings(self):
        for api in self.APIS:
            # Register the API
            self.add_api(api.NAME, api.VERSIONS)

    def add_api(self,
                api,
                versions):

        if api in self._api_mappings:
            logging.warning(u'API {api} already registered, Updating API registration'.format(api=api))

        self._api_mappings[api] = versions

    def available_apis(self):
        return self._api_mappings.keys()

    def available_api_versions(self,
                               api):
        return self._api_mappings[api].keys()

    def _get_api_version(self,
                         api):

        """ Implement in subclass for a versioned API.
            If this is not overridden then all API's will get a version = 0

        This method is used to determine the current API version for a specific api.

        :param api: The api to get the version for.
        :return:    The current version number for the API.
                    return 0 if no version found or API is un-versioned.
        """

        return 0

    def api_version(self,
                    api):

        """ This method is used to determine the current API version for a specific api.

        DO NOT OVERRIDE

        :param api: The api to get the version for.
        :return:    The current version number for the API.
                    return 0 if no version found or API is un-versioned.
        """

        try:
            return self._api_versions[api]

        except KeyError:
            pass

        self._api_versions[api] = self._active_api_versions.get(api, self._get_api_version(api))

        return self._api_versions[api]

    def api(self,
            api,
            **params):

        try:
            return self._apis[api]

        except KeyError:
            pass

        real_api_version = self.api_version(api)

        try:
            supported_api_versions = self._api_mappings[api]

        except KeyError:
            raise AttributeError(u'No API named {api}'.format(api=api))

        params.update(self.api_parameters)

        if api in self._api_parameter_overrides:
            logging.info(u'Host override provided for api ({api}): '
                         u'{override}'.format(api=api,
                                              override=self._api_parameter_overrides[api]))

            overrides = self._api_parameter_overrides[api]

            params.update(dict(api_root=self.api_root(host_override=overrides[u'host'],
                                                      port_override=overrides[u'port']),
                               **overrides))

        for api_version in range(real_api_version, -1, -1):
            try:
                versioned_api_class = supported_api_versions[api_version]

                # Pass on exception classes of the API layer to the API
                versioned_api_class.API_ERROR = self.API_ERROR
                versioned_api_class.API_MISSING = self.API_MISSING
                versioned_api_class.API_UNRESPONSIVE = self.API_UNRESPONSIVE

                if real_api_version != api_version:
                    logging.warning(u'API ({api}) v{real_api_version} not available. '
                                    u'Using v{api_version} as fallback.'.format(api=api,
                                                                                real_api_version=real_api_version,
                                                                                api_version=api_version))

                self._apis[api] = versioned_api_class(**params)

                return self._apis[api]

            except KeyError:
                pass

        raise self.API_MISSING(u'API ({api}) v{real_api_version} not available. '
                               u'No fallback version available. '
                               u'Available versions:{available}'.format(api=api,
                                                                        real_api_version=real_api_version,
                                                                        available=supported_api_versions.keys()
                                                                        if supported_api_versions
                                                                        else None))

    def reset_cache(self):
        self._cache.reset_cache()

    def close(self):
        if u'_apis' in vars(self):
            for api in list(self._apis.values()):
                api.close()

    def __del__(self):
        self.close()

    def __getattr__(self,
                    api):
        return self.api(api)

    def __getitem__(self,
                    api):
        return self.api(api)

# encoding: utf-8

import logging_helper
from ._exceptions import APIError, APIMissing, APIUnresponsive
from .api_support_cache import SupportAPIDualCache

logging = logging_helper.setup_logging()


class APISupportLayer(object):

    """ Base class that provides a common interface to versioned HTTP / WS API's.
        Create one Versioned API support per host:port combination.
    """

    NAME = u''  # You should set this if using caching!

    APIS = []  # List of api modules to make available as part of this support layer (must be populated by subclass)

    DEFAULT_PORT = 443  # Assume HTTPS unless told otherwise
    PATH_PREFIX = u''

    # Custom Exceptions
    API_ERROR = APIError
    API_MISSING = APIMissing
    API_UNRESPONSIVE = APIUnresponsive

    def __init__(self,
                 host,
                 port=None,
                 path_prefix=None,
                 active_api_versions=None,
                 api_parameter_overrides=None):
        """
        :param host:
        :param port:
        :param path_prefix:
        :param active_api_versions:     Dictionary of API name to version number.
                                        This will override the latest and fallback
                                        versions.
        :param api_parameter_overrides: A dict of key value pairs where the key is the api name
                                        to override the host of and the value is the new host.
        """

        if port is None:
            try:
                host, port = host.split(u':')
            except ValueError:
                port = self.DEFAULT_PORT

        if path_prefix is None:
            path_prefix = self.PATH_PREFIX

        # API Root components
        self._host = host
        self._port = port
        self._path_prefix = path_prefix

        self._apis = {}
        self._api_versions = {}
        self._api_mappings = {}
        self._active_api_versions = active_api_versions if active_api_versions else {}

        # Setup cache
        self._cache = SupportAPIDualCache(api_name=self.NAME if self.NAME else self.__class__.__name__)

        self.api_parameters = dict(api_root=self.api_root(),
                                   cache=self._cache,
                                   cache_host=self._host)
        self._api_parameter_overrides = api_parameter_overrides if api_parameter_overrides else {}

        self._load_api_mappings()

    def api_root(self,
                 host_override=None,
                 port_override=None):
        api_parts = [u'{host}:{port}'.format(host=self._host if host_override is None else host_override,
                                             port=self._port if port_override is None else port_override)]

        if self._path_prefix:
            api_parts.append(self._path_prefix)

        return u'/'.join(api_parts)

    def _load_api_mappings(self):
        for api in self.APIS:
            # Register the API
            self.add_api(api.NAME, api.VERSIONS)

    def add_api(self,
                api,
                versions):

        if api in self._api_mappings:
            logging.warning(u'API {api} already registered, Updating API registration'.format(api=api))

        self._api_mappings[api] = versions

    def available_apis(self):
        return self._api_mappings.keys()

    def available_api_versions(self,
                               api):
        return self._api_mappings[api].keys()

    def _get_api_version(self,
                         api):

        """ Implement in subclass for a versioned API.
            If this is not overridden then all API's will get a version = 0

        This method is used to determine the current API version for a specific api.

        :param api: The api to get the version for.
        :return:    The current version number for the API.
                    return 0 if no version found or API is un-versioned.
        """

        return 0

    def api_version(self,
                    api):

        """ This method is used to determine the current API version for a specific api.

        DO NOT OVERRIDE

        :param api: The api to get the version for.
        :return:    The current version number for the API.
                    return 0 if no version found or API is un-versioned.
        """

        try:
            return self._api_versions[api]

        except KeyError:
            pass

        self._api_versions[api] = self._active_api_versions.get(api, self._get_api_version(api))

        return self._api_versions[api]

    def api(self,
            api,
            **params):

        try:
            return self._apis[api]

        except KeyError:
            pass

        real_api_version = self.api_version(api)

        try:
            supported_api_versions = self._api_mappings[api]

        except KeyError:
            raise AttributeError(u'No API named {api}'.format(api=api))

        params.update(self.api_parameters)

        if api in self._api_parameter_overrides:
            logging.info(u'Host override provided for api ({api}): '
                         u'{override}'.format(api=api,
                                              override=self._api_parameter_overrides[api]))

            overrides = self._api_parameter_overrides[api]

            params.update(dict(api_root=self.api_root(host_override=overrides[u'host'],
                                                      port_override=overrides[u'port']),
                               **overrides))

        for api_version in range(real_api_version, -1, -1):
            try:
                versioned_api_class = supported_api_versions[api_version]

                # Pass on exception classes of the API layer to the API
                versioned_api_class.API_ERROR = self.API_ERROR
                versioned_api_class.API_MISSING = self.API_MISSING
                versioned_api_class.API_UNRESPONSIVE = self.API_UNRESPONSIVE

                if real_api_version != api_version:
                    logging.warning(u'API ({api}) v{real_api_version} not available. '
                                    u'Using v{api_version} as fallback.'.format(api=api,
                                                                                real_api_version=real_api_version,
                                                                                api_version=api_version))

                self._apis[api] = versioned_api_class(**params)

                return self._apis[api]

            except KeyError:
                pass

        raise self.API_MISSING(u'API ({api}) v{real_api_version} not available. '
                               u'No fallback version available. '
                               u'Available versions:{available}'.format(api=api,
                                                                        real_api_version=real_api_version,
                                                                        available=supported_api_versions.keys()
                                                                        if supported_api_versions
                                                                        else None))

    def reset_cache(self):
        self._cache.reset_cache()

    def close(self):
        if u'_apis' in vars(self):
            for api in list(self._apis.values()):
                api.close()

    def __del__(self):
        self.close()

    def __getattr__(self,
                    api):
        return self.api(api)

    def __getitem__(self,
                    api):
        return self.api(api)

# encoding: utf-8 Base class that provides a common interface to versioned HTTP / WS API's.         Create one Versioned API support per host:port combination. # You should set this if using caching! # List of api modules to make available as part of this support layer (must be populated by subclass) # Assume HTTPS unless told otherwise # Custom Exceptions :param host:         :param port:         :param path_prefix:         :param active_api_versions:     Dictionary of API name to version number.                                         This will override the latest and fallback                                         versions.         :param api_parameter_overrides: A dict of key value pairs where the key is the api name                                         to override the host of and the value is the new host. # API Root components # Setup cache # Register the API Implement in subclass for a versioned API.             If this is not overridden then all API's will get a version = 0          This method is used to determine the current API version for a specific api.          :param api: The api to get the version for.         :return:    The current version number for the API.                     return 0 if no version found or API is un-versioned. This method is used to determine the current API version for a specific api.          DO NOT OVERRIDE          :param api: The api to get the version for.         :return:    The current version number for the API.                     return 0 if no version found or API is un-versioned. # Pass on exception classes of the API layer to the API