"""XML RPC server and client."""

import socketserver as SocketServer
import threading

from xmlrpc import server as SimpleXMLRPCServer
from xmlrpc import client as xmlrpclib
from xml.parsers import expat

from plaso.multi_process import logger
from plaso.multi_process import rpc


class XMLRPCClient(rpc.RPCClient):
  """XML RPC client."""

  _RPC_FUNCTION_NAME = ''

  def __init__(self):
    """Initializes a RPC client."""
    super().__init__()
    self._xmlrpc_proxy = None

  def CallFunction(self):
    """Calls the function via RPC."""
    if self._xmlrpc_proxy is None:
      return None

    rpc_call = getattr(self._xmlrpc_proxy, self._RPC_FUNCTION_NAME, None)
    if rpc_call is None:
      return None

    try:
      return rpc_call()  # pylint: disable=not-callable
    except (
        expat.ExpatError, SocketServer.socket.error,
        xmlrpclib.Fault) as exception:
      logger.warning(f'Unable to make RPC call with error: {exception!s}')
      return None

  def Close(self):
    """Closes the RPC communication channel to the server."""
    self._xmlrpc_proxy = None

  def Open(self, hostname, port):
    """Opens a RPC communication channel to the server.

    Args:
      hostname (str): hostname or IP address to connect to for requests.
      port (int): port to connect to for requests.

    Returns:
      bool: True if the communication channel was established.
    """
    server_url = f'http://{hostname:s}:{port:d}'

    try:
      self._xmlrpc_proxy = xmlrpclib.ServerProxy(
          server_url, allow_none=True)
    except SocketServer.socket.error as exception:
      logger.warning((
          f'Unable to connect to RPC server on {hostname:s}:{port:d} with '
          f'error: {exception!s}'))
      return False

    return True


class ThreadedXMLRPCServer(rpc.RPCServer):
  """Threaded XML RPC server."""

  _RPC_FUNCTION_NAME = ''
  _THREAD_NAME = ''

  def __init__(self, callback):
    """Initialize a threaded RPC server.

    Args:
      callback (function): callback function to invoke on get status RPC
          request.
    """
    super().__init__(callback)
    self._rpc_thread = None
    self._xmlrpc_server = None

  def _Close(self):
    """Closes the RPC communication channel for clients."""
    if self._xmlrpc_server:
      self._xmlrpc_server.shutdown()
      self._xmlrpc_server.server_close()
      self._xmlrpc_server = None

  def _Open(self, hostname, port):
    """Opens the RPC communication channel for clients.

    Args:
      hostname (str): hostname or IP address to connect to for requests.
      port (int): port to connect to for requests.

    Returns:
      bool: True if the communication channel was successfully opened.
    """
    try:
      self._xmlrpc_server = SimpleXMLRPCServer.SimpleXMLRPCServer(
          (hostname, port), logRequests=False, allow_none=True)
    except SocketServer.socket.error as exception:
      logger.warning((
          f'Unable to bind a RPC server on {hostname:s}:{port:d} with error: '
          f'{exception!s}'))
      return False

    self._xmlrpc_server.register_function(
        self._callback, self._RPC_FUNCTION_NAME)
    return True

  def Start(self, hostname, port):
    """Starts the process status RPC server.

    Args:
      hostname (str): hostname or IP address to connect to for requests.
      port (int): port to connect to for requests.

    Returns:
      bool: True if the RPC server was successfully started.
    """
    if not self._Open(hostname, port):
      return False

    self._rpc_thread = threading.Thread(
        name=self._THREAD_NAME, target=self._xmlrpc_server.serve_forever)
    self._rpc_thread.start()
    return True

  def Stop(self):
    """Stops the process status RPC server."""
    self._Close()

    if self._rpc_thread.is_alive():
      self._rpc_thread.join()
    self._rpc_thread = None


class XMLProcessStatusRPCClient(XMLRPCClient):
  """XML process status RPC client."""

  _RPC_FUNCTION_NAME = 'status'


class XMLProcessStatusRPCServer(ThreadedXMLRPCServer):
  """XML process status threaded RPC server."""

  _RPC_FUNCTION_NAME = 'status'
  _THREAD_NAME = 'process_status_rpc_server'
