import ast
import asyncio
from typing import Any, Awaitable, Callable, Dict
from xml.parsers.expat import ExpatError

from aioxmlrpc.client import Fault, ProtocolError, ServerProxy

from .. import app_config, util
from ..broker import Broker, BrokerOptions
from ..logger import Logger
from ..request import Request
from ..response import Response

NAME = 'Xmlrpc'
"""Name of the main class of the module.

Used by the App class
to obtain the correct class via getattr

"""

# Should get these values elsewhere
server_info = {
    'proxy': {'port': '5062', 'path': '/'},
    'loadbalancer': {'port': '5060', 'path': '/'},
    'appserver': {'port': '8090', 'path': '/'},
    'xmpp': {'port': '5582', 'path': '/'}
}


class XmlrpcOptions(BrokerOptions):
    """Xmlrpc broker options.

    Attributes:
        method (str): method to run
        service (str): the type of service
        params (str): dynamic parameters for the service
    """
    method: str
    service: str
    params: str


class Xmlrpc(Broker):
    """Xmlrpc broker class, inherits Broker."""

    def __init__(self, send_req_cb: Callable[[Request],
                                             Awaitable[None]]) -> None:
        """Constructor for Xmlrpc class.

        Args:
            send_req_cb (callable): callback that a broker can use to trigger
                sending another request. Set by the App class.

        Returns:
            None

        """
        self._logger = Logger(util.class_name(self))
        super().__init__(send_req_cb)

    async def start(self) -> None:
        """Starts the broker.

        Currently does nothing as there is no
        specific logic related to the broker startup. Reserved for
        future use and consistency with the parent class

        Args:
            None

        Returns
            None

        """
        pass

    async def process_request(self,
                              request: Request,
                              options: Dict[str, Any],
                              callback: Callable[[Request, Response],
                                                 Awaitable[None]]) -> None:
        """Processes the provided request and sends back responses.

        Uses the provided callback function

        Args:
            request (Request): Request message object
            options (Dict): broker options
            callback (Callable[[Request, Response], Awaitable[None]]):
                callback function to dispatch the responses to

        Returns
            None

        """
        xmlrpc_options = XmlrpcOptions()
        xmlrpc_options.__dict__.update(options)

        response = Response()
        response.status = 'accepted'

        method = getattr(xmlrpc_options, 'method', None)
        service = xmlrpc_options.service
        path = ''
        headers = {}
        if request.options['path']:
            path = str(request.options['path'])
        if request.options['headers']:
            headers = ast.literal_eval(str(request.options['headers']))

        self.log.debug(util.compact_str(
            f"""xmlrpc call: method={method} service={service}
                path={path} headers={headers}"""
        ))

        try:
            xmlrpc_server_port = server_info[service]['port']
            xmlrpc_server = f'http://127.0.0.1:{xmlrpc_server_port}{path}'

            client = ServerProxy(xmlrpc_server, headers=headers)

            values = ''
            if request.data:
                values = ast.literal_eval(str(request.data))
            if method:
                response.data = await getattr(client, method)(values)
            else:
                try:
                    await client.no_method()
                except ExpatError:
                    pass

            response.status = 'done'
        except asyncio.CancelledError as e:
            self.log.error(f'Error: {e}')
            response.status = 'Task cancelled.'
            response.status = 'error'
        except ProtocolError as e:
            self.log.error(f'Error: {e}')
            response.reason = f'HTTP error ({e.errcode}): {e.errmsg}.'
            response.status = 'error'
        except Fault as e:
            self.log.error(f'Error: {e}')
            response.reason = f'Fault ({e.faultCode}): {e.faultString}.'
            response.status = 'error'
        except ValueError as e:
            self.log.error(f'Error: {e}')
            response.reason = 'ValueError: Invalid literal expression. ' + \
                'Please provide a valid string (between single quotes),' + \
                ' number, list, or dictionary.'
            response.status = 'error'
            self.log.error(f'Error: {response.reason}')

        await asyncio.ensure_future(callback(request, response))
