from typing import Any, Dict, Mapping, Optional, Union

from fastapi import HTTPException, status
from pydantic import BaseModel, Field


# Inspired by: https://cloud.google.com/apis/design/errors
class ProblemDetails(BaseModel):
    code: Optional[int] = Field(
        None,
        ge=100,
        lt=600,
        example=400,
        description="The HTTP status code generated by the origin server for this occurrence of the problem.",
    )
    message: Optional[str] = Field(
        None,
        # example="Service Unavailable",
        description="A developer-facing human-readable error message in English.",
    )
    explanation: Optional[str] = Field(
        None,
        # example="Service Unavailable",
        description="A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.",
    )
    details: Optional[Any] = Field(
        None,
        description="A map of additional problem details.",
    )


# Based on RFC 7807 Standard: https://tools.ietf.org/html/rfc7807
# https://opensource.zalando.com/restful-api-guidelines/problem-1.0.1.yaml
# class ProblemDetailsFormat(BaseModel):
#     typ(e): str = Field(
#         "about:blank",
#         # example="problem/connection-error",
#         description="A relative URI reference that uniquely identifies the problem type only in the context of the provided API. In combination with the API endpoint, this might point to further documentation.",
#     )
#     status: Optional[int] = Field(
#         None,
#         ge=100,
#         lt=600,
#         # example=503,
#         description="The HTTP status code generated by the origin server for this occurrence of the problem.",
#     )
#     title: Optional[str] = Field(
#         None,
#         # example="Service Unavailable",
#         description="A short summary of the problem type. Written in English and readable for engineers, usually not suited for non technical stakeholders and not localized.",
#     )
#     detail: Optional[str] = Field(
#         None,
#         # example="Connection to database timed out",
#         description="A human readable explanation specific to this occurrence of the problem that is helpful to locate the problem and give advice on how to proceed. Written in English and readable for engineers, usually not suited for non technical stakeholders and not localized.",
#     )
#     instance: Optional[str] = Field(
#         None,
#         description="A relative URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.",
#     )
#     metadata: Optional[Dict] = Field(
#         None,
#         description="A map of additional problem details/metadata.",
#     )


class ClientBaseError(HTTPException):
    """Basic exception class for all errors that should be shown to the client/user.

    The error details will be shown to the client (user) if it is not handled otherwise.
    """

    def __init__(
        self,
        status_code: int,
        message: str,
        explanation: Optional[str] = None,
        metadata: Optional[Dict] = None,
        resource: Optional[str] = None,
    ) -> None:
        """Initializes the exception.

        Args:
            status_code: The HTTP status code associated with the error.
            message: A short summary of the error.
            explanation (optional): A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.
            metadata (optional): Additional problem details/metadata.
            resource (optional): A resource name (relative URI reference) of a specific resource instance associated with the error.
        """

        super(ClientBaseError, self).__init__(
            status_code=status_code,
            detail=message,
        )

        self.explanation = explanation
        self.metadata = metadata
        self.resource = resource


class ServerBaseError(Exception):
    """Basic exception class for all server internal errors that should not be shown with details to the user.

    If the error is not handled, an `Internal Server Error` (Status Code 500) will be shown
    to the client (user) without any additional details. In this case, the exception will be
    automatically logged.
    """

    def __init__(self, *args: object) -> None:
        """Initializes the exception.

        Args:
            args: A collection of
        """
        super(ServerBaseError, self).__init__(*args)


class UnauthenticatedError(ClientBaseError):
    """Client error that indicates invalid, expired, or missing authentication credentials.

    The error message should contain specific details about the problem, e.g.:

    - No access token was provided.

    The error details will be shown to the client (user) if it is not handled otherwise.
    """

    _HTTP_STATUS_CODE = status.HTTP_401_UNAUTHORIZED
    _DEFAULT_MESSAGE = "Invalid authentication credentials."
    _DEFAULT_EXPLANATION = "This can happen because credentials or token are missing, expired, or otherwise invalid."

    def __init__(
        self,
        message: Optional[str] = None,
        explanation: Optional[str] = None,
        metadata: Optional[Dict] = None,
    ) -> None:
        """Initializes the error.

        Args:
            message (optional): A message shown to the user that overwrites the default message.
            explanation (optional): A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.
            metadata (optional): Additional problem details/metadata.
        """
        super(UnauthenticatedError, self).__init__(
            status_code=UnauthenticatedError._HTTP_STATUS_CODE,
            message=message or UnauthenticatedError._DEFAULT_MESSAGE,
            explanation=explanation or UnauthenticatedError._DEFAULT_EXPLANATION,
            metadata=metadata,
        )


class PermissionDeniedError(ClientBaseError):
    """Client error that indicates that a client does not have sufficient permission for the request.

    The error message should contain specific details about the resource, e.g.:

    - Permission 'xxx' denied on resource 'yyy'.

    The error details will be shown to the client (user) if it is not handled otherwise.
    """

    _HTTP_STATUS_CODE = status.HTTP_403_FORBIDDEN
    _DEFAULT_MESSAGE = "The authorized user does not have sufficient permission."
    _DEFAULT_EXPLANATION = "This can happen because the token does not have the right scopes or the user doesn't have the required permission."

    def __init__(
        self,
        message: Optional[str] = None,
        explanation: Optional[str] = None,
        metadata: Optional[Dict] = None,
        resource: Optional[str] = None,
    ) -> None:
        """Initializes the error.

        Args:
            message (optional): A message shown to the user that overwrites the default message.
            explanation (optional): A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.
            metadata (optional): Additional problem details/metadata.
            resource (optional): A resource name (relative URI reference) of a specific resource instance associated with the error.
        """
        super(PermissionDeniedError, self).__init__(
            status_code=PermissionDeniedError._HTTP_STATUS_CODE,
            message=message or PermissionDeniedError._DEFAULT_MESSAGE,
            explanation=explanation or PermissionDeniedError._DEFAULT_EXPLANATION,
            metadata=metadata,
            resource=resource,
        )


class ResourceNotFoundError(ClientBaseError):
    """Client error that indicates that a specified resource is not found.

    The error message should contain specific details about the resource, e.g.:

    - Resource 'xxx' not found.

    The error details will be shown to the client (user) if it is not handled otherwise.
    """

    _HTTP_STATUS_CODE = status.HTTP_404_NOT_FOUND
    _DEFAULT_MESSAGE = "A specified resource is not found."

    def __init__(
        self,
        message: Optional[str] = None,
        explanation: Optional[str] = None,
        metadata: Optional[Dict] = None,
        resource: Optional[str] = None,
    ) -> None:
        """Initializes the error.

        This error should be raised if

        Args:
            message (optional): A message shown to the user that overwrites the default message.
            explanation (optional): A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.
            metadata (optional): Additional problem details/metadata.
            resource (optional): A resource name (relative URI reference) of a specific resource instance associated with the error.
        """
        super(ResourceNotFoundError, self).__init__(
            status_code=ResourceNotFoundError._HTTP_STATUS_CODE,
            message=message or ResourceNotFoundError._DEFAULT_MESSAGE,
            explanation=explanation,
            metadata=metadata,
            resource=resource,
        )


class ResourceAlreadyExistsError(ClientBaseError):
    """Client error that indicates that a resource that a client tried to create already exists.

    The error message should contain specific details about the problem and resource, e.g.:

    - Resource 'xxx' already exists.
    - Couldn’t acquire lock on resource ‘xxx’.

    The error details will be shown to the client (user) if it is not handled otherwise.
    """

    _HTTP_STATUS_CODE = status.HTTP_409_CONFLICT
    _DEFAULT_MESSAGE = "The resource already exists."

    def __init__(
        self,
        message: Optional[str] = None,
        explanation: Optional[str] = None,
        metadata: Optional[Dict] = None,
        resource: Optional[str] = None,
    ) -> None:
        """Initializes the error.

        Args:
            message (optional): A message shown to the user that overwrites the default message.
            explanation (optional): A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.
            metadata (optional): Additional problem details/metadata.
            resource (optional): A resource name (relative URI reference) of a specific resource instance associated with the error.
        """
        super(ResourceAlreadyExistsError, self).__init__(
            status_code=ResourceAlreadyExistsError._HTTP_STATUS_CODE,
            message=message or ResourceAlreadyExistsError._DEFAULT_MESSAGE,
            explanation=explanation,
            metadata=metadata,
        )


class ClientValueError(ClientBaseError, ValueError):
    """Client error that indicates that the client input is invalid.

    The error message should contain specific details about the problem, e.g.:

    - Request field x.y.z is xxx, expected one of [yyy, zzz].
    - Parameter 'age' is out of range [0, 125].

    The error details will be shown to the client (user) if it is not handled otherwise.
    """

    _HTTP_STATUS_CODE = status.HTTP_400_BAD_REQUEST
    _DEFAULT_MESSAGE = "An invalid argument was specified by the client. "

    def __init__(
        self,
        message: Optional[str] = None,
        explanation: Optional[str] = None,
        metadata: Optional[Dict] = None,
        resource: Optional[str] = None,
    ) -> None:
        """Initializes the error.

        Args:
            message (optional): A message shown to the user that overwrites the default message.
            explanation (optional): A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.
            metadata (optional): Additional problem details/metadata.
            resource (optional): A resource name (relative URI reference) of a specific resource instance associated with the error.
        """
        super(ClientValueError, self).__init__(
            status_code=ClientValueError._HTTP_STATUS_CODE,
            message=message or ClientValueError._DEFAULT_MESSAGE,
            explanation=explanation,
            metadata=metadata,
            resource=resource,
        )


class ResourceUpdateFailedError(ClientBaseError):
    """Client error that indicates that a requested update for a resource failed.

    The error message should contain specific details about the problem and resource, e.g.:

    - Unable to apply patch update for resource 'xxx'.

    The error details will be shown to the client (user) if it is not handled otherwise.
    """

    _HTTP_STATUS_CODE = status.HTTP_409_CONFLICT
    _DEFAULT_MESSAGE = "Resource update failed. Try again."
    _DEFAULT_EXPLANATION = "The requested resource updated could not be applied. The reason could concurrent or conflicting modifications to the resource. Please try the update again."

    def __init__(
        self,
        message: Optional[str] = None,
        explanation: Optional[str] = None,
        metadata: Optional[Dict] = None,
        resource: Optional[str] = None,
    ) -> None:
        """Initializes the error.

        Args:
            message (optional): A message shown to the user that overwrites the default message.
            explanation (optional): A human readable explanation specific to this error that is helpful to locate the problem and give advice on how to proceed.
            metadata (optional): Additional problem details/metadata.
            resource (optional): A resource name (relative URI reference) of a specific resource instance associated with the error.
        """
        super(ResourceUpdateFailedError, self).__init__(
            status_code=ResourceUpdateFailedError._HTTP_STATUS_CODE,
            message=message or ResourceUpdateFailedError._DEFAULT_MESSAGE,
            explanation=explanation or ResourceUpdateFailedError._DEFAULT_EXPLANATION,
            metadata=metadata,
            resource=resource,
        )


CREATE_RESOURCE_RESPONSES: Mapping[Union[int, str], Dict[str, Any]] = {
    status.HTTP_409_CONFLICT: {
        "description": "The resource already exists.",
        "model": ProblemDetails,
    }
}

GET_RESOURCE_RESPONSES: Mapping[Union[int, str], Dict[str, Any]] = {
    status.HTTP_404_NOT_FOUND: {
        "description": "A specified resource is not found.",
        "model": ProblemDetails,
    }
}

UPDATE_RESOURCE_RESPONSES: Mapping[Union[int, str], Dict[str, Any]] = {
    **GET_RESOURCE_RESPONSES,
    status.HTTP_409_CONFLICT: {
        "description": "Resource update failed.",
        "model": ProblemDetails,
    },
}

AUTH_ERROR_RESPONSES: Mapping[Union[int, str], Dict[str, Any]] = {
    status.HTTP_401_UNAUTHORIZED: {
        "description": "Invalid authentication credentials.",
        "model": ProblemDetails,
    },
    status.HTTP_403_FORBIDDEN: {
        "description": "The authorized user does not have sufficient permission.",
        "model": ProblemDetails,
    },
}

VALIDATION_ERROR_RESPONSE: Mapping[Union[int, str], Dict[str, Any]] = {
    status.HTTP_422_UNPROCESSABLE_ENTITY: {
        "description": "Validation Error.",
        "model": ProblemDetails,
    },
}
