"""
This module standarize http traffic and the error handling for further communication with the GOG Galaxy 2.0.

It is recommended to use provided convenient methods for HTTP requests, especially when dealing with authorized sessions.
Examplary simple web service could looks like:

    .. code-block:: python

        import logging
        from galaxy.http import create_client_session, handle_exception

        class BackendClient:
            AUTH_URL = 'my-integration.com/auth'
            HEADERS = {
                "My-Custom-Header": "true",
            }
            def __init__(self):
                self._session = create_client_session(headers=self.HEADERS)

            async def authenticate(self):
                await self._session.request('POST', self.AUTH_URL)

            async def close(self):
                # to be called on plugin shutdown
                await self._session.close()

            async def _authorized_request(self, method, url, *args, **kwargs):
                with handle_exceptions():
                    return await self._session.request(method, url, *args, **kwargs)
"""

import asyncio
import ssl
from contextlib import contextmanager
from http import HTTPStatus

import aiohttp
import certifi
import logging

from galaxy.api.errors import (
    AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
    TooManyRequests, UnknownBackendResponse, UnknownError
)


#: Default limit of the simultaneous connections for ssl connector.
DEFAULT_LIMIT = 20
#: Default timeout in seconds used for client session.
DEFAULT_TIMEOUT = 60


class HttpClient:
    """
    .. deprecated:: 0.41
      Use http module functions instead
    """
    def __init__(self, limit=DEFAULT_LIMIT, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), cookie_jar=None):
        connector = create_tcp_connector(limit=limit)
        self._session = create_client_session(connector=connector, timeout=timeout, cookie_jar=cookie_jar)

    async def close(self):
        """Closes connection. Should be called in :meth:`~galaxy.api.plugin.Plugin.shutdown`"""
        await self._session.close()

    async def request(self, method, url, *args, **kwargs):
        with handle_exception():
            return await self._session.request(method, url, *args, **kwargs)


def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
    """
    Creates TCP connector with resonable defaults.
    For details about available parameters refer to
    `aiohttp.TCPConnector <https://docs.aiohttp.org/en/stable/client_reference.html#tcpconnector>`_
    """
    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ssl_context.load_verify_locations(certifi.where())
    kwargs.setdefault("ssl", ssl_context)
    kwargs.setdefault("limit", DEFAULT_LIMIT)
    # due to https://github.com/python/mypy/issues/4001
    return aiohttp.TCPConnector(*args, **kwargs)  # type: ignore


def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
    """
    Creates client session with resonable defaults.
    For details about available parameters refer to
    `aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_

    Examplary customization:

    .. code-block:: python

        from galaxy.http import create_client_session, create_tcp_connector

        session = create_client_session(
            headers={
                "Keep-Alive": "true"
            },
            connector=create_tcp_connector(limit=40),
            timeout=100)
    """
    kwargs.setdefault("connector", create_tcp_connector())
    kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT))
    kwargs.setdefault("raise_for_status", True)
    # due to https://github.com/python/mypy/issues/4001
    return aiohttp.ClientSession(*args, **kwargs)  # type: ignore


@contextmanager
def handle_exception():
    """
    Context manager translating network related exceptions
    to custom :mod:`~galaxy.api.errors`.
    """
    try:
        yield
    except asyncio.TimeoutError:
        raise BackendTimeout()
    except aiohttp.ServerDisconnectedError:
        raise BackendNotAvailable()
    except aiohttp.ClientConnectionError:
        raise NetworkError()
    except aiohttp.ContentTypeError:
        raise UnknownBackendResponse()
    except aiohttp.ClientResponseError as error:
        if error.status == HTTPStatus.UNAUTHORIZED:
            raise AuthenticationRequired()
        if error.status == HTTPStatus.FORBIDDEN:
            raise AccessDenied()
        if error.status == HTTPStatus.SERVICE_UNAVAILABLE:
            raise BackendNotAvailable()
        if error.status == HTTPStatus.TOO_MANY_REQUESTS:
            raise TooManyRequests()
        if error.status >= 500:
            raise BackendError()
        if error.status >= 400:
            logging.warning(
                "Got status %d while performing %s request for %s",
                error.status, error.request_info.method, str(error.request_info.url)
            )
            raise UnknownError()
    except aiohttp.ClientError:
        logging.exception("Caught exception while performing request")
        raise UnknownError()