import datetime
import itertools as it
import logging
import requests
import time

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, created_by, auth=None, session=None, timeout=30):
        self._api = base_url
        self._created_by = created_by
        self._timeout = timeout

        try:
            self._auth = auth
            if not auth and session.auth:
                self._auth = session.auth
        except AttributeError:
            pass

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

    def close(self):
        if self._session:
            self._session.close()

    def _http_request(self, method, path, auth, send, return_value=True):  # noqa
        """
        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
            )
        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(self, cmd, path, auth, send, return_value=True):  # noqa
        for i in it.count(1):
            try:
                return self._http_request(
                    cmd, path, auth, send, return_value=return_value
                )
            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.exception("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):
        """
        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):
        time.sleep(5)

    def _get(self, path):
        return self._http("GET", path, False, None)

    def _put(self, path, data, return_value=True):
        return self._http("PUT", path, True, data, return_value=return_value)

    def _post(self, path, data, optionalAuth=False, forceAuth=False):
        # the Notes API allows certain POSTs by non-authenticated users
        auth = optionalAuth and self._auth
        if forceAuth:
            auth = True
        return self._http("POST", path, auth, data)

    def _delete(self, path, data):
        return self._http("DELETE", path, True, data)
