# Standard library imports
import collections
import datetime
from json import loads
import logging
import os
from threading import Lock
from typing import Callable, Dict, List, Tuple, Type, Union


# Local imports
from .api import developer_analytics
from .api.developer_analytics.rest import ApiException as DeveloperAnalyticsException
from .api.developer_key_management.rest import (
    ApiException as DeveloperKeyManagementException,
)
from .error import Error
from .multiton import Multiton
from .rates import Rates
from .reference import Reference
from .token import ApplicationToken, UserToken, KeyPairToken


class APIPrivate(metaclass=Multiton):
    """
    This base class encapsulates the private code and data related to the internal operations of the API.
    It is designed to abstract away the implementation details that are unlikely to be of interest
    to end users, focusing instead on internal functionality.

    Key features:
    - **Private Code Encapsulation**: This class is intended to be used internally by the system and
      not directly by the end user. It handles private data and methods, providing a clean interface
      for higher-level API operations.

    - **Separation of Handcrafted and Autogenerated Code**:
      The class distinguishes between manually written code (handcrafted) and code generated by tools
      (e.g., Swagger/OpenAPI). This separation improves maintainability by making it easier to
      update or regenerate the autogenerated code without affecting custom logic.

    Use case:
    - The `APIPrivate` class is typically instantiated by internal components and used to manage
      lower-level API interactions. It is not designed for direct interaction by end users or API clients.

    """

    # TODO Improve efficiency, unique objects could be created in parallel.
    # Use these to mitigate the duplication risk that Mutition has while treading.
    _lock_rates = Lock()
    _lock_key_pair_token = Lock()
    _lock_user_token = Lock()
    _lock_application_token = Lock()

    def __init__(
        self,
        path: str or None = None,
        application: str or dict or None = None,
        user: str or dict or None = None,
        header: str or dict or None = None,
        throttle: bool or None = False,
        timeout: float or None = -1.0,
        key_pair: str or dict or None = None,
        digital_signatures: bool or None = False,
        async_req: bool or None = False,
    ) -> None:
        """
        VERY IMPORTANT:
        - Except for unit tests, never use this base class directly; during normal use, it is meant to be inherited.
        - Avoid making backward-incompatible changes to the constructor’s signature, as this could break end user code.
        - To ensure proper type hinting and documentation for end users:
         a. Method parameters must be an exact copy from the API class __init__ method.
         b. After the dashed line, this docstring should be an exact copy from the API __init__ method.

        ===================================================================================

        Instantiate an API object, then use it to call hundreds of eBay APIs.

        Load credentials from an ebay_rest.json file or supply dicts that mimic records in said file.
        See https://github.com/matecsaj/ebay_rest/blob/main/tests/ebay_rest_EXAMPLE.json.
        Warning, hard coding credentials in code is a security risk.

        :param path (str, optional):
        If using an ebay_rest.json file that is not in the current working directory, supply a full path.

        :param application (str or dict, optional) :
        Supply the name of the desired application record in ebay_rest.json or a dict with application credentials.
        Can omit when ebay_rest.json contains only one application record.

        :param user (str or dict, optional) :
        Supply the name of the desired user record in ebay_rest.json or a dict with user credentials.
        Can omit when ebay_rest.json contains only one user record.

        :param header (str or dict, optional) :
        Supply the name of the desired header record in ebay_rest.json or a dict with header credentials.
        Can omit when ebay_rest.json contains only one header record.

        :param
        throttle (bool, optional) : When True block the call if a below the prorated call limit, defaults to False.
                                    Note, the sandbox has no call limits.

        :param
        timeout (float, optional) : When invoked with the floating-point timeout argument set to a positive value,
        throttle for at most the number of seconds specified by timeout and as below the prorated call limit. A timeout
        argument of -1 specifies an unbounded wait. It is forbidden to specify a timeout when the throttle is False.
        Defaults to -1.

        :param
        key_pair (str or dict, optional) :
        Supply the name of the desired eBay public/private key pair record in ebay_rest.json or a dict with
        the key pair details.
        Can omit when ebay_rest.json contains only one record.

        :param
        digital_signatures (bool, optional): Use eBay digital signatures

        :param
        async_req (bool, optional) : When True make asynchronous HTTP requests, defaults to False for synchronous.
        !!!IGNORE THIS OPTION, THE CODE FOR IT IS INCOMPLETE!!!

        :return (object) : An API object.
        """
        # if present, load the configuration file
        config_contents = None
        if path is None:
            path = os.getcwd()
        self._config_location = os.path.join(path, "ebay_rest.json")
        if os.path.isfile(self._config_location):
            try:
                with open(self._config_location, "r") as f:
                    config_contents = loads(f.read())
            except IOError as e:
                raise Error(
                    number=99001,
                    reason="Unable to open " + self._config_location,
                    cause=e,
                )
        else:
            if not (
                isinstance(application, dict)
                and isinstance(user, dict)
                and isinstance(header, dict)
            ):
                detail = (
                    "Either at "
                    + self._config_location
                    + " do this https://github.com/matecsaj/ebay_rest/blob/main/tests/ebay_rest_EXAMPLE.json."
                    + " Or, supply dicts to all of the params application, user, and header."
                )
                raise Error(
                    number=99015,
                    reason="Missing configuration information.",
                    detail=detail,
                )

        # get configuration sections from parameters or the loaded file
        self._application = self._process_config_section(
            config_contents, "applications", application
        )
        self._user = self._process_config_section(config_contents, "users", user)
        self._header = self._process_config_section(config_contents, "headers", header)
        self._key_pair = self._process_config_section(
            config_contents, "key_pairs", key_pair, mandatory=False
        )
        self._use_digital_signatures = digital_signatures

        # check the application keys and values
        # True if the dictionary key is required and False when optional.
        # Nothing appears to use dev_id! What should it be used for?
        application_keys = [
            ("app_id", True),
            ("cert_id", True),
            ("dev_id", False),
            ("redirect_uri", True),
        ]
        self._check_keys(self._application, application_keys, "application")

        # check the user keys and values
        user_keys = [
            ("email_or_username", True),
            ("password", True),
            ("scopes", False),
            ("refresh_token", False),
            ("refresh_token_expiry", False),
        ]
        self._check_keys(self._user, user_keys, "user")

        # check the key pair keys and values
        key_pair_keys = [
            ("creation_time", False),
            ("expiration_time", False),
            ("jwe", False),
            ("private_key", False),
            ("public_key", False),
            ("signing_key_cipher", False),
            ("signing_key_id", False),
        ]
        self._check_keys(self._key_pair, key_pair_keys, "key_pair")

        # Determine if we are using the sandbox. Of course production is the only alternative.
        self._sandbox = self._application["cert_id"].startswith("SBX-")

        # check the header keys and values

        # get valid marketplace ids
        self._marketplace_ids = list(Reference.get_marketplace_id_values())

        # get all the languages that marketplaces use
        marketplace_languages = set()
        for marketplace_id_value in Reference.get_marketplace_id_values().values():
            for key in marketplace_id_value[1]:
                marketplace_languages.add(key)
        marketplace_languages = list(marketplace_languages)

        header_keys_values = [
            ("accept_language", marketplace_languages),
            (
                "affiliate_campaign_id",
                None,
            ),  # None indicates that any value is acceptable.
            ("affiliate_reference_id", None),
            ("device_id", None),
            ("content_language", marketplace_languages),
            ("country", list(Reference.get_country_codes().keys())),
            ("currency", list(Reference.get_currency_codes().keys())),
            ("marketplace_id", self._marketplace_ids),
            ("zip", None),
        ]
        self._check_header(header_keys_values)

        # check the throttle parameters
        detail = None
        if throttle not in (True, False):
            detail = "Parameter throttle must be unspecified, True or False."
        else:
            self._throttle = throttle
        if not isinstance(timeout, float):
            detail = "Parameter timeout must be unspecified or a float."
        elif timeout != -1.0 and timeout <= 0.0:
            detail = "Parameter timeout must be unspecified, -1 or positive."
        else:
            self._timeout = timeout
        if detail:
            raise Error(
                number=99002, reason="Bad throttling parameters.", detail=detail
            )

        # check the async_req parameter
        if async_req not in (True, False):
            detail = (
                f"Parameter async_req {async_req} must be unspecified, True or False."
            )
            raise Error(number=99016, reason="Bad async_req parameter.", detail=detail)
        else:
            self._async_req = async_req

        if (
            self._sandbox
        ):  # The sandbox will not return rates so there is no point to doing throttling.
            self._throttle = False
            self._timeout = -1.0
            self._rates = None
        else:
            with APIPrivate._lock_rates:
                # If sandbox starts return rates, you will need to add a sandbox param to the Rates constructor.
                self._rates = Rates(app_id=self._application["app_id"])

        # preload the multipurpose header self._end_user_ctx
        equates = list()
        if "affiliate_campaign_id" in self._header:
            if len(self._header["affiliate_campaign_id"]) > 0:
                equates.append(
                    "affiliateCampaignId=" + self._header["affiliate_campaign_id"]
                )
        if "affiliate_reference_id" in self._header:
            if len(self._header["affiliate_reference_id"]) > 0:
                equates.append(
                    "affiliateReferenceId=" + self._header["affiliate_reference_id"]
                )
        if "country" in self._header:
            if len(self._header["country"]) > 0:
                equates.append("contextualLocation=country=" + self._header["country"])
                if "zip" in self._header:
                    if len(self._header["zip"]) > 0:
                        equates.append(",zip=" + self._header["zip"])
        if "device_id" in self._header:
            if len(self._header["device_id"]) > 0:
                equates.append("deviceId=" + self._header["device_id"])
        if equates:
            self._end_user_ctx = "".join(equates)
        else:
            self._end_user_ctx = None

        with APIPrivate._lock_application_token:
            self._application_token = ApplicationToken(
                self._sandbox,
                # application/client credentials
                client_id=self._application["app_id"],
                client_secret=self._application["cert_id"],
                ru_name=self._application["redirect_uri"],
            )

        with APIPrivate._lock_user_token:
            self._user_token = UserToken(
                self._sandbox,
                # application/client credentials
                client_id=self._application["app_id"],
                client_secret=self._application["cert_id"],
                ru_name=self._application["redirect_uri"],
                # user credentials
                user_id=self._user["email_or_username"],
                user_password=self._user["password"],
                user_scopes=(
                    None if "scopes" not in self._user else self._user["scopes"]
                ),
                # user token supply
                user_refresh_token=(
                    None
                    if "refresh_token" not in self._user
                    else self._user["refresh_token"]
                ),
                user_refresh_token_expiry=(
                    None
                    if "refresh_token_expiry" not in self._user
                    else self._user["refresh_token_expiry"]
                ),
            )

        with APIPrivate._lock_key_pair_token:
            self._key_pair_token = KeyPairToken(
                creation_time=self._key_pair.get("creation_time", None),
                expiration_time=self._key_pair.get("expiration_time", None),
                jwe=self._key_pair.get("jwe", None),
                private_key=self._key_pair.get("private_key", None),
                public_key=self._key_pair.get("public_key", None),
                signing_key_cipher=self._key_pair.get("signing_key_cipher", None),
                signing_key_id=self._key_pair.get("signing_key_id", None),
            )

        return

    @staticmethod
    def _process_config_section(
        config_contents: dict,
        section: str,
        parameter: str or dict or None,
        mandatory: bool = True,
    ) -> dict or None:
        """
        Get a configuration section from the parameter or the loaded config file.

        :param config_contents (dict, required)
        :param section (str, required)
        :param parameter (str or dict or None, required)
        :param mandatory (bool, optional)
        :return result (dict or None)
        """
        result = None
        detail = None
        param_name = section[:-1]

        if isinstance(parameter, dict):
            result = parameter

        elif isinstance(parameter, str):
            if len(parameter) == 0:
                detail = (
                    "Empty strings are not allowed for the "
                    + param_name
                    + " parameter."
                )

            if config_contents:
                if section in config_contents:
                    if parameter in config_contents[section]:
                        result = config_contents[section][parameter]
                    else:
                        detail = (
                            "Unable to find section "
                            + parameter
                            + " in the configuration file."
                        )
                else:
                    detail = (
                        "Section "
                        + section
                        + " is missing from the configuration file."
                    )
            else:
                detail = (
                    "The parameter "
                    + param_name
                    + " should not be a string or the configuration file should exist."
                )

        elif parameter is None:
            if config_contents:
                if section in config_contents:
                    sections = config_contents[section].keys()
                    if len(sections) == 1:
                        result = config_contents[section][tuple(sections)[0]]
                    else:
                        detail = (
                            "Perhaps parameter "
                            + param_name
                            + " should be one of "
                            + ", ".join(sections)
                            + "."
                        )
                else:
                    detail = (
                        "The parameter "
                        + param_name
                        + " should not be none or section "
                        + section
                        + " is missing from the configuration file."
                    )
            else:
                detail = (
                    "The parameter "
                    + param_name
                    + " should not be None or the configuration file should exist."
                )

        else:
            detail = (
                "Parameter "
                + param_name
                + " must be a Dict, String or None but it is a "
                + str(type(parameter))
                + "."
            )

        if result is None:
            if mandatory:
                raise Error(
                    number=99003,
                    reason="Get configuration for " + param_name + " problem.",
                    detail=detail,
                )
            else:
                return {}
        else:
            # delete blank lines, to eliminate subsequent blank line checks
            to_delete = []
            for key in result:
                if isinstance(result[key], str):
                    if len(result[key].strip()) == 0:
                        to_delete.append(key)
            for key in to_delete:
                del result[key]

            return result

    @staticmethod
    def _check_keys(dict_: dict, keys: List[Tuple[str, bool]], name: str) -> None:
        """
        True if the dictionary key is required and False when optional.
        :param dict_ (dict, required)
        :param keys (List[Tuple[str, bool]], required)
        :param name (str, required)
        :return None (None)
        """
        valid_keys = []
        for key, required in keys:
            valid_keys.append(key)

            if key in dict_:
                if key == "scopes":
                    if not isinstance(dict_[key], list):
                        raise Error(
                            number=99004,
                            reason="The key's value must be a list.",
                            detail=key + " in " + name,
                        )
                else:
                    if not isinstance(dict_[key], str):
                        raise Error(
                            number=99005,
                            reason="The key's value must be a string.",
                            detail=key + " in " + name,
                        )
            if required:
                if key in dict_:
                    if len(dict_[key]) == 0:
                        raise Error(
                            number=99006,
                            reason="The key's value can not be of zero length.",
                            detail=key + " in " + name,
                        )
                else:
                    raise Error(
                        number=99007,
                        reason="A required key missing",
                        detail=key + " in " + name,
                    )

        for key in dict_:
            if key not in valid_keys:
                raise Error(
                    number=99008,
                    reason="Found an unexpected key.",
                    detail=key + " in " + name,
                )

    def _check_header(
        self, keys_values: List[Union[Tuple[str, list], Tuple[str, None]]]
    ) -> None:
        """
        Check header keys and values.
        :param keys_values (List[Union[Tuple[str, list], Tuple[str, None]]], required)
        """
        header = self._header
        valid_keys = []
        for key, values in keys_values:
            valid_keys.append(key)
            if key in header:
                if isinstance(header[key], str):
                    if values is not None:
                        if header[key] not in values:
                            values.sort()
                            detail = (
                                "Header key "
                                + key
                                + " has value "
                                + header[key]
                                + ". Choose from "
                                + ", ".join(values)
                                + "."
                            )
                            raise Error(
                                number=99009,
                                reason="Invalid header value.",
                                detail=detail,
                            )
                else:
                    raise Error(
                        number=99010,
                        reason="Header values must be strings.",
                        detail="Check key " + key + ".",
                    )

        for key in header:
            if key not in valid_keys:
                raise Error(number=99011, reason="Unexpected header key.", detail=key)

    def _method_single(
        self,
        function_configuration: callable,
        base_path: str,
        function_instance: type,
        function_client: callable or type,
        method: str,
        swagger_method_exception: Type[Exception],
        user_access_token: bool,
        rate_keys: List[str],
        params: Tuple[str] or str or None,
        **kwargs: Dict[str, int],
    ) -> collections:
        """Do the work for method that returns a single object.

        :param function_configuration (callable, required):
        :param base_path (str, required):
        :param function_instance (type, required):
        :param function_client (class or type, required):
        :param method (str, required):
        :param swagger_method_exception (Type[Exception], required):
        :param user_access_token (bool, required):
        :param rate_keys (List[str]: required:
        :param params: (Tuple[str] or str or None, required)
        :param kwargs: (Dict[str, int], required)
        :return object: (collections)
        """
        swagger_method = self._get_swagger_method(
            function_configuration,
            base_path,
            function_instance,
            function_client,
            method,
            user_access_token,
            params,
        )

        self._swagger_throttle(base_path=base_path, rate_keys=rate_keys)

        return self._call_swagger(
            swagger_method, params, kwargs, swagger_method_exception
        )

    def _method_paged(
        self,
        function_configuration: callable,
        base_path: str,
        function_instance: type,
        function_client: Callable or type,
        method: str,
        swagger_method_exception: Type[Exception],
        user_access_token: bool,
        rate_keys: List[str],
        params: Tuple[str] or str or None,
        **kwargs,  # TODO Is it wrong to put Dict[str, int] or is PyCharm's warning system buggy?
    ) -> collections:
        """Do the work for method that yields objects from repeated calls which is termed Paging by eBay.

        Across all pages, eBay has a hard limit on how many records it will return. This is subject to change
        and can vary by call. The swagger call will raise an exception at the limit. 10,000 is a common hard limit.

        :param function_configuration (callable, required):
        :param base_path (str, required):
        :param function_instance (type, required):
        :param function_client (Callable or type, required):
        :param method (str, required):
        :param swagger_method_exception (Type[Exception], required):
        :param params (Tuple[str] or str or None, required):
        :param kwargs (Dict[str, int], required):
        :return collection (collections):
        """
        page_controls = [
            "href",
            "limit",
            "next",
            "offset",
            "prev",
            "total",
            "warnings",
            "refinement",
        ]
        page_limit = 200  # the maximum number of records per page, as dictated by eBay
        yield_record_count = 0
        yielded_info = list()

        # co-opt some parameters
        if "offset" in kwargs:
            raise Error(
                number=99012,
                reason="Don't supply an offset parameter. It is automatically handled.",
            )

        if "limit" in kwargs:
            if not isinstance(kwargs["limit"], int):
                reason = (
                    "The limit must be an integer, you supplied a "
                    + str(type(kwargs["limit"]))
                    + "."
                )
                raise Error(number=99013, reason=reason)
            records_desired = kwargs["limit"]
            if records_desired <= 0:
                reason = (
                    "The limit must be greater that zero, you supplied "
                    + str(records_desired)
                    + "."
                )
                raise Error(number=99014, reason=reason)
            if records_desired < page_limit:
                kwargs["limit"] = (
                    records_desired  # just get enough records to satisfy what is desired
                )
            else:
                kwargs["limit"] = (
                    page_limit  # fill pages with as many records as possible
                )
        else:
            records_desired = None  # the user wants all possible records
            kwargs["limit"] = page_limit  # fill pages with as many records as possible

        swagger_method = self._get_swagger_method(
            function_configuration,
            base_path,
            function_instance,
            function_client,
            method,
            user_access_token,
            params,
        )

        # loop though pages until a reason to stop presents itself
        offset = 0  # start at the first record; yes the record index starts at zero
        record_list_key = None  # a placeholder for the dictionary key that will refer to the list of records
        loop = True
        result = None
        while loop:
            self._swagger_throttle(base_path=base_path, rate_keys=rate_keys)

            kwargs["offset"] = offset  # get the next page of results
            # TODO If the caller does not process all yielded results within five minutes, the token might expire.
            result = self._call_swagger(
                swagger_method, params, kwargs, swagger_method_exception
            )

            if result is None:
                break

            if (
                record_list_key is None
            ):  # if still needed, find the dictionary key to the list of results
                for key in result:
                    if isinstance(result[key], list):
                        record_list_key = key
                        page_controls.append(key)
                        break
                    # it will not be found when the record set is totally empty

            # Yield unique non-record information.
            for key in result:
                if key not in page_controls:
                    if result[key] is not None:
                        yield_info = {key: result[key]}
                        if yield_info not in yielded_info:
                            # TODO For some 'warnings' handle the problem or raise an exception.
                            yield yield_info
                            yielded_info.append(yield_info)

            # Determine the number of records in the current page.
            records_in_page = 0
            if record_list_key is not None:  # is the record set totally empty?
                if record_list_key in result:  # is the current page a well-formed?
                    if (
                        result[record_list_key] is not None
                    ):  # does the current page have > zero results?
                        records_in_page = len(
                            result[record_list_key]
                        )  # all good, get the record count

            # Yield each record then stop looping and prepare the next page's offset.
            if records_in_page:
                for element in result[record_list_key]:
                    yield {"record": element}
                    yield_record_count += 1
                    if records_desired is not None:
                        records_desired -= 1
                        if records_desired <= 0:
                            loop = False
                            break
                offset += records_in_page
                if result["total"] <= offset:
                    loop = False
            else:
                loop = False

        # Warning, if the caller stopped this generator prematurely then the following will not happen.
        if result is None:
            yield_max = 0
        else:
            yield_max = result["total"]
        yield {
            "total": {
                "records_yielded": yield_record_count,
                "records_available": yield_max,
            }
        }

    def _get_swagger_method(
        self,
        function_configuration: callable,
        base_path: str,
        function_instance: type,
        function_client: callable or type,
        method: str,
        user_access_token: bool,
        params: Tuple[str] or str or None,
    ) -> callable:
        """
        Get a callable Swagger method that is ready to use.

        :param function_configuration (Callable, required):
        :param base_path (str, required):
        :param function_instance (type, required):
        :param function_client (Callable or type, required):
        :param method (str, required):
        :param user_access_token (bool, required):
        :param params (Tuple[str] or str or None, required):
        :return function (callable)
        """
        # Configure OAuth2 access token for authorization: api_auth
        configuration = function_configuration()
        if user_access_token:
            configuration.access_token = self._user_token.get()
        else:
            configuration.access_token = self._application_token.get()

        # Load key pair for digital signature
        use_digital_signatures = (
            self._use_digital_signatures and "key_management" not in base_path
        )
        if use_digital_signatures:
            self._key_pair_token._ensure_key_pair(self)
            configuration.api_key["key_pair"] = self._key_pair_token.key_dict()

        # Configure the host endpoint
        if self._sandbox:
            configuration.host = configuration.host.replace(
                ".ebay.com", ".sandbox.ebay.com"
            )
        # check for flawed host and if so compensate
        if "{basePath}" in configuration.host:
            configuration.host = configuration.host.replace("{basePath}", base_path)
        else:
            logging.debug(
                "eBay or Swagger has fixed the flaw so remove the compensating code."
            )

        # create an instance of the API class
        api_instance = function_instance(function_client(configuration))

        # The request headers that eBay accepts are mostly described here.
        # https://developer.ebay.com/api-docs/static/rest-request-components.html#headers

        # Accept, Accept-Charset & Accept-Encoding
        # Do nothing because the Swagger generated code handles them.

        # Accept-Language
        if self._header["accept_language"]:
            api_instance.api_client.default_headers["Accept-Language"] = self._header[
                "accept_language"
            ]

        # Authorization
        # Do nothing because the Swagger generated code handles it.

        # Digital signatures
        # Add 'x-ebay-enforce-signature', and then the rest is handled in the modified Swagger code.
        if use_digital_signatures:
            api_instance.api_client.default_headers["x-ebay-enforce-signature"] = "true"

        # Content-Language
        if self._header["content_language"]:
            api_instance.api_client.default_headers["Content-Language"] = self._header[
                "content_language"
            ]

        # X-EBAY-C-MARKETPLACE-ID
        marketplace_id = self._header["marketplace_id"]
        # some calls have a positional parameter for this, and the header must use the same value
        if user_access_token:  # all such calls happen to require a user access token
            # TODO instead it would be safer to check if 'method' is in a list of ones that belong
            for param in params or []:
                if param in self._marketplace_ids:
                    marketplace_id = param
        api_instance.api_client.default_headers["X-EBAY-C-MARKETPLACE-ID"] = (
            marketplace_id
        )

        # X-EBAY-C-ENDUSERCTX
        # beware that the site_id is a bit different for the Buy API
        # https://developer.ebay.com/api-docs/buy/static/ref-marketplace-supported.html

        # header for shipping information accuracy per https://developer.ebay.com/api-docs/buy/static/api-browse.html
        if "/buy/browse" in base_path and self._end_user_ctx:
            api_instance.api_client.default_headers["X-EBAY-C-ENDUSERCTX"] = (
                self._end_user_ctx
            )

        # return the callable function
        return getattr(api_instance, method)

    def _swagger_throttle(self, base_path: str, rate_keys: list) -> None:
        """Block when the swagger method is below it's prorated call limit.

        Call this just before calling a swagger method. Only do for the first paging call.

        :param base_path (str, required):
        :param rate (list, required) : Strings, keys used to look up a rate
        """
        if not self._sandbox:  # eBay does not limit calls to the sandbox
            # the base_path check prevents endless recursive calls to self.developer_analytics_get_rate_limits()
            if not self._throttle or base_path.startswith("/developer/analytics"):
                self._rates.decrement_rate(base_path=base_path, rate_keys=rate_keys)
            else:
                # if rates need to be refreshed, then do so.
                if self._rates.need_refresh():
                    limits = self.copy_of_developer_analytics_get_rate_limits()
                    self._rates.refresh_developer_analytics(
                        rate_limits=limits["rate_limits"]
                    )

                    # decrement the rate, throttling if needed
                    self._rates.decrement_rate_throttled(
                        base_path=base_path, rate_keys=rate_keys, timeout=self._timeout
                    )

    def copy_of_developer_analytics_get_rate_limits(self, **kwargs):
        """
        TO AVOID CIRCULAR IMPORTS, here in is a copy the API class's method developer_analytics_get_rate_limits.
        """
        return self._method_single(
            developer_analytics.Configuration,
            "/developer/analytics/v1_beta",
            developer_analytics.RateLimitApi,
            developer_analytics.ApiClient,
            "get_rate_limits",
            DeveloperAnalyticsException,
            False,
            ["developer.analytics", "rate_limit"],
            None,
            **kwargs,
        )  # noqa: E501

    def _de_swagger(self, obj: collections):
        """Take a Swagger data object and return the Python styled equivalent.

        There is a Java vibe to objects returned from Swagger generated code and other issues:
        1. non-eBay attributes are meaningless Swagger artifacts,
        2. public attributes should not have leading underscores,
        3. the class objects are effectively dicts,
        4. class names are in CamelCase.

        Do a "deep copy" and fix problems along the way.

        To learn more about Swagger, visit https://swagger.io.

        :param obj (collections, required): The object returned by a Swagger call.
        :return obj (collections)
        """
        basic_types = (
            bool,
            bytes,
            datetime.date,
            datetime.datetime,
            float,
            int,
            type(None),
            str,
        )  # omitted ones that are unlikely to ever be used

        if type(obj) in basic_types:  # leaf node
            return obj

        elif obj.__class__.__module__ != "builtins":  # a user defined class object?
            new_dict = dict()
            for attr, value in obj.__dict__.items():
                if attr not in [
                    "attribute_map",
                    "discriminator",
                    "swagger_types",
                ]:  # skip Swagger specific
                    success = True

                    # cope with Swagger presenting public attributes as private
                    if not attr.startswith("_") or attr.startswith("__"):
                        logging.debug(
                            "A public or dunder is not anticipated, did Swagger fix the problem?"
                        )
                        new_value = (
                            None  # this pointless line is here to avoid a lint warning
                        )
                        success = False
                    else:
                        attr = attr[1:]  # remove the single leading underscore

                        if type(value) in basic_types:
                            new_value = value

                        elif isinstance(value, list):
                            new_value = list()
                            for element in value:
                                new_value.append(self._de_swagger(element))

                        elif (
                            value.__class__.__module__ != "builtins"
                        ):  # a user defined class object?
                            new_value = self._de_swagger(value)

                        else:
                            logging.debug(
                                "Need to handle attribute "
                                + attr
                                + " of type "
                                + type(value)
                                + "."
                            )
                            new_value = None  # this pointless line is here to avoid a lint warning
                            success = False

                    if success:
                        new_dict[attr] = new_value

            return new_dict

        else:
            logging.debug("Unexpected object of type " + type(obj) + ".")
            return obj  # something needs to be returned, hopefully it is useful as is

    def _call_swagger(
        self,
        swagger_method: Callable,
        params: Tuple[str] or str or None,
        kwargs: Dict[str, Dict[str, int]],
        swagger_method_exception: Type[Exception],
    ) -> collections:
        """
        Call the API method generated by Swagger and tidy the result.

        :param swagger_method (Callable, required)
        :param params (Tuple[str] or str or None, required)
        :param kwargs (Dict[str, Dict[str, int]], required)
        :param swagger_method_exception (Type[Exception, required)
        :return collection (collections)
        """
        # Swagger defaults to False, only add the key word argument if need be.
        if self._async_req:
            kwargs["async_req"] = self._async_req
        try:
            if params:
                if isinstance(params, tuple):
                    if kwargs:
                        api_response = swagger_method(*params, **kwargs)
                    else:
                        api_response = swagger_method(*params)
                else:
                    if kwargs:
                        api_response = swagger_method(params, **kwargs)
                    else:
                        api_response = swagger_method(params)
            else:
                if kwargs:
                    api_response = swagger_method(**kwargs)
                else:
                    api_response = swagger_method()

        except swagger_method_exception as e:
            # error.status will be 100 to 599, see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
            raise Error(
                number=99000 + e.status, reason=e.reason, detail=e.body, cause=e
            )

        except DeveloperKeyManagementException as e:
            raise Error(
                number=99018,
                reason="A Digital Signature problem.",
                detail=f"{e}",
                cause=e,
            )

        else:
            if (
                self._async_req
            ):  # TODO Wait for the asynchronous HTTP request to finish.
                detail = (
                    f"Don't use async_req=True; the feature is currently incomplete."
                )
                raise Error(
                    number=99017, reason="Bad async_req parameter.", detail=detail
                )
            return self._de_swagger(api_response)

    def get_digital_signature_key(self, create_new=False):
        """Load the details of the current public/private key pair suitable for
        entering into ebay_rest.json or as an API parameter.

        If create_new is True, creates a new public/private key pair if
        (and only if) required. Otherwise, if no private key is supplied,
        return an error.
        """
        if not self._use_digital_signatures:
            raise Error(
                number=99018,
                reason="Digital Signatures not enabled",
                detail="Set digital_signatures=True when creating API instance",
            )

        if not self._key_pair_token._has_valid_key(self):
            if create_new:
                self._key_pair_token._create_key_pair(self)
            else:
                raise Error(
                    number=99019,
                    reason="New key pair needed",
                    detail="get_digital_signature_key parameter create_new parameter must be True",
                )
        key = self._key_pair_token._load_key()
        return key
