import asyncio
import time
import typing
import logging
from aioupnp.protocols.scpd import scpd_post
from aioupnp.device import Service
from aioupnp.fault import UPnPError

log = logging.getLogger(__name__)


def soap_optional_str(x: typing.Optional[typing.Union[str, int]]) -> typing.Optional[str]:
    return str(x) if x is not None and str(x).lower() not in ['none', 'nil'] else None


def soap_bool(x: typing.Optional[typing.Union[str, int]]) -> bool:
    return False if not x or str(x).lower() in ['false', 'False'] else True


class GetSpecificPortMappingEntryResponse(typing.NamedTuple):
    internal_port: int
    lan_address: str
    enabled: bool
    description: str
    lease_time: int


class GetGenericPortMappingEntryResponse(typing.NamedTuple):
    gateway_address: str
    external_port: int
    protocol: str
    internal_port: int
    lan_address: str
    enabled: bool
    description: str
    lease_time: int


class SCPDRequestDebuggingInfo(typing.NamedTuple):
    method: str
    kwargs: typing.Dict[str, typing.Union[str, int, bool]]
    response_xml: bytes
    result: typing.Optional[typing.Union[str, int, bool, GetSpecificPortMappingEntryResponse,
                                         GetGenericPortMappingEntryResponse]]
    err: typing.Optional[Exception]
    ts: float


def recast_return(return_annotation, result: typing.Union[str, int, bool, typing.Dict[str, typing.Union[int, str]]],
                  result_keys: typing.List[str]) -> typing.Optional[
                typing.Union[str, int, bool, GetSpecificPortMappingEntryResponse, GetGenericPortMappingEntryResponse]]:
    if len(result_keys) == 1:
        if isinstance(result, (str, int, bool)):
            single_result = result
        else:
            if result_keys[0] in result:
                single_result = result[result_keys[0]]
            else:  # check for the field having incorrect capitalization
                flattened = {k.lower(): v for k, v in result.items()}
                if result_keys[0].lower() in flattened:
                    single_result = flattened[result_keys[0].lower()]
                else:
                    raise UPnPError(f"expected response key {result_keys[0]}, got {list(result.keys())}")
        if return_annotation is bool:
            return soap_bool(single_result)
        if return_annotation is str:
            return soap_optional_str(single_result)
        return None if single_result is None else int(single_result)
    elif return_annotation in [GetGenericPortMappingEntryResponse, GetSpecificPortMappingEntryResponse]:
        assert isinstance(result, dict)
        arg_types: typing.Dict[str, typing.Type[typing.Any]] = return_annotation._field_types
        assert len(arg_types) == len(result_keys)
        recast_results: typing.Dict[str, typing.Optional[typing.Union[str, int, bool]]] = {}
        for i, (field_name, result_key) in enumerate(zip(arg_types, result_keys)):
            result_field_name = result_keys[i]
            field_type = arg_types[field_name]
            if field_type is bool:
                recast_results[field_name] = soap_bool(result.get(result_field_name, None))
            elif field_type is str:
                recast_results[field_name] = soap_optional_str(result.get(result_field_name, None))
            elif field_type is int:
                recast_results[field_name] = int(result[result_field_name]) if result_field_name in result else None
        return return_annotation(**recast_results)
    return None


class SOAPCommands:
    """
    Type annotated wrappers for common UPnP SOAP functions

    A SOAPCommands object has its command attributes overridden during device discovery with SOAPCommand objects
    for the commands implemented by the gateway.

    SOAPCommand will use the typing annotations provided here to properly cast the types of arguments and results
    to their expected types.
    """

    SOAP_COMMANDS: typing.List[str] = [
        'AddPortMapping',
        'GetGenericPortMappingEntry',
        'GetSpecificPortMappingEntry',
        'DeletePortMapping',
        'GetExternalIPAddress',
        # 'SetConnectionType',
        # 'GetNATRSIPStatus',
        # 'GetConnectionTypeInfo',
        # 'GetStatusInfo',
        # 'ForceTermination',
        # 'RequestConnection',
        # 'GetCommonLinkProperties',
        # 'GetTotalBytesSent',
        # 'GetTotalBytesReceived',
        # 'GetTotalPacketsSent',
        # 'GetTotalPacketsReceived',
        # 'X_GetICSStatistics',
        # 'GetDefaultConnectionService',
        # 'SetDefaultConnectionService',
        # 'SetEnabledForInternet',
        # 'GetEnabledForInternet',
        # 'GetMaximumActiveConnections',
        # 'GetActiveConnections'
    ]

    def __init__(self, loop: asyncio.AbstractEventLoop, base_address: bytes, port: int) -> None:
        self._loop = loop
        self._registered: typing.Dict[Service,
                                      typing.Dict[str, typing.Tuple[typing.List[str], typing.List[str]]]] = {}
        self._wrappers_no_args: typing.Dict[str, typing.Callable[[], typing.Awaitable[typing.Any]]] = {}
        self._wrappers_kwargs: typing.Dict[str, typing.Callable[..., typing.Awaitable[typing.Any]]] = {}

        self._base_address = base_address
        self._port = port
        self._request_debug_infos: typing.List[SCPDRequestDebuggingInfo] = []

    def is_registered(self, name: str) -> bool:
        if name not in self.SOAP_COMMANDS:
            raise ValueError("unknown command")
        for service in self._registered.values():
            if name in service:
                return True
        return False

    def get_service(self, name: str) -> Service:
        if name not in self.SOAP_COMMANDS:
            raise ValueError("unknown command")
        for service, commands in self._registered.items():
            if name in commands:
                return service
        raise ValueError(name)

    def _register_soap_wrapper(self, name: str) -> None:
        annotations: typing.Dict[str, typing.Any] = typing.get_type_hints(getattr(self, name))
        service = self.get_service(name)
        input_names: typing.List[str] = self._registered[service][name][0]
        output_names: typing.List[str] = self._registered[service][name][1]

        async def wrapper(**kwargs: typing.Any) -> typing.Optional[
              typing.Union[str, int, bool, GetSpecificPortMappingEntryResponse, GetGenericPortMappingEntryResponse]]:

            assert service.controlURL is not None
            assert service.serviceType is not None
            response, xml_bytes, err = await scpd_post(
                service.controlURL, self._base_address.decode(), self._port, name, input_names,
                service.serviceType.encode(), self._loop, **kwargs
            )
            if err is not None:
                assert isinstance(xml_bytes, bytes)
                self._request_debug_infos.append(SCPDRequestDebuggingInfo(name, kwargs, xml_bytes, None, err, time.time()))
                raise err
            assert 'return' in annotations
            try:
                result = recast_return(annotations['return'], response, output_names)
                self._request_debug_infos.append(SCPDRequestDebuggingInfo(name, kwargs, xml_bytes, result, None, time.time()))
            except Exception as err:
                if isinstance(err, asyncio.CancelledError):
                    raise
                self._request_debug_infos.append(SCPDRequestDebuggingInfo(name, kwargs, xml_bytes, None, err, time.time()))
                raise UPnPError(f"Raised {str(type(err).__name__)}({str(err)}) parsing response for {name}")
            return result

        if not len(list(k for k in annotations if k != 'return')):
            self._wrappers_no_args[name] = wrapper
        else:
            self._wrappers_kwargs[name] = wrapper
        return None

    def register(self, name: str, service: Service, inputs: typing.List[str], outputs: typing.List[str]) -> None:
        if name not in self.SOAP_COMMANDS:
            raise AttributeError(name)
        if self.is_registered(name):
            raise AttributeError(f"{name} is already a registered SOAP command")
        if service not in self._registered:
            self._registered[service] = {}
        self._registered[service][name] = inputs, outputs
        self._register_soap_wrapper(name)

    async def AddPortMapping(self, NewRemoteHost: str, NewExternalPort: int, NewProtocol: str, NewInternalPort: int,
                             NewInternalClient: str, NewEnabled: int, NewPortMappingDescription: str,
                             NewLeaseDuration: str) -> None:
        """Returns None"""
        name = "AddPortMapping"
        if not self.is_registered(name):
            raise NotImplementedError()
        assert name in self._wrappers_kwargs
        await self._wrappers_kwargs[name](
            NewRemoteHost=NewRemoteHost, NewExternalPort=NewExternalPort, NewProtocol=NewProtocol,
            NewInternalPort=NewInternalPort, NewInternalClient=NewInternalClient, NewEnabled=NewEnabled,
            NewPortMappingDescription=NewPortMappingDescription, NewLeaseDuration=NewLeaseDuration
        )
        return None

    async def GetGenericPortMappingEntry(self, NewPortMappingIndex: int) -> GetGenericPortMappingEntryResponse:
        """
        Returns (NewRemoteHost, NewExternalPort, NewProtocol, NewInternalPort, NewInternalClient, NewEnabled,
                 NewPortMappingDescription, NewLeaseDuration)
        """
        name = "GetGenericPortMappingEntry"
        if not self.is_registered(name):
            raise NotImplementedError()
        assert name in self._wrappers_kwargs
        result: GetGenericPortMappingEntryResponse = await self._wrappers_kwargs[name](
            NewPortMappingIndex=NewPortMappingIndex
        )
        return result

    async def GetSpecificPortMappingEntry(self, NewRemoteHost: str, NewExternalPort: int,
                                          NewProtocol: str) -> GetSpecificPortMappingEntryResponse:
        """Returns (NewInternalPort, NewInternalClient, NewEnabled, NewPortMappingDescription, NewLeaseDuration)"""
        name = "GetSpecificPortMappingEntry"
        if not self.is_registered(name):
            raise NotImplementedError()
        assert name in self._wrappers_kwargs
        result: GetSpecificPortMappingEntryResponse = await self._wrappers_kwargs[name](
            NewRemoteHost=NewRemoteHost, NewExternalPort=NewExternalPort, NewProtocol=NewProtocol
        )
        return result

    async def DeletePortMapping(self, NewRemoteHost: str, NewExternalPort: int, NewProtocol: str) -> None:
        """Returns None"""
        name = "DeletePortMapping"
        if not self.is_registered(name):
            raise NotImplementedError()
        assert name in self._wrappers_kwargs
        await self._wrappers_kwargs[name](
            NewRemoteHost=NewRemoteHost, NewExternalPort=NewExternalPort, NewProtocol=NewProtocol
        )
        return None

    async def GetExternalIPAddress(self) -> str:
        """Returns (NewExternalIPAddress)"""
        name = "GetExternalIPAddress"
        if not self.is_registered(name):
            raise NotImplementedError()
        assert name in self._wrappers_no_args
        result: str = await self._wrappers_no_args[name]()
        if not result:
            raise UPnPError("Got null external ip address")
        return result

    # async def GetNATRSIPStatus(self) -> Tuple[bool, bool]:
    #     """Returns (NewRSIPAvailable, NewNATEnabled)"""
    #     name = "GetNATRSIPStatus"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[bool, bool] = await self._wrappers_no_args[name]()
    #     return result[0], result[1]
    #
    # async def SetConnectionType(self, NewConnectionType: str) -> None:
    #     """Returns None"""
    #     name = "SetConnectionType"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_kwargs
    #     await self._wrappers_kwargs[name](NewConnectionType=NewConnectionType)
    #     return None
    #
    # async def GetConnectionTypeInfo(self) -> Tuple[str, str]:
    #     """Returns (NewConnectionType, NewPossibleConnectionTypes)"""
    #     name = "GetConnectionTypeInfo"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[str, str] = await self._wrappers_no_args[name]()
    #     return result
    #
    # async def GetStatusInfo(self) -> Tuple[str, str, int]:
    #     """Returns (NewConnectionStatus, NewLastConnectionError, NewUptime)"""
    #     name = "GetStatusInfo"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[str, str, int] = await self._wrappers_no_args[name]()
    #     return result
    #
    # async def ForceTermination(self) -> None:
    #     """Returns None"""
    #     name = "ForceTermination"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     await self._wrappers_no_args[name]()
    #     return None
    #
    # async def RequestConnection(self) -> None:
    #     """Returns None"""
    #     name = "RequestConnection"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     await self._wrappers_no_args[name]()
    #     return None
    #
    # async def GetCommonLinkProperties(self) -> Tuple[str, int, int, str]:
    #     """Returns (NewWANAccessType, NewLayer1UpstreamMaxBitRate, NewLayer1DownstreamMaxBitRate,
    #      NewPhysicalLinkStatus)"""
    #     name = "GetCommonLinkProperties"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[str, int, int, str] = await self._wrappers_no_args[name]()
    #     return result
    #
    # async def GetTotalBytesSent(self) -> int:
    #     """Returns (NewTotalBytesSent)"""
    #     name = "GetTotalBytesSent"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[int] = await self._wrappers_no_args[name]()
    #     return result[0]
    #
    # async def GetTotalBytesReceived(self) -> int:
    #     """Returns (NewTotalBytesReceived)"""
    #     name = "GetTotalBytesReceived"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[int] = await self._wrappers_no_args[name]()
    #     return result[0]
    #
    # async def GetTotalPacketsSent(self) -> int:
    #     """Returns (NewTotalPacketsSent)"""
    #     name = "GetTotalPacketsSent"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[int] = await self._wrappers_no_args[name]()
    #     return result[0]
    #
    # async def GetTotalPacketsReceived(self) -> int:
    #     """Returns (NewTotalPacketsReceived)"""
    #     name = "GetTotalPacketsReceived"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[int] = await self._wrappers_no_args[name]()
    #     return result[0]
    #
    # async def X_GetICSStatistics(self) -> Tuple[int, int, int, int, str, str]:
    #     """Returns (TotalBytesSent, TotalBytesReceived, TotalPacketsSent, TotalPacketsReceived,
    #     Layer1DownstreamMaxBitRate, Uptime)"""
    #     name = "X_GetICSStatistics"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[int, int, int, int, str, str] = await self._wrappers_no_args[name]()
    #     return result
    #
    # async def GetDefaultConnectionService(self) -> str:
    #     """Returns (NewDefaultConnectionService)"""
    #     name = "GetDefaultConnectionService"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[str] = await self._wrappers_no_args[name]()
    #     return result[0]
    #
    # async def SetDefaultConnectionService(self, NewDefaultConnectionService: str) -> None:
    #     """Returns (None)"""
    #     name = "SetDefaultConnectionService"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_kwargs
    #     await self._wrappers_kwargs[name](NewDefaultConnectionService=NewDefaultConnectionService)
    #     return None
    #
    # async def SetEnabledForInternet(self, NewEnabledForInternet: bool) -> None:
    #     name = "SetEnabledForInternet"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_kwargs
    #     await self._wrappers_kwargs[name](NewEnabledForInternet=NewEnabledForInternet)
    #     return None
    #
    # async def GetEnabledForInternet(self) -> bool:
    #     name = "GetEnabledForInternet"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[bool] = await self._wrappers_no_args[name]()
    #     return result[0]
    #
    # async def GetMaximumActiveConnections(self, NewActiveConnectionIndex: int) -> None:
    #     name = "GetMaximumActiveConnections"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_kwargs
    #     await self._wrappers_kwargs[name](NewActiveConnectionIndex=NewActiveConnectionIndex)
    #     return None
    #
    # async def GetActiveConnections(self) -> Tuple[str, str]:
    #     """Returns (NewActiveConnDeviceContainer, NewActiveConnectionServiceID"""
    #     name = "GetActiveConnections"
    #     if not self.is_registered(name):
    #         raise NotImplementedError()
    #     assert name in self._wrappers_no_args
    #     result: Tuple[str, str] = await self._wrappers_no_args[name]()
    #     return result
