# Copyright 2024 University of Calgary
#
# 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.
"""
Class definition for an AuroraX API request
"""

import json
import requests
import datetime
from typing import Literal, Dict, List, Union
from ....exceptions import AuroraXAPIError, AuroraXUnauthorizedError, AuroraXMaintenanceError
from .response import AuroraXAPIResponse


class AuroraXAPIRequest:
    """
    Class definition for an AuroraX API request

    Attributes:
        url (str): 
            API endpoint URL for the request

        method (str): 
            The HTTP method to use. Valid values are: `get`, `post`, `put`, `delete`, `patch`

        params (Dict): 
            URL parameters to send in the request, defaults to `{}`

        body (Dict): 
            The body of the request (ie. post data), defaults to `{}`

        headers (Dict): 
            Any headers to send as part of the request (in addition to the default ones), defaults to `{}`
            
        null_response (bool): 
            Signifies if we expect a response from the API that has no body/data in it (ie. 
            requests to upload data that respond with just a 202 status code), defaults to `False`
    """

    __API_KEY_HEADER_NAME = "x-aurorax-api-key"

    def __init__(self,
                 aurorax_obj,
                 url: str,
                 method: Literal["get", "post", "put", "delete", "patch"],
                 params: Dict = {},
                 body: Union[List, Dict] = {},
                 headers: Dict = {},
                 null_response: bool = False):
        self.__aurorax_obj = aurorax_obj
        self.url = url
        self.method = method
        self.params = params
        self.body = body
        self.headers = headers
        self.null_response = null_response

    def __json_converter(self, o):
        # NOTE: this method is a last-ditch catch for any datetimes that snuck
        # into the request body without already being converted to string format.
        # Since this should never happen, we exclude it from the test suite
        if (isinstance(o, datetime.datetime) is True):  # pragma: nocover-ok
            return str(o)

    def __merge_headers(self):
        # set initial headers
        all_headers = self.__aurorax_obj.api_headers

        # add headers passed into the class
        #
        # NOTE: we don't usually pass any extra headers, so exclude this from
        # the test suite
        for key, value in self.headers.items():  # pragma: nocover-ok
            all_headers[key.lower()] = value

        # add api key
        if self.__aurorax_obj.api_key:
            all_headers[self.__API_KEY_HEADER_NAME] = self.__aurorax_obj.api_key

        # return
        return all_headers

    def execute(self) -> AuroraXAPIResponse:
        """
        Execute an AuroraX API request
    
        Returns:
            An `pyaurorax.search.api.AuroraXAPIResponse` object

        Raises:
            pyaurorax.exceptions.AuroraXAPIError: Error during API call
            pyaurorax.exceptions.AuroraXUnauthorizedError: Unauthorized user attempted to perform this action
            pyaurorax.exceptions.AuroraXMaintenanceError: AuroraX API is currently undergoing maintenance
        """
        # sanitize data
        body_santized = json.dumps(self.body, default=self.__json_converter)

        # make request
        try:
            req = requests.request(self.method,
                                   self.url,
                                   headers=self.__merge_headers(),
                                   params=self.params,
                                   data=body_santized,
                                   timeout=self.__aurorax_obj.api_timeout)
        except requests.exceptions.Timeout:  # pragma: nocover-ok
            raise AuroraXAPIError("API request timeout reached") from None

        # check if authorization worked (raised by API or Nginx)
        if (req.status_code == 401):  # pragma: nocover-ok
            if (req.headers["Content-Type"] == "application/json"):
                if ("error_message" in req.json()):
                    # this will be an error message that the API meant to send
                    raise AuroraXUnauthorizedError("API error code %d: %s" % (req.status_code, req.json()["error_message"]))
                else:
                    raise AuroraXUnauthorizedError("API error code 401: unauthorized")
            else:
                raise AuroraXUnauthorizedError("API error code 401: unauthorized")

        # check for 404 error (raised by API or by Nginx)
        if (req.status_code == 404):  # pragma: nocover-ok
            if (req.headers["Content-Type"] == "application/json"):
                if ("error_message" in req.json()):
                    # this will be an error message that the API meant to send
                    raise AuroraXAPIError("API error code %d: %s" % (req.status_code, req.json()["error_message"]))
                else:
                    # this will likely be a 404 from the API
                    raise AuroraXAPIError("API error code 404: not found")
            else:
                raise AuroraXAPIError("API error code 404: not found")

        # check for 400
        if (req.status_code == 400):  # pragma: nocover-ok
            raise AuroraXAPIError("API error code %d: %s" % (req.status_code, req.content.decode()))

        # check for server error
        if (req.status_code == 500):  # pragma: nocover-ok
            try:
                response_json = req.json()
                if ("error_message" in response_json):
                    raise AuroraXAPIError("API error code %d: %s" % (req.status_code, response_json["error_message"]))
                else:
                    raise AuroraXAPIError("API error code %d: %s" % (req.status_code, response_json))
            except Exception as e:
                raise AuroraXAPIError("API error code %d: %s" % (req.status_code, req.content)) from e

        # check for maintenance mode error
        if (req.status_code == 502):  # pragma: nocover-ok
            raise AuroraXAPIError("API error code %d: API inaccessible, bad gateway" % (req.status_code))

        # check for maintenance mode error
        if (req.status_code == 503):  # pragma: nocover-ok
            response_json = req.json()
            if ("maintenance mode" in response_json["error_message"].lower()):
                raise AuroraXMaintenanceError(response_json["error_message"])
            else:
                raise AuroraXAPIError("API error code %d: %s" % (req.status_code, response_json["error_message"]))

        # check content type
        if (self.null_response is False):  # pragma: nocover-ok
            if (req.headers["Content-Type"] == "application/json"):
                if (len(req.content) == 0):
                    raise AuroraXAPIError("API error code %d: no response received" % (req.status_code))
                else:
                    response_data = req.json()
            else:
                raise AuroraXAPIError("API error code %d: %s" % (req.status_code, req.content.decode()))
        else:
            if (req.status_code in [200, 201, 202, 204]):
                response_data = None
            else:
                response_data = req.json()

        # create response object
        res = AuroraXAPIResponse(request=req, data=response_data, status_code=req.status_code)

        # return
        return res

    def __str__(self) -> str:
        return self.__repr__()  # pragma: nocover-ok

    def __repr__(self) -> str:
        return f"AuroraXAPIRequest(method='{self.method}', url='{self.url}')"  # pragma: nocover-ok
