import asyncio
import json
import sys
from datetime import datetime
from typing import Optional

import httpx
from httpx import HTTPError, Response
from typing_extensions import TypedDict

from .error_handler import UnauthorizedException, ForbiddenException, ApiException, \
    ValidationException, InternalException, NotFoundException, TooManyRequestsException
from .timeout_exception import TimeoutException
from ..models import ExceptionMessage, date

if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())


class RetryOpts(TypedDict):
    retries: Optional[int]
    """Maximum amount of request retries, default value is 5."""
    minDelayInSeconds: Optional[float]
    """Minimum delay in seconds until request retry, default value is 1."""
    maxDelayInSeconds: Optional[float]
    """Maximum delay in seconds until request retry, default value is 30."""


class RequestOptions(TypedDict):
    """Options for HttpClient requests."""
    method: Optional[str]
    url: str
    headers: Optional[dict]
    params: Optional[dict]
    body: Optional[dict]
    files: Optional[dict]


class HttpClient:
    """HTTP client library based on requests module."""
    def __init__(self, timeout: float = None, retry_opts=None):
        """Initializes HttpClient class instance.

        Args:
            timeout: Request timeout in seconds.
        """
        timeout = timeout or 60
        if retry_opts is None:
            retry_opts = {}
        self._timeout = timeout
        self._retries = retry_opts['retries'] if 'retries' in retry_opts else 5
        self._minRetryDelayInSeconds = retry_opts['minDelayInSeconds'] if 'minDelayInSeconds' in retry_opts else 1
        self._maxRetryDelayInSeconds = retry_opts['maxDelayInSeconds'] if 'maxDelayInSeconds' in retry_opts else 30

    async def request(self, options: dict, end_time: float = None):
        """Performs a request. Response errors are returned as ApiException or subclasses.

        Args:
            options: Request options.
            end_time: End time for the request.

        Returns:
            Request result.
        """
        if not end_time:
            end_time = datetime.now().timestamp() + self._maxRetryDelayInSeconds * self._retries
        retry_after_seconds = 0
        try:
            response = await self._make_request(options)
            response.raise_for_status()
            if response.status_code == 202:
                retry_after_seconds = response.headers['retry-after']
                if isinstance(retry_after_seconds, str):
                    retry_after_seconds = float(retry_after_seconds)
            if response.content:
                try:
                    response = response.json()
                except Exception as err:
                    print('Error parsing json', err)
        except HTTPError as err:
            raise self._convert_error(err)
        if retry_after_seconds:
            await self._handle_retry(end_time, retry_after_seconds)
            response = await self.request(options, end_time)
        return response

    async def request_with_failover(self, options: RequestOptions, retry_counter: int = 0,
                                    end_time: float = None) -> Response:
        """Performs a request. Response errors are returned as ApiException or subclasses.

        Args:
            options: Request options.
            retry_counter: Retry counter.
            end_time: End time for a request.

        Returns:
            A request response.
        """
        if not end_time:
            end_time = datetime.now().timestamp() + self._maxRetryDelayInSeconds * self._retries
        retry_after_seconds = 0
        try:
            response = await self._make_request(options)
            response.raise_for_status()
            if response.status_code == 202:
                retry_after_seconds = response.headers['retry-after']
                if isinstance(retry_after_seconds, str):
                    retry_after_seconds = float(retry_after_seconds)
            if hasattr(response, 'content') and response.content:
                try:
                    response = response.json()
                except Exception as err:
                    print('Error parsing json', err)
        except HTTPError as err:
            retry_counter = await self._handle_error(err, retry_counter, end_time)
            return await self.request_with_failover(options, retry_counter, end_time)
        if retry_after_seconds:
            await self._handle_retry(end_time, retry_after_seconds)
            response = await self.request_with_failover(options, retry_counter, end_time)
        return response

    async def _make_request(self, options: RequestOptions) -> Response:
        async with httpx.AsyncClient(timeout=self._timeout) as client:
            method = options['method'] if ('method' in options) else 'GET'
            url = options['url']
            params = options['params'] if 'params' in options else None
            files = options['files'] if 'files' in options else None
            headers = options['headers'] if 'headers' in options else None
            body = options['body'] if 'body' in options else None
            req = client.build_request(method, url, params=params, files=files, headers=headers, json=body)
            response = await client.send(req)
            return response

    async def _handle_retry(self, end_time: float, retry_after: float):
        if end_time > datetime.now().timestamp() + retry_after:
            await asyncio.sleep(retry_after)
        else:
            raise TimeoutException('Timed out waiting for the end of the process of calculating metrics')

    async def _handle_error(self, err, retry_counter: int, end_time: float):
        if err.__class__.__name__ == 'ConnectTimeout':
            error = err
        else:
            error = self._convert_error(err)
        if error.__class__.__name__ in ['InternalException', 'ApiException'] and retry_counter < self._retries:
            pause = min(pow(2, retry_counter) * self._minRetryDelayInSeconds, self._maxRetryDelayInSeconds)
            await asyncio.sleep(pause)
            return retry_counter + 1
        elif error.__class__.__name__ == 'TooManyRequestsException':
            retry_time = date(error.metadata['recommendedRetryTime']).timestamp()
            if retry_time < end_time:
                await asyncio.sleep(retry_time - datetime.now().timestamp())
                return retry_counter
        raise error

    def _convert_error(self, err: HTTPError):
        if err.__class__.__name__ == 'ConnectTimeout':
            return err
        try:
            response: ExceptionMessage or TypedDict = json.loads(err.response.text)
        except Exception:
            response = {}
        err_message = response['message'] if 'message' in response else (
            err.response.reason_phrase if hasattr(err, 'response') else None)
        url = err.request.url
        status = None
        if hasattr(err, 'response'):
            status = err.response.status_code
        if status == 400:
            details = response['details'] if 'details' in response else []
            return ValidationException(err_message, details, url)
        elif status == 401:
            return UnauthorizedException(err_message, url)
        elif status == 403:
            return ForbiddenException(err_message, url)
        elif status == 404:
            return NotFoundException(err_message, url)
        elif status == 429:
            err_metadata = response['metadata'] if 'metadata' in response else {}
            return TooManyRequestsException(err_message, err_metadata, url)
        elif status == 500:
            return InternalException(err_message, url)
        else:
            return ApiException(err_message, status, url)
