"""
HTTP session management for the OpenStreetMap API.
"""

import datetime
import itertools as it
import logging
import requests
import time
from typing import Any, Optional, Tuple, Union

from . import errors

logger = logging.getLogger(__name__)


class OsmApiSession:
    MAX_RETRY_LIMIT = 5
    """Maximum retries if a call to the remote API fails (default: 5)"""

    def __init__(
        self,
        base_url: str,
        created_by: str,
        auth: Optional[Tuple[str, str]] = None,
        session: Optional[requests.Session] = None,
        timeout: int = 30,
    ) -> None:
        self._api = base_url
        self._created_by = created_by
        self._timeout = timeout

        try:
            self._auth: Optional[Any] = auth
            if not auth and session.auth:  # type: ignore[union-attr]
                self._auth = session.auth  # type: ignore[union-attr]
        except AttributeError:
            pass

        self._http_session = session
        self._session = self._get_http_session()

    def close(self) -> None:
        if self._session:
            self._session.close()

    def _http_request(  # noqa: C901
        self,
        method: str,
        path: str,
        auth: bool,
        send: Optional[Union[str, bytes]],
        return_value: bool = True,
        params: Optional[dict] = None,
    ) -> bytes:
        """
        Returns the response generated by an HTTP request.

        `method` is a HTTP method to be executed
        with the request data. For example: 'GET' or 'POST'.
        `path` is the path to the requested resource relative to the
        base API address stored in self._api. Should start with a
        slash character to separate the URL.
        `auth` is a boolean indicating whether authentication should
        be preformed on this request.
        `send` contains additional data that might be sent in a
        request.
        `return_value` indicates wheter this request should return
        any data or not.

        If the username or password is missing,
        `OsmApi.UsernamePasswordMissingError` is raised.

        If the requested element has been deleted,
        `OsmApi.ElementDeletedApiError` is raised.

        If the requested element can not be found,
        `OsmApi.ElementNotFoundApiError` is raised.

        If the response status code indicates an error,
        `OsmApi.ApiError` is raised.
        """
        logger.debug(f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S} {method} {path}")

        # Add API base URL to path
        path = self._api + path

        if auth and not self._auth:
            raise errors.UsernamePasswordMissingError("Username/Password missing")

        try:
            response = self._session.request(
                method, path, data=send, timeout=self._timeout, params=params
            )
        except requests.exceptions.Timeout as e:
            raise errors.TimeoutApiError(
                0, f"Request timed out (timeout={self._timeout})", ""
            ) from e
        except requests.exceptions.ConnectionError as e:
            raise errors.ConnectionApiError(0, f"Connection error: {str(e)}", "") from e
        except requests.exceptions.RequestException as e:
            raise errors.ApiError(0, str(e), "") from e

        if response.status_code != 200:
            payload = response.content.strip()
            if response.status_code == 401:
                raise errors.UnauthorizedApiError(
                    response.status_code, response.reason, payload
                )
            if response.status_code == 404:
                raise errors.ElementNotFoundApiError(
                    response.status_code, response.reason, payload
                )
            elif response.status_code == 410:
                raise errors.ElementDeletedApiError(
                    response.status_code, response.reason, payload
                )
            raise errors.ApiError(response.status_code, response.reason, payload)
        if return_value and not response.content:
            raise errors.ResponseEmptyApiError(
                response.status_code, response.reason, ""
            )

        logger.debug(f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S} {method} {path}")
        return response.content

    def _http(  # type: ignore[return-value]  # noqa: C901
        self,
        cmd: str,
        path: str,
        auth: bool,
        send: Optional[Union[str, bytes]],
        return_value: bool = True,
        params: Optional[dict] = None,
    ) -> bytes:
        for i in it.count(1):
            try:
                return self._http_request(
                    cmd, path, auth, send, return_value=return_value, params=params
                )
            except errors.ApiError as e:
                if e.status >= 500:
                    if i == self.MAX_RETRY_LIMIT:
                        raise
                    if i != 1:
                        self._sleep()
                    self._session = self._get_http_session()
                else:
                    logger.debug("ApiError Exception occured")
                    raise
            except errors.UsernamePasswordMissingError:
                raise
            except Exception as e:
                logger.exception("General exception occured")
                if i == self.MAX_RETRY_LIMIT:
                    if isinstance(e, errors.OsmApiError):
                        raise
                    raise errors.MaximumRetryLimitReachedError(
                        f"Give up after {i} retries"
                    ) from e
                if i != 1:
                    self._sleep()
                self._session = self._get_http_session()

    def _get_http_session(self) -> requests.Session:
        """
        Creates a requests session for connection pooling.
        """
        if self._http_session:
            session = self._http_session
        else:
            session = requests.Session()

        session.auth = self._auth
        session.headers.update({"user-agent": self._created_by})
        return session

    def _sleep(self) -> None:
        time.sleep(5)

    def _get(self, path: str, params: Optional[dict] = None) -> bytes:
        return self._http("GET", path, False, None, params=params)

    def _put(
        self, path: str, data: Optional[Union[str, bytes]], return_value: bool = True
    ) -> bytes:
        return self._http("PUT", path, True, data, return_value=return_value)

    def _post(
        self,
        path: str,
        data: Optional[Union[str, bytes]],
        optionalAuth: bool = False,
        forceAuth: bool = False,
        params: Optional[dict] = None,
    ) -> bytes:
        # the Notes API allows certain POSTs by non-authenticated users
        auth = optionalAuth and self._auth
        if forceAuth:
            auth = True
        return self._http("POST", path, bool(auth), data, params=params)

    def _delete(self, path: str, data: Optional[Union[str, bytes]]) -> bytes:
        return self._http("DELETE", path, True, data)
