# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""HTTP Exceptions used by keystoneauth1."""

import typing as ty

import requests

from keystoneauth1.exceptions import auth
from keystoneauth1.exceptions import base


__all__ = (
    'HttpError',
    'HTTPClientError',
    'BadRequest',
    'Unauthorized',
    'PaymentRequired',
    'Forbidden',
    'NotFound',
    'MethodNotAllowed',
    'NotAcceptable',
    'ProxyAuthenticationRequired',
    'RequestTimeout',
    'Conflict',
    'Gone',
    'LengthRequired',
    'PreconditionFailed',
    'RequestEntityTooLarge',
    'RequestUriTooLong',
    'UnsupportedMediaType',
    'RequestedRangeNotSatisfiable',
    'ExpectationFailed',
    'UnprocessableEntity',
    'HttpServerError',
    'InternalServerError',
    'HttpNotImplemented',
    'BadGateway',
    'ServiceUnavailable',
    'GatewayTimeout',
    'HttpVersionNotSupported',
    'from_response',
)


class HttpError(base.ClientException):
    """The base exception class for all HTTP exceptions."""

    http_status = 0
    message = "HTTP Error"

    def __init__(
        self,
        message: ty.Optional[str] = None,
        details: ty.Optional[str] = None,
        response: ty.Optional[requests.Response] = None,
        request_id: ty.Optional[str] = None,
        url: ty.Optional[str] = None,
        method: ty.Optional[str] = None,
        http_status: ty.Optional[int] = None,
        retry_after: int = 0,
    ):
        self.http_status = http_status or self.http_status
        self.message = message or self.message
        self.details = details
        self.request_id = request_id
        self.response = response
        self.url = url
        self.method = method
        formatted_string = f"{self.message} (HTTP {self.http_status})"
        self.retry_after = retry_after
        if request_id:
            formatted_string += f" (Request-ID: {request_id})"
        super().__init__(formatted_string)


class HTTPClientError(HttpError):
    """Client-side HTTP error.

    Exception for cases in which the client seems to have erred.
    """

    message = "HTTP Client Error"


class HttpServerError(HttpError):
    """Server-side HTTP error.

    Exception for cases in which the server is aware that it has
    erred or is incapable of performing the request.
    """

    message = "HTTP Server Error"


class BadRequest(HTTPClientError):
    """HTTP 400 - Bad Request.

    The request cannot be fulfilled due to bad syntax.
    """

    http_status = 400
    message = "Bad Request"


class Unauthorized(HTTPClientError):
    """HTTP 401 - Unauthorized.

    Similar to 403 Forbidden, but specifically for use when authentication
    is required and has failed or has not yet been provided.
    """

    http_status = 401
    message = "Unauthorized"


class PaymentRequired(HTTPClientError):
    """HTTP 402 - Payment Required.

    Reserved for future use.
    """

    http_status = 402
    message = "Payment Required"


class Forbidden(HTTPClientError):
    """HTTP 403 - Forbidden.

    The request was a valid request, but the server is refusing to respond
    to it.
    """

    http_status = 403
    message = "Forbidden"


class NotFound(HTTPClientError):
    """HTTP 404 - Not Found.

    The requested resource could not be found but may be available again
    in the future.
    """

    http_status = 404
    message = "Not Found"


class MethodNotAllowed(HTTPClientError):
    """HTTP 405 - Method Not Allowed.

    A request was made of a resource using a request method not supported
    by that resource.
    """

    http_status = 405
    message = "Method Not Allowed"


class NotAcceptable(HTTPClientError):
    """HTTP 406 - Not Acceptable.

    The requested resource is only capable of generating content not
    acceptable according to the Accept headers sent in the request.
    """

    http_status = 406
    message = "Not Acceptable"


class ProxyAuthenticationRequired(HTTPClientError):
    """HTTP 407 - Proxy Authentication Required.

    The client must first authenticate itself with the proxy.
    """

    http_status = 407
    message = "Proxy Authentication Required"


class RequestTimeout(HTTPClientError):
    """HTTP 408 - Request Timeout.

    The server timed out waiting for the request.
    """

    http_status = 408
    message = "Request Timeout"


class Conflict(HTTPClientError):
    """HTTP 409 - Conflict.

    Indicates that the request could not be processed because of conflict
    in the request, such as an edit conflict.
    """

    http_status = 409
    message = "Conflict"


class Gone(HTTPClientError):
    """HTTP 410 - Gone.

    Indicates that the resource requested is no longer available and will
    not be available again.
    """

    http_status = 410
    message = "Gone"


class LengthRequired(HTTPClientError):
    """HTTP 411 - Length Required.

    The request did not specify the length of its content, which is
    required by the requested resource.
    """

    http_status = 411
    message = "Length Required"


class PreconditionFailed(HTTPClientError):
    """HTTP 412 - Precondition Failed.

    The server does not meet one of the preconditions that the requester
    put on the request.
    """

    http_status = 412
    message = "Precondition Failed"


class RequestEntityTooLarge(HTTPClientError):
    """HTTP 413 - Request Entity Too Large.

    The request is larger than the server is willing or able to process.
    """

    http_status = 413
    message = "Request Entity Too Large"

    def __init__(
        self,
        message: ty.Optional[str] = None,
        details: ty.Optional[str] = None,
        response: ty.Optional[requests.Response] = None,
        request_id: ty.Optional[str] = None,
        url: ty.Optional[str] = None,
        method: ty.Optional[str] = None,
        http_status: ty.Optional[int] = None,
        retry_after: int = 0,
    ):
        try:
            self.retry_after = int(retry_after)
        except (KeyError, ValueError):
            self.retry_after = 0

        super().__init__(
            message=message,
            details=details,
            response=response,
            request_id=request_id,
            url=url,
            method=method,
            http_status=http_status,
        )


class RequestUriTooLong(HTTPClientError):
    """HTTP 414 - Request-URI Too Long.

    The URI provided was too long for the server to process.
    """

    http_status = 414
    message = "Request-URI Too Long"


class UnsupportedMediaType(HTTPClientError):
    """HTTP 415 - Unsupported Media Type.

    The request entity has a media type which the server or resource does
    not support.
    """

    http_status = 415
    message = "Unsupported Media Type"


class RequestedRangeNotSatisfiable(HTTPClientError):
    """HTTP 416 - Requested Range Not Satisfiable.

    The client has asked for a portion of the file, but the server cannot
    supply that portion.
    """

    http_status = 416
    message = "Requested Range Not Satisfiable"


class ExpectationFailed(HTTPClientError):
    """HTTP 417 - Expectation Failed.

    The server cannot meet the requirements of the Expect request-header field.
    """

    http_status = 417
    message = "Expectation Failed"


class UnprocessableEntity(HTTPClientError):
    """HTTP 422 - Unprocessable Entity.

    The request was well-formed but was unable to be followed due to semantic
    errors.
    """

    http_status = 422
    message = "Unprocessable Entity"


class InternalServerError(HttpServerError):
    """HTTP 500 - Internal Server Error.

    A generic error message, given when no more specific message is suitable.
    """

    http_status = 500
    message = "Internal Server Error"


# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
    """HTTP 501 - Not Implemented.

    The server either does not recognize the request method, or it lacks
    the ability to fulfill the request.
    """

    http_status = 501
    message = "Not Implemented"


class BadGateway(HttpServerError):
    """HTTP 502 - Bad Gateway.

    The server was acting as a gateway or proxy and received an invalid
    response from the upstream server.
    """

    http_status = 502
    message = "Bad Gateway"


class ServiceUnavailable(HttpServerError):
    """HTTP 503 - Service Unavailable.

    The server is currently unavailable.
    """

    http_status = 503
    message = "Service Unavailable"


class GatewayTimeout(HttpServerError):
    """HTTP 504 - Gateway Timeout.

    The server was acting as a gateway or proxy and did not receive a timely
    response from the upstream server.
    """

    http_status = 504
    message = "Gateway Timeout"


class HttpVersionNotSupported(HttpServerError):
    """HTTP 505 - HttpVersion Not Supported.

    The server does not support the HTTP protocol version used in the request.
    """

    http_status = 505
    message = "HTTP Version Not Supported"


# _code_map contains all the classes that have http_status attribute.
_code_map = {
    400: BadRequest,
    401: Unauthorized,
    402: PaymentRequired,
    403: Forbidden,
    404: NotFound,
    405: MethodNotAllowed,
    406: NotAcceptable,
    407: ProxyAuthenticationRequired,
    408: RequestTimeout,
    409: Conflict,
    410: Gone,
    411: LengthRequired,
    412: PreconditionFailed,
    413: RequestEntityTooLarge,
    414: RequestUriTooLong,
    415: UnsupportedMediaType,
    416: RequestedRangeNotSatisfiable,
    417: ExpectationFailed,
    422: UnprocessableEntity,
    500: InternalServerError,
    501: HttpNotImplemented,
    502: BadGateway,
    503: ServiceUnavailable,
    504: GatewayTimeout,
    505: HttpVersionNotSupported,
}


def from_response(
    response: requests.Response, method: str, url: str
) -> ty.Union[HttpError, auth.MissingAuthMethods]:
    """Return an instance of :class:`HttpError` or subclass based on response.

    :param response: instance of `requests.Response` class
    :param method: HTTP method used for request
    :param url: URL used for request
    """
    req_id = response.headers.get("x-openstack-request-id")

    message = None
    details = None

    retry_after = 0
    if "retry-after" in response.headers:
        try:
            retry_after = int(response.headers["retry-after"])
        except ValueError:
            pass

    content_type = response.headers.get("Content-Type", "")
    if content_type.startswith("application/json"):
        try:
            body = response.json()
        except ValueError:
            pass
        else:
            if isinstance(body, dict) and isinstance(body.get("error"), dict):
                message = body["error"].get("message")
                details = body["error"].get("details")
            elif isinstance(body, dict) and isinstance(
                body.get("errors"), list
            ):
                # if the error response follows the API SIG guidelines, it
                # will return a list of errors. in this case, only the first
                # error is shown, but if there are multiple the user will be
                # alerted to that fact.
                errors = body["errors"]
                if len(errors) == 0:
                    # just in case we get an empty array
                    message = None
                    details = None
                else:
                    if len(errors) > 1:
                        # if there is more than one error, let the user know
                        # that multiple were seen.
                        msg_hdr = (
                            "Multiple error responses, showing first only: "
                        )
                    else:
                        msg_hdr = ""

                    message = "{}{}".format(msg_hdr, errors[0].get("title"))
                    details = errors[0].get("detail")
            else:
                message = "Unrecognized schema in response body."
    elif content_type.startswith("text/"):
        details = response.text

    # we check explicity for 401 in case of auth receipts
    if (
        response.status_code == 401
        and "Openstack-Auth-Receipt" in response.headers
    ):
        return auth.MissingAuthMethods(response)

    try:
        cls = _code_map[response.status_code]
    except KeyError:
        if 500 <= response.status_code < 600:
            cls = HttpServerError
        elif 400 <= response.status_code < 500:
            cls = HTTPClientError
        else:
            cls = HttpError

    return cls(
        http_status=response.status_code,
        response=response,
        method=method,
        url=url,
        request_id=req_id,
        retry_after=retry_after,
        message=message,
        details=details,
    )
