"""
The Evennia Server service acts as an AMP-client when talking to the
Portal. This module sets up the Client-side communication.

"""

import os

from django.conf import settings
from twisted.internet import protocol

import evennia
from evennia.server.portal import amp
from evennia.utils import logger
from evennia.utils.utils import class_from_module


class AMPClientFactory(protocol.ReconnectingClientFactory):
    """
    This factory creates an instance of an AMP client connection. This handles communication from
    the be the Evennia 'Server' service to the 'Portal'. The client will try to auto-reconnect on a
    connection error.

    """

    # Initial reconnect delay in seconds.
    initialDelay = 1
    factor = 1.5
    maxDelay = 1
    noisy = False

    def __init__(self, server):
        """
        Initializes the client factory.

        Args:
            server (server): server instance.

        """
        self.server = server
        self.protocol = class_from_module(settings.AMP_CLIENT_PROTOCOL_CLASS)
        self.maxDelay = 10
        # not really used unless connecting to multiple servers, but
        # avoids having to check for its existence on the protocol
        self.broadcasts = []

    def startedConnecting(self, connector):
        """
        Called when starting to try to connect to the Portal AMP server.

        Args:
            connector (Connector): Twisted Connector instance representing
                this connection.

        """
        pass

    def buildProtocol(self, addr):
        """
        Creates an AMPProtocol instance when connecting to the AMP server.

        Args:
            addr (str): Connection address. Not used.

        """
        self.resetDelay()
        self.server.amp_protocol = AMPServerClientProtocol()
        self.server.amp_protocol.factory = self
        return self.server.amp_protocol

    def clientConnectionLost(self, connector, reason):
        """
        Called when the AMP connection to the MUD server is lost.

        Args:
            connector (Connector): Twisted Connector instance representing
                this connection.
            reason (str): Eventual text describing why connection was lost.

        """
        logger.log_info("Server disconnected from the portal.")
        protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)

    def clientConnectionFailed(self, connector, reason):
        """
        Called when an AMP connection attempt to the MUD server fails.

        Args:
            connector (Connector): Twisted Connector instance representing
                this connection.
            reason (str): Eventual text describing why connection failed.

        """
        logger.log_msg("Attempting to reconnect to Portal ...")
        protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)


class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
    """
    This protocol describes the Server service (acting as an AMP-client)'s communication with the
    Portal (which acts as the AMP-server)

    """

    # sending AMP data

    def connectionMade(self):
        """
        Called when a new connection is established.

        """
        # print("AMPClient new connection {}".format(self))
        info_dict = self.factory.server.get_info_dict()
        super().connectionMade()
        # first thing we do is to request the Portal to sync all sessions
        # back with the Server side. We also need the startup mode (reload, reset, shutdown)
        self.send_AdminServer2Portal(
            amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict
        )
        # run the intial setup if needed
        self.factory.server.run_initial_setup()

    def data_to_portal(self, command, sessid, **kwargs):
        """
        Send data across the wire to the Portal

        Args:
            command (AMP Command): A protocol send command.
            sessid (int): A unique Session id.
            kwargs (any): Any data to pickle into the command.

        Returns:
            deferred (deferred or None): A deferred with an errback.

        Notes:
            Data will be sent across the wire pickled as a tuple
            (sessid, kwargs).

        """
        # print("server data_to_portal: {}, {}, {}".format(command, sessid, kwargs))
        return self.callRemote(command, packed_data=amp.dumps((sessid, kwargs))).addErrback(
            self.errback, command.key
        )

    def send_MsgServer2Portal(self, session, **kwargs):
        """
        Access method - executed on the Server for sending data
            to Portal.

        Args:
            session (Session): Unique Session.
            kwargs (any, optiona): Extra data.

        """
        return self.data_to_portal(amp.MsgServer2Portal, session.sessid, **kwargs)

    def send_AdminServer2Portal(self, session, operation="", **kwargs):
        """
        Administrative access method called by the Server to send an
        instruction to the Portal.

        Args:
            session (Session): Session.
            operation (char, optional): Identifier for the server
                operation, as defined by the global variables in
                `evennia/server/amp.py`.
            kwargs (dict, optional): Data going into the adminstrative.

        """
        return self.data_to_portal(
            amp.AdminServer2Portal, session.sessid, operation=operation, **kwargs
        )

    # receiving AMP data

    @amp.MsgStatus.responder
    def server_receive_status(self, question):
        return {"status": "OK"}

    @amp.MsgPortal2Server.responder
    @amp.catch_traceback
    def server_receive_msgportal2server(self, packed_data):
        """
        Receives message arriving to server. This method is executed
        on the Server.

        Args:
            packed_data (str): Data to receive (a pickled tuple (sessid,kwargs))

        """
        sessid, kwargs = self.data_in(packed_data)
        session = evennia.SERVER_SESSION_HANDLER.get(sessid, None)
        if session:
            evennia.SERVER_SESSION_HANDLER.data_in(session, **kwargs)
        return {}

    @amp.AdminPortal2Server.responder
    @amp.catch_traceback
    def server_receive_adminportal2server(self, packed_data):
        """
        Receives admin data from the Portal (allows the portal to
        perform admin operations on the server). This is executed on
        the Server.

        Args:
            packed_data (str): Incoming, pickled data.

        """
        sessid, kwargs = self.data_in(packed_data)
        operation = kwargs.pop("operation", "")

        if operation == amp.PCONN:  # portal_session_connect
            # create a new session and sync it
            evennia.SERVER_SESSION_HANDLER.portal_connect(kwargs.get("sessiondata"))

        elif operation == amp.PCONNSYNC:  # portal_session_sync
            evennia.SERVER_SESSION_HANDLER.portal_session_sync(kwargs.get("sessiondata"))

        elif operation == amp.PDISCONN:  # portal_session_disconnect
            # session closed from portal sid
            session = evennia.SERVER_SESSION_HANDLER.get(sessid)
            if session:
                evennia.SERVER_SESSION_HANDLER.portal_disconnect(session)

        elif operation == amp.PDISCONNALL:  # portal_disconnect_all
            # portal orders all sessions to close
            evennia.SERVER_SESSION_HANDLER.portal_disconnect_all()

        elif operation == amp.PSYNC:  # portal_session_sync
            # force a resync of sessions from the portal side. This happens on
            # first server-connect.
            server_restart_mode = kwargs.get("server_restart_mode", "shutdown")
            evennia.EVENNIA_SERVER_SERVICE.run_init_hooks(server_restart_mode)
            evennia.SERVER_SESSION_HANDLER.portal_sessions_sync(kwargs.get("sessiondata"))
            evennia.SERVER_SESSION_HANDLER.portal_start_time = kwargs.get("portal_start_time")

        elif operation == amp.SRELOAD:  # server reload
            # shut down in reload mode
            evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync()
            evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="reload")

        elif operation == amp.SRESET:
            # shut down in reset mode
            evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync()
            evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="reset")

        elif operation == amp.SSHUTD:  # server shutdown
            # shutdown in stop mode
            evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="shutdown")

        else:
            raise Exception("operation %(op)s not recognized." % {"op": operation})

        return {}
