import copy
import base64
import os
from dataclasses import dataclass
from typing import List, Tuple
from ..helpers import Helpers, RequestHelpers
from .errors import UnAuthorizedHubError, HubError
from ..types import HttpTransportType, HubProtocolEncoding, DEFAULT_ENCODING


class NegotiateValidationError(ValueError):
    pass


@dataclass(frozen=True)
class AvailableTransport:
    transport: HttpTransportType
    transfer_formats: List[HubProtocolEncoding]

    @classmethod
    def from_dict(cls, data: dict) -> "AvailableTransport":
        if not isinstance(data, dict):
            raise NegotiateValidationError(
                "availableTransports item must be an object")

        transport = HttpTransportType(data.get("transport"))

        transfer_formats = data.get("transferFormats")

        if not isinstance(transfer_formats, list) or not transfer_formats:
            raise NegotiateValidationError(
                f"transferFormats for {transport} must be a non-empty list"
            )

        transfer_formats = [
            HubProtocolEncoding(fmt)
            for fmt in transfer_formats
        ]

        return cls(
            transport=transport,
            transfer_formats=transfer_formats,
        )


class BaseResponse(object):
    negotiate_version: int
    connection_id: str
    access_token: str
    available_transports: List[AvailableTransport]

    def get_id(self) -> str:
        if self.negotiate_version == 0:
            return self.connection_id

        if self.negotiate_version == 1:
            return self.access_token

        raise ValueError(
            f"Negotiate version invalid {self.negotiate_version}")


@dataclass(frozen=True)
class AzureResponse(BaseResponse):
    negotiate_version = 1
    connection_id = None
    access_token: str
    url: str
    available_transports: List[AvailableTransport]

    @classmethod
    def from_dict(cls, data: dict) -> "NegotiateResponse":
        access_token = data.get("accessToken")
        if not isinstance(access_token, str) or not access_token:
            raise NegotiateValidationError(
                "accessToken must be a non-empty string")

        url = data.get("url")
        if not isinstance(url, str) or not url:
            raise NegotiateValidationError(
                "url must be a non-empty string")
        return cls(
            access_token=access_token,
            url=url,
            available_transports=[
                AvailableTransport(
                    transfer_formats=[
                        HubProtocolEncoding.binary,
                        HubProtocolEncoding.text
                    ],
                    transport=HttpTransportType.web_sockets
                ),
                AvailableTransport(
                    transfer_formats=[
                        HubProtocolEncoding.text
                    ],
                    transport=HttpTransportType.server_sent_events
                ),
                AvailableTransport(
                    transfer_formats=[
                        HubProtocolEncoding.binary,
                        HubProtocolEncoding.text
                    ],
                    transport=HttpTransportType.long_polling
                )
            ]
        )


@dataclass(frozen=True)
class NegotiateResponse(BaseResponse):
    negotiate_version: int
    connection_id: str
    access_token: str
    url: str
    available_transports: List[AvailableTransport]

    @classmethod
    def from_dict(cls, data: dict) -> "NegotiateResponse":
        if not isinstance(data, dict):
            raise NegotiateValidationError(
                "Negotiate response must be a JSON object")

        version = data.get("negotiateVersion")
        if not isinstance(version, int):
            raise NegotiateValidationError(
                "negotiateVersion must be an integer")

        connection_id = data.get("connectionId")
        if not isinstance(connection_id, str) or not connection_id:
            raise NegotiateValidationError(
                "connectionId must be a non-empty string")

        transports = data.get("availableTransports")
        if not isinstance(transports, list) or not transports:
            raise NegotiateValidationError(
                "availableTransports must be a non-empty list")

        parsed_transports = [
            AvailableTransport.from_dict(t) for t in transports
        ]

        access_token = data.get("accessToken", None)
        url = data.get("url", None)

        return cls(
            negotiate_version=version,
            connection_id=connection_id,
            available_transports=parsed_transports,
            access_token=access_token,
            url=url
        )


class NegotiationHandler(object):
    def __init__(
            self,
            url,
            headers,
            proxies,
            ssl_context,
            skip_negotiation):
        self.logger = Helpers.get_logger()
        self.url = url
        self.headers = headers
        self.proxies = proxies
        self.ssl_context = ssl_context
        self.skip_negotiation = skip_negotiation

    def negotiate(self) -> Tuple[str, dict, NegotiateResponse]:
        if self.skip_negotiation:
            key = base64.b64encode(os.urandom(16)).decode(DEFAULT_ENCODING)
            return self.url, self.headers, NegotiateResponse(
                negotiate_version=0,
                connection_id=key,
                access_token=None,
                url=self.url,
                available_transports=[
                    AvailableTransport(
                        HttpTransportType.web_sockets,
                        transfer_formats=[
                            HubProtocolEncoding.text,
                            HubProtocolEncoding.binary]
                    )
                ]
            )
        url = self.url
        headers = copy.deepcopy(self.headers)
        headers.update({'Content-Type': 'application/json'})

        negotiate_url = Helpers.get_negotiate_url(self.url)

        self.logger.debug("Negotiate url:{0}".format(negotiate_url))

        response = RequestHelpers.post(
            negotiate_url,
            headers=headers,
            proxies=self.proxies,
            ssl_context=self.ssl_context)

        status_code, data = response.status_code, response.json()

        self.logger.debug(
            "Negotiate response status code {0}".format(status_code))

        self.logger.debug(
            "Negotiate response {0}".format(data))

        if status_code != 200:
            raise HubError(status_code)\
                if status_code != 401 else UnAuthorizedHubError()

        error = data.get("error", None)

        if error is not None:  # pragma: no cover
            raise HubError(error)

        is_azure_response = 'url' in data.keys()\
            and 'accessToken' in data.keys()

        negotiate_response = AzureResponse.from_dict(data)\
            if is_azure_response else NegotiateResponse.from_dict(data)

        if "connectionId" in data.keys():
            url = Helpers.encode_connection_id(
                self.url, negotiate_response.connection_id)

        # Azure
        if is_azure_response:
            self.logger.debug(
                "Azure url, reformat headers, token and url {0}".format(data))

            url = negotiate_response.url\
                if negotiate_response.url.startswith("ws") else\
                Helpers.http_to_websocket(negotiate_response.url)

            headers = {
                "Authorization": "Bearer " + negotiate_response.access_token
                }

        return url, headers, negotiate_response
