import json
from typing import Dict


class CogniteAPIError(Exception):
    """Cognite API Error

    Raised if a given request fails.

    Args:
        message (str):  The error message produced by the API
        code (int):     The error code produced by the failure
        x_request_id (str): The request-id generated for the failed request.
        extra (Dict):   A dict of any additional information.

    Examples:
        Catching an API-error and handling it based on the error code::

            from cognite.client import CogniteClient
            from cognite.client.exceptions import CogniteAPIError

            c = CogniteClient()

            try:
                c.login.status()
            except APIError as e:
                if e.code == 401:
                    print("You are not authorized")
                elif e.code == 400:
                    print("Something is wrong with your request")
                elif e.code == 500:
                    print("Something went terribly wrong. Here is the request-id: {}".format(e.x_request_id)
                print("The message returned from the API: {}".format(e.message))
    """

    def __init__(self, message: str, code: int = None, x_request_id: str = None, extra: Dict = None):
        self.message = message
        self.code = code
        self.x_request_id = x_request_id
        self.extra = extra

    def __str__(self):
        if self.extra:
            pretty_extra = json.dumps(self.extra, indent=4, sort_keys=True)
            return "{} | code: {} | X-Request-ID: {}\n{}".format(
                self.message, self.code, self.x_request_id, pretty_extra
            )
        return "{} | code: {} | X-Request-ID: {}".format(self.message, self.code, self.x_request_id)


class CogniteCompoundAPIError(CogniteAPIError):
    """Cognite Compound API Error

    Raised if one or more of concurrent requests fails. Some items may have been succesfully processed by the API, so this
    exception describes which assets we know have been processed (2xx), which may have been processed (5xx), and which have
    not been proccessed (4xx).
    
    Args:
        successful (List): List of items which were successfully proccessed.
        failed (List): List of items which failed.
        unknown (List): List of items which may or may not have been successfully processed.
    """

    def __init__(self, successful, failed, unknown, unwrap_fn, raised_from: CogniteAPIError):
        super().__init__(message=raised_from.message, code=raised_from.code, extra=raised_from.extra)
        self.successful = successful
        self.failed = failed
        self.unknown = unknown
        self._unwrap_fn = unwrap_fn or (lambda x: x)

    def __str__(self):
        return "The API Failed to process some items.\nSuccessful (2xx): {}\nUnknown (5xx): {}\nFailed (4xx): {}".format(
            [self._unwrap_fn(f) for f in self.successful],
            [self._unwrap_fn(f) for f in self.unknown],
            [self._unwrap_fn(f) for f in self.failed],
        )


class CogniteImportError(Exception):
    """Cognite Import Error

    Raised if the user attempts to use functionality which requires an uninstalled package.

    Args:
        module (str): Name of the module which could not be imported
        message (str): The error message to output.
    """

    def __init__(self, module: str, message: str = None):
        self.module = module
        self.message = message or "The functionality your are trying to use requires '{}' to be installed.".format(
            self.module
        )

    def __str__(self):
        return self.message


class CogniteMissingClientError(Exception):
    """Cognite Missing Client Error

    Raised if the user attempts to make use of a method which requires the cognite_client being set, but it is not.
    """

    def __str__(self):
        return "A CogniteClient has not been set on this object. Pass it in the constructor to use it."
