import json
import warnings
from json.decoder import JSONDecodeError
from time import sleep
from typing import Dict
from typing import List
from typing import Optional
from typing import Union

from requests.exceptions import HTTPError

from valr_python.exceptions import IncompleteOrderWarning
from valr_python.exceptions import RESTAPIException
from valr_python.exceptions import TooManyRequestsWarning
from valr_python.rest_base import MethodClientABC
from valr_python.utils import DecimalEncoder
from valr_python.utils import _get_valr_headers

__all__ = ('Client',)


class Client(MethodClientABC):
    """
        Synchronous Python SDK for the VALR REST API.

            >>> from valr_python import Client
            >>> from valr_python.exceptions import IncompleteOrderWarning
            >>>
            >>> c = Client(api_key='api_key', api_secret='api_secret')
            >>> c.rate_limiting_support = True # honour HTTP 429 "Retry-After" header values
            >>> limit_order = {
            ...     "side": "SELL",
            ...     "quantity": 0.1,
            ...     "price": 10000,
            ...     "pair": "BTCZAR",
            ...     "post_only": True,
            ... }
            >>> try:
            ...    res = c.post_limit_order(**limit_order)
            ...    order_id = res['id']
            ...    print(order_id)
            ... except IncompleteOrderWarning as w:  # HTTP 202 Accepted handling for incomplete orders
            ...    order_id = w.data['id']
            ...    print(order_id)
            ... except Exception as e:
            ...    print(e)
            "558f5e0a-ffd1-46dd-8fae-763d93fa2f25"
            >>>
        """

    def _do(self, method: str, path: str, data: Optional[Dict] = None,
            is_authenticated: bool = False) -> Optional[Union[List, Dict]]:
        """Executes API request and returns the response.

        Includes HTTP 429 handling by honouring VALR's 429 Retry-After header cool-down.
        """
        headers = {}
        if data:
            data = json.dumps(data, cls=DecimalEncoder)  # serialize decimals as str
            headers["Content-Type"] = "application/json"
        if is_authenticated:
            headers.update(_get_valr_headers(api_key=self.api_key, api_secret=self.api_secret, method=method,
                                             path=path, data=data))
        url = self._base_url + '/' + path.lstrip('/')
        args = dict(timeout=self._timeout, data=data, headers=headers)
        res = self._session.request(method, url, **args)

        try:
            res.raise_for_status()
            e = res.json()
            self._raise_for_api_error(e)
            # provide warning with bundled response dict for incomplete transactions
            if res.status_code == 202:
                warnings.warn(IncompleteOrderWarning(data=e, message="Order processing incomplete"))
            return e
        except HTTPError as he:
            print(he)
            if res.status_code == 429:
                if self._rate_limiting_support:
                    try:
                        retry_after = float(res.headers['Retry-After'])
                        warnings.warn(f"HTTP 429 response received. Applying Retry-After {retry_after}sec back-off",
                                      TooManyRequestsWarning)
                        sleep(retry_after)
                        return self._do(method=method, path=path, is_authenticated=is_authenticated, data=data)
                    except (KeyError, ValueError):
                        raise RESTAPIException(res.status_code,
                                               f'valr-python: HTTP 429 processing failed. '
                                               f'HTTP ({res.status_code}): {res.headers}')
                else:
                    # avoid JSONDecodeError - VALR 429 response has html body
                    raise he
            e = res.json()
            self._raise_for_api_error(e)
            # bubble HTTP errors that VALR API doesn't report on
            raise he
        except JSONDecodeError as jde:
            raise RESTAPIException(res.status_code,
                                   f'valr-python: unknown API error. HTTP ({res.status_code}): {jde.msg}')
