import time

import requests

from .exceptions import ApiError, AuthError, NetworkError, NotFoundError, RateLimitError, ServerError

DEFAULT_BASE_URL = "https://api.meteoflow.com"
DEFAULT_TIMEOUT = 10
DEFAULT_RETRIES = 2
DEFAULT_BACKOFF = 0.5
DEFAULT_USER_AGENT = "meteoflow-python/0.1.0"


class HttpTransport(object):
    def __init__(self,
                 base_url=DEFAULT_BASE_URL,
                 timeout=DEFAULT_TIMEOUT,
                 retries=DEFAULT_RETRIES,
                 backoff_factor=DEFAULT_BACKOFF,
                 user_agent=None,
                 proxies=None,
                 verify_ssl=True,
                 session=None,
                 retry_on_429=True):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.retries = retries
        self.backoff_factor = backoff_factor
        self.user_agent = user_agent or DEFAULT_USER_AGENT
        self.proxies = proxies
        self.verify_ssl = verify_ssl
        self.retry_on_429 = retry_on_429
        self.session = session or requests.Session()

    def get(self, path, params=None, headers=None):
        return self.request("GET", path, params=params, headers=headers)

    def request(self, method, path, params=None, headers=None):
        url = self.base_url + path
        request_headers = {
            "User-Agent": self.user_agent,
        }
        if headers:
            request_headers.update(headers)

        attempt = 0
        while True:
            try:
                response = self.session.request(
                    method,
                    url,
                    params=params,
                    headers=request_headers,
                    timeout=self.timeout,
                    proxies=self.proxies,
                    verify=self.verify_ssl,
                )
            except requests.RequestException as exc:
                if attempt >= self.retries:
                    raise NetworkError(str(exc))
                self._sleep_backoff(attempt)
                attempt += 1
                continue

            if self._should_retry(response, method) and attempt < self.retries:
                self._sleep_backoff(attempt)
                attempt += 1
                continue

            if response.status_code < 200 or response.status_code >= 300:
                self._raise_api_error(response, params)

            try:
                return response.json()
            except ValueError:
                raise ApiError(
                    response.status_code,
                    response.text,
                    "Invalid JSON response",
                    response.url,
                    params,
                )

    def _sleep_backoff(self, attempt):
        if self.backoff_factor is None:
            return
        delay = self.backoff_factor * (2 ** attempt)
        if delay > 0:
            time.sleep(delay)

    def _should_retry(self, response, method):
        if method != "GET":
            return False
        if response.status_code >= 500:
            return True
        if response.status_code == 429 and self.retry_on_429:
            return True
        return False

    def _raise_api_error(self, response, params):
        status = response.status_code
        message = self._extract_message(response)
        body = response.text
        url = response.url
        if status in (401, 403):
            raise AuthError(status, body, message, url, params)
        if status == 404:
            raise NotFoundError(status, body, message, url, params)
        if status == 429:
            raise RateLimitError(status, body, message, url, params)
        if status >= 500:
            raise ServerError(status, body, message, url, params)
        raise ApiError(status, body, message, url, params)

    def _extract_message(self, response):
        try:
            data = response.json()
        except ValueError:
            return response.text
        if isinstance(data, dict):
            for key in ("message", "error", "detail"):
                if key in data and data[key]:
                    return data[key]
        return response.text
