# -*- coding: utf-8 -*-
import json
import logging


import random
from requests.exceptions import ConnectionError
from requests import Session
import time

from jira.exceptions import JIRAError

logging.getLogger("jira").addHandler(logging.NullHandler())


def raise_on_error(r, verb="???", **kwargs):
    request = kwargs.get("request", None)
    # headers = kwargs.get('headers', None)

    if r is None:
        raise JIRAError(None, **kwargs)

    if r.status_code >= 400:
        error = ""
        if r.status_code == 403 and "x-authentication-denied-reason" in r.headers:
            error = r.headers["x-authentication-denied-reason"]
        elif r.text:
            try:
                response = json.loads(r.text)
                if "message" in response:
                    # Jira 5.1 errors
                    error = response["message"]
                elif "errorMessages" in response and len(response["errorMessages"]) > 0:
                    # Jira 5.0.x error messages sometimes come wrapped in this array
                    # Sometimes this is present but empty
                    errorMessages = response["errorMessages"]
                    if isinstance(errorMessages, (list, tuple)):
                        error = errorMessages[0]
                    else:
                        error = errorMessages
                # Catching only 'errors' that are dict. See https://github.com/pycontribs/jira/issues/350
                elif (
                    "errors" in response
                    and len(response["errors"]) > 0
                    and isinstance(response["errors"], dict)
                ):
                    # Jira 6.x error messages are found in this array.
                    error_list = response["errors"].values()
                    error = ", ".join(error_list)
                else:
                    error = r.text
            except ValueError:
                error = r.text
        raise JIRAError(
            r.status_code, error, r.url, request=request, response=r, **kwargs
        )
    # for debugging weird errors on CI
    if r.status_code not in [200, 201, 202, 204]:
        raise JIRAError(r.status_code, request=request, response=r, **kwargs)
    # testing for the WTH bug exposed on
    # https://answers.atlassian.com/questions/11457054/answers/11975162
    if (
        r.status_code == 200
        and len(r.content) == 0
        and "X-Seraph-LoginReason" in r.headers
        and "AUTHENTICATED_FAILED" in r.headers["X-Seraph-LoginReason"]
    ):
        pass


class ResilientSession(Session):
    """This class is supposed to retry requests that do return temporary errors.

    At this moment it supports: 502, 503, 504
    """

    def __init__(self, timeout=None):
        self.max_retries = 3
        self.timeout = timeout
        super(ResilientSession, self).__init__()

        # Indicate our preference for JSON to avoid https://bitbucket.org/bspeakmon/jira-python/issue/46 and https://jira.atlassian.com/browse/JRA-38551
        self.headers.update({"Accept": "application/json,*.*;q=0.9"})

    def __recoverable(self, response, url, request, counter=1):
        msg = response
        if isinstance(response, ConnectionError):
            logging.warning(
                "Got ConnectionError [%s] errno:%s on %s %s\n%s\n%s"
                % (
                    response,
                    response.errno,
                    request,
                    url,
                    vars(response),
                    response.__dict__,
                )
            )
        if hasattr(response, "status_code"):
            if response.status_code in [502, 503, 504, 401]:
                # 401 UNAUTHORIZED still randomly returned by Atlassian Cloud as of 2017-01-16
                msg = "%s %s" % (response.status_code, response.reason)
                # 2019-07-25: Disabled recovery for codes above^
                return False
            elif not (
                response.status_code == 200
                and len(response.content) == 0
                and "X-Seraph-LoginReason" in response.headers
                and "AUTHENTICATED_FAILED" in response.headers["X-Seraph-LoginReason"]
            ):
                return False
            else:
                msg = "Atlassian's bug https://jira.atlassian.com/browse/JRA-41559"

        # Exponential backoff with full jitter.
        delay = min(60, 10 * 2 ** counter) * random.random()
        logging.warning(
            "Got recoverable error from %s %s, will retry [%s/%s] in %ss. Err: %s"
            % (request, url, counter, self.max_retries, delay, msg)
        )
        logging.debug("response.headers: %s", response.headers)
        logging.debug("response.body: %s", response.content)
        time.sleep(delay)
        return True

    def __verb(self, verb, url, retry_data=None, **kwargs):

        d = self.headers.copy()
        d.update(kwargs.get("headers", {}))
        kwargs["headers"] = d

        # if we pass a dictionary as the 'data' we assume we want to send json
        # data
        data = kwargs.get("data", {})
        if isinstance(data, dict):
            data = json.dumps(data)

        retry_number = 0
        while retry_number <= self.max_retries:
            response = None
            exception = None
            try:
                method = getattr(super(ResilientSession, self), verb.lower())
                response = method(url, timeout=self.timeout, **kwargs)
                if response.status_code >= 200 and response.status_code <= 299:
                    return response
            except ConnectionError as e:
                logging.warning(
                    "%s while doing %s %s [%s]" % (e, verb.upper(), url, kwargs)
                )
                exception = e
            retry_number += 1

            if retry_number <= self.max_retries:
                response_or_exception = response if response is not None else exception
                if self.__recoverable(
                    response_or_exception, url, verb.upper(), retry_number
                ):
                    if retry_data:
                        # if data is a stream, we cannot just read again from it,
                        # retry_data() will give us a new stream with the data
                        kwargs["data"] = retry_data()
                    continue
                else:
                    break

        if exception is not None:
            raise exception
        raise_on_error(response, verb=verb, **kwargs)
        return response

    def get(self, url, **kwargs):
        return self.__verb("GET", url, **kwargs)

    def post(self, url, **kwargs):
        return self.__verb("POST", url, **kwargs)

    def put(self, url, **kwargs):
        return self.__verb("PUT", url, **kwargs)

    def delete(self, url, **kwargs):
        return self.__verb("DELETE", url, **kwargs)

    def head(self, url, **kwargs):
        return self.__verb("HEAD", url, **kwargs)

    def patch(self, url, **kwargs):
        return self.__verb("PATCH", url, **kwargs)

    def options(self, url, **kwargs):
        return self.__verb("OPTIONS", url, **kwargs)
