from smb.common.aiotvm import TvmClient
from smb.common.aiotvm.lib.exceptions import InvalidParams
from typing import Mapping, Optional, Union
from urllib.parse import urlunsplit
from xmlrpc.client import ProtocolError, dumps, loads

import aiohttp


class _XMLRpcMethod:
    def __init__(self, name, callback):
        self._name = name
        self._callback = callback

    async def __call__(self, *args):
        return await self._callback(self._name, *args)

    def __getattr__(self, item):
        return type(self)(f"{self._name}.{item}", self._callback)


class XmlRpcClient:
    COMMON_HTTP_HEADERS = {"Content-Type": "text/xml"}

    def __init__(
        self,
        host: str,
        port: int = 80,
        path: str = "/",
        *,
        use_https: bool = False,
        additional_headers: Optional[Mapping[str, str]] = None,
        timeout: int = 5,
        session: Optional[aiohttp.ClientSession] = None,
        tvm_client: TvmClient = None,
        tvm_destination: Union[str, int] = None,
    ):
        protocol = "https" if use_https else "http"

        self._uri = urlunsplit((protocol, f"{host}:{port}", path, None, None))
        self._headers = self.COMMON_HTTP_HEADERS.copy()
        if additional_headers is not None:
            self._headers.update(additional_headers)

        self._session = session or aiohttp.ClientSession(
            timeout=aiohttp.ClientTimeout(total=timeout)
        )
        self._own_session = session is None
        self._tvm_client = tvm_client
        self._tvm_destination = tvm_destination
        if tvm_client and not tvm_destination:
            raise InvalidParams(f"Invalid tvm_destination: {tvm_destination}.")

    async def close(self):
        if self._own_session:
            await self._session.close()

    def __getattr__(self, item):
        return _XMLRpcMethod(item, self.request)

    async def request(self, methodname, *args):
        request_data = dumps(args, methodname=methodname).encode()

        headers = self._headers
        if self._tvm_client:
            headers = headers.copy()
            headers["X-Ya-Service-Ticket"] = await self._tvm_client.retrieve_ticket(
                destination=self._tvm_destination
            )

        async with self._session.post(
            self._uri, data=request_data, headers=headers
        ) as response:
            response_body = await response.read()

            if response.status != 200:
                raise ProtocolError(
                    self._uri,
                    response.status,
                    response_body.decode(),
                    dict(response.headers),
                )

        resp, _ = loads(response_body, use_builtin_types=True)

        return resp
