""" HTTPS-part of the Werk24 client
"""
import urllib.parse
from types import TracebackType
from typing import Dict, Optional, Type

import aiohttp
from pydantic import HttpUrl

from werk24.exceptions import (
    BadRequestException,
    RequestTooLargeException,
    ResourceNotFoundException,
    ServerException,
    UnauthorizedException,
    UnsupportedMediaType,
)
from werk24.models.helpdesk import W24HelpdeskTask
from werk24.models.techread import W24PresignedPost

from .auth_client import AuthClient


class TechreadClientHttps:

    """Translation map from the server response
    to the W24TechreadArchitectureStatus enum
    """

    def __init__(self, techread_version: str, support_base_url: str):
        """
        Initialize a new session with the https server.

        Arguments:
            techread_server_https {str} -- Domain of the Techread https server
            techread_version {str} -- Techread Version
            support_base_url {str} -- Base URL for support requests
        """
        self._techread_version = techread_version
        self._techread_session_https: Optional[aiohttp.ClientSession] = None
        self._auth_client: Optional[AuthClient] = None
        self._support_base_url = support_base_url

    async def __aenter__(self) -> "TechreadClientHttps":
        """Create a new HTTP session that is being used for the whole
        connection. Be sure to keep the session alive.

        Raises:
            RuntimeError  -- Raise when the developer enters the session
                without having called register_auth_client()

        Returns:
            TechreadClientHttps -- TechreadClientHttps version with active
                session
        """
        self._techread_session_https = aiohttp.ClientSession()
        return self

    async def __aexit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> None:
        """Close the session"""
        if self._techread_session_https is not None:
            await self._techread_session_https.close()

    def register_auth_client(self, auth_client: AuthClient) -> None:
        """Register the reference to the authentication service

        Arguments:
            auth_client {AuthClient} -- Reference to Authentication
                client
        """
        self._auth_client = auth_client

    async def upload_associated_file(
        self, presigned_post: W24PresignedPost, content: Optional[bytes]
    ) -> None:
        """Upload an associated file to the API.
        This can either be a technical drawing or a
        3D model. Potentially we will sometime extend
        this to also include cover pages.

        NOTE: the complete message size must not be
        larger than 10 MB

        Arguments:
            request_id {str} -- UUID4 request id that you obtained
                from the websocket

            filetype {str} -- filetype that we want to upload.
                currently supported: drawing, model

            content {bytes} -- content of the file as bytes

        Raises:

            BadRequestException: Raised when the request body
                cannot be interpreted. This normally indicates
                that the API version has been updated and that
                we missed a corner case. If you encounter this
                exception, it is very likely our mistake. Please
                get in touch!

            UnauthorizedException: Raised when the token
                or the requested file have expired

            ResourceNotFoundException: Raised when you are requesting
                an endpoint that does not exist. Again, you should
                not encounter this, but if you do, let us know.

            RequestTooLargeException: Raised when the status
                code was 413

            UnsupportedMediaTypException: Raised when the file you
                submitted cannot be read (because its media type
                is not supported by the API).

            ServerException: Raised for all other status codes
                that are not 2xx
        """
        # ignore if payload is empty
        if content is None:
            return

        # generate the form data by merging the presigned
        # fields with the file
        form = aiohttp.FormData()
        for key, value in presigned_post.fields_.items():
            form.add_field(key, value)
        form.add_field("file", content)

        # create a new fresh session that does not
        # carry the authentication token
        async with aiohttp.ClientSession() as sess:
            async with sess.post(str(presigned_post.url), data=form) as resp:
                # check the status code of the response and
                # raise the appropriate exception
                self._raise_for_status(str(presigned_post.url), resp.status)

    async def download_payload(self, payload_url: HttpUrl) -> bytes:
        """Return the payload from the server

        Arguments:
            payload_url {HttpUrl} -- Url of the payload

        Raises:
            RuntimeError: Hard Error that is raised when
                the function is asked to download a payload
                from an untrusted source.
                This provides some sort of protection against
                payload-injection and token-theft. When you
                see this error showing up, you should
                definitely INVESTIGATE AND LET US KNOW
                IMMEDIATELY!!!
                Call all our numbers on a Sunday morning
                at 3am if it must be. Even if its Christmas
                and Easter on the same day.

            BadRequestException: Raised when the request body
                cannot be interpreted. This normally indicates
                that the API version has been updated and that
                we missed a corner case. If you encounter this
                exception, it is very likely our mistake. Please
                get in touch!

            UnauthorizedException: Raised when the token
                or the requested file have expired

            ResourceNotFoundException: Raised when you are requesting
                an endpoint that does not exist. Again, you should
                not encounter this, but if you do, let us know.

            RequestTooLargeException: Raised when the status
                code was 413

            UnsupportedMediaTypException: Raised when the file you
                submitted cannot be read (because its media type
                is not supported by the API).

            ServerException: Raised for all other status codes
                that are not 2xx
        Returns:
            bytes -- Payload
        """
        # send the get request to the endpoint
        try:
            response = await self._get(payload_url)

        # reraise the exceptions
        except (
            UnauthorizedException,  # pylint: disable=try-except-raise
            RequestTooLargeException,
            ServerException,
            BadRequestException,
            ResourceNotFoundException,
        ):
            raise

        # otherwise return the response text
        return await response.content.read()

    async def _get(self, url: str) -> aiohttp.ClientResponse:
        """Send a GET request request and return the
        response object. The method automatically
        injects the authentication token into the
        request.

        Arguments:
            url {str} -- URL that is to be requested

        Raises:
            BadRequestException: Raised when the request body
                cannot be interpreted. This normally indicates
                that the API version has been updated and that
                we missed a corner case. If you encounter this
                exception, it is very likely our mistake. Please
                get in touch!

            UnauthorizedException: Raised when the token
                or the requested file have expired

            ResourceNotFoundException: Raised when you are requesting
                an endpoint that does not exist. Again, you should
                not encounter this, but if you do, let us know.

            RequestTooLargeException: Raised when the status
                code was 413

            UnsupportedMediaTypException: Raised when the file you
                submitted cannot be read (because its media type
                is not supported by the API).

            ServerException: Raised for all other status codes
                that are not 2xx

        Returns:
            aiohttp.ClientResponse -- Client response for the get request
        """

        # ensure that the session was started
        if self._techread_session_https is None:
            raise RuntimeError("You executed a command without opening a session")

        # send the request
        response = await self._techread_session_https.get(str(url))

        # check the status code of the response and
        # raise the appropriate exception
        try:
            self._raise_for_status(url, response.status)
        except (UnauthorizedException, ServerException) as exception:
            raise exception

        # if the call was successful, return
        return response

    @staticmethod
    def _raise_for_status(url: str, status_code: int) -> None:
        """Raise the correct exception depending on the
        status code

        Arguments:
            url {str} - - requested url
            status_code {int} - - response status code

        Raises:
            BadRequestException: Raised when the request body
                cannot be interpreted. This normally indicates
                that the API version has been updated and that
                we missed a corner case. If you encounter this
                exception, it is very likely our mistake. Please
                get in touch!

            UnauthorizedException: Raised when the token
                or the requested file have expired

            ResourceNotFoundException: Raised when you are requesting
                an endpoint that does not exist. Again, you should
                not encounter this, but if you do, let us know.

            RequestTooLargeException: Raised when the status
                code was 413

            UnsupportedMediaTypException: Raised when the file you
                submitted cannot be read(because its media type
                is not supported by the API).

            ServerException: Raised for all other status codes
                that are not 2xx

        """

        # raise a bad request exception if the status
        # code 400 was returned. This normally indicates
        # that the API has been updated and the integration
        # tests have missed a case
        if status_code == 400:
            raise BadRequestException()

        # raise an unauthorized exception if the
        # status code is
        # * 401 (Unauthorized) or
        # * 403 (Forbidden)
        if status_code in {401, 403}:
            raise UnauthorizedException()

        # NOTE: a 404 does not occur, as the
        # server does not want to tell you
        # whether the file does not exist
        # or whether your token is wrong.
        # Makes brute force attacks more expensive.
        # We deal with it anyway so we can change
        # in the future
        if status_code == 404:
            raise ResourceNotFoundException()

        # if the status code is 413, you have submitted
        # a file that is too large.
        if status_code == 413:
            raise RequestTooLargeException()

        # if the status code is 415, you have submitted
        # a file whose media type is not supported by the API
        if status_code == 415:
            raise UnsupportedMediaType()

        # If the resposne code is anything other
        # than unauthorized or 200 (OK), we trigger
        # a ServerException.
        if not 200 <= status_code <= 299:
            raise ServerException(f"Request failed '{url}' with code {status_code}")

    async def create_helpdesk_task(self, task: W24HelpdeskTask) -> W24HelpdeskTask:
        """
        Create a Helpdesk ticket.

        Args:
            task (W24HelpdeskTask): Helpdesk task to be created

        Raises:
            BadRequestException: Raised when the request body
                cannot be interpreted. This normally indicates
                that the API version has been updated and that
                we missed a corner case. If you encounter this
                exception, it is very likely our mistake. Please
                get in touch!

            UnauthorizedException: Raised when the token
                or the requested file have expired

            ResourceNotFoundException: Raised when you are requesting
                an endpoint that does not exist. Again, you should
                not encounter this, but if you do, let us know.

            RequestTooLargeException: Raised when the status
                code was 413

            UnsupportedMediaTypException: Raised when the file you
                submitted cannot be read(because its media type
                is not supported by the API).

            ServerException: Raised for all other status codes
                that are not 2xx

        Returns:
            W24HelpdeskTask: Created helpdesk task with an updated task_id.
        """
        headers = self._make_helpdesk_headers()
        url = self._make_support_url("helpdesk/create-task")
        async with aiohttp.ClientSession(headers=headers) as session:
            response = await session.post(url, json=task.json())
            self._raise_for_status(url, response.status)

        # return the updated task
        return W24HelpdeskTask.parse_raw(await response.text())

    def _make_support_url(self, path: str) -> str:
        """Make the support url for the help desk requests.

        Arguments:
            path {str} -- Path to the endpoint

        Returns:
            str -- URL to the endpoint
        """
        return urllib.parse.urljoin(f"https://{self._support_base_url}", path)

    def _make_helpdesk_headers(self) -> Dict[str, str]:
        """
        Make the headers for the help desk requests.

        Simply the authorization header at this stage.

        Returns:
            Dict[str, str]: Help desk headers
        """
        headers = {"Authorization": f"Bearer {self._auth_client.token}"}
        return headers
