# -*- coding: utf-8 -*-

"""Simplistic thread-safe HTTP sessions."""

__all__ = ['SessionOptions', 'Session']

import platform
import threading
import typing
import requests
import anaconda_navigator

if typing.TYPE_CHECKING:
    import typing_extensions


TKey = typing.TypeVar('TKey')
TValue = typing.TypeVar('TValue')


def merge_dicts(*args: typing.Mapping[TKey, TValue]) -> typing.Dict[TKey, TValue]:
    """Combine values of multiple mappings into a single dictionary."""
    arg: typing.Mapping[TKey, TValue]
    result: typing.Dict[TKey, TValue] = {}
    for arg in args:
        result.update(arg)
    return result


class SessionOptions(typing.Generic[TKey, TValue]):
    """
    Proxy-mapping, which allows editing common session options in a thread-safe way.

    Each access to this collection must be independent and atomic, as it might be changed between the calls in other
    thread.

    :param lock: Parent lock to use for accessing wrapped content.
    :param content: Content to control access to.
    """

    __slots__ = ('__lock', '__content')

    def __init__(self, lock: threading.Lock, content: typing.MutableMapping[TKey, TValue]) -> None:
        """Initialize new :class:`~SessionOptions` instance."""
        self.__lock: 'typing_extensions.Final[threading.Lock]' = lock
        self.__content: 'typing_extensions.Final[typing.MutableMapping[TKey, TValue]]' = content

    def clear(self) -> None:
        """Clear content."""
        with self.__lock:
            self.__content.clear()

    def copy(self) -> typing.Dict[TKey, TValue]:
        """Create a copy of content."""
        with self.__lock:
            return dict(self.__content)

    def update(
            self,
            content: typing.Union[
                None,
                typing.Mapping[TKey, TValue],
                typing.Iterable[typing.Tuple[TKey, TValue]],
            ] = None,
            **kwargs: TValue
    ) -> None:
        """Add new records to the content."""
        with self.__lock:
            if content is None:
                self.__content.update(**kwargs)
            else:
                self.__content.update(content, **kwargs)

    def __getitem__(self, key: TKey) -> TValue:
        """Execute wrapped `__getitem__` method in a thread-safe way."""
        with self.__lock:
            return self.__content[key]

    def __setitem__(self, key: TKey, value: TValue) -> None:
        """Execute wrapped `__setitem__` method in a thread-safe way."""
        with self.__lock:
            self.__content[key] = value

    def __delitem__(self, key: TKey) -> None:
        """Execute wrapped `__delitem__` method in a thread-safe way."""
        with self.__lock:
            del self.__content[key]


class Session:
    """
    Requests wrapper with common configuration for requests.

    This wrapper is created to ensure thread-safe operation, as :class:`~requests.Session` might have some issues in
    this area.
    """

    __slots__ = ('__lock', '__cookies', '__headers')

    def __init__(self) -> None:
        """Initialize new :class:`~Session` instance."""
        self.__lock: 'typing_extensions.Final[threading.Lock]' = threading.Lock()
        self.__cookies: 'typing_extensions.Final[typing.Dict[str, str]]' = {}
        self.__headers: 'typing_extensions.Final[typing.Dict[str, str]]' = {
            'Authorization': '',
            'User-Agent': ' '.join([
                'Mozilla/5.0',
                f'({platform.system()} {platform.machine()})',
                f'Python/{platform.python_version()}',
                f'requests/{requests.__version__}',
                f'Navigator/{anaconda_navigator.__version__}',
            ]),
        }

    @property
    def cookies(self) -> 'SessionOptions[str, str]':  # noqa: D401
        """Collection of cookies used by all requests."""
        return SessionOptions(lock=self.__lock, content=self.__cookies)

    @property
    def headers(self) -> 'SessionOptions[str, str]':  # noqa: D401
        """Collection of headers used by all requests."""
        return SessionOptions(lock=self.__lock, content=self.__headers)

    def request(
            self,
            method: typing.Union[str, bytes],
            url: typing.Union[str, bytes],
            *,
            cookies: typing.Optional[typing.Mapping[str, str]] = None,
            headers: typing.Optional[typing.Mapping[str, str]] = None,
            raise_for_status: bool = False,
            **kwargs: typing.Any,
    ) -> requests.Response:
        """
        Send a new HTTP(s) request.

        :param method: HTTP method to use for request (:code:`'GET'`, :code:`'POST'`, etc.).
        :param url: URL to send request to.
        :param cookies: Collection of additional cookies to use with this request.
        :param headers: Collection of additional headers to use with this request.
        :param raise_for_status: Raise exception for non-200 results.
        :param kwargs: Additional options to use with :mod:`requests`.
        :return: HTTP response details.
        """
        if cookies is None:
            cookies = {}
        if headers is None:
            headers = {}

        with self.__lock:
            cookies = merge_dicts(self.__cookies, cookies)
            headers = merge_dicts(self.__headers, headers)

        result: requests.Response = requests.request(method, url, cookies=cookies, headers=headers, **kwargs)
        if raise_for_status:
            result.raise_for_status()
        return result
