from multiprocessing import Pipe, Process
from multiprocessing import connection as mp_connection

import click
import fabric

from .hostinfo import HostInfo, HostInfoList


def run_on_host(
    hostinfo: HostInfo,
    workdir: str,
    recv_conn: mp_connection.Connection,
    send_conn: mp_connection.Connection,
    env: dict,
) -> None:
    """
    Use fabric connection to execute command on local or remote hosts.

    Args:
        hostinfo (HostInfo): host information
        workdir (str): the directory to execute the command
        recv_conn (multiprocessing.connection.Connection): receive messages from the master sender
        send_conn (multiprocessing.connection.Connection): send messages to the master receiver
        env (dict): a dictionary for environment variables
    """

    fab_conn = fabric.Connection(hostinfo.hostname, port=hostinfo.port)
    finish = False
    env_msg = " ".join([f'{k}="{v}"' for k, v in env.items()])

    # keep listening until exit
    while not finish:
        # receive cmd
        cmds = recv_conn.recv()

        if cmds == "exit":
            # exit from the loop
            finish = True
            break
        else:
            # execute the commands
            try:
                # cd to execute directory
                with fab_conn.cd(workdir):
                    # propagate the runtime environment
                    with fab_conn.prefix(f"export {env_msg}"):
                        if hostinfo.is_local_host:
                            # execute on the local machine
                            fab_conn.local(cmds, hide=False)
                        else:
                            # execute on the remote machine
                            fab_conn.run(cmds, hide=False)
                    send_conn.send("success")
            except Exception as e:
                click.echo(
                    f"Error: failed to run {cmds} on {hostinfo.hostname}, is localhost: {hostinfo.is_local_host}, exception: {e}"
                )
                send_conn.send("failure")

    # shutdown
    send_conn.send("finish")
    fab_conn.close()


class MultiNodeRunner:
    """
    A runner to execute commands on an array of machines. This runner
    is inspired by Nezha (https://github.com/zhuzilin/NeZha).
    """

    def __init__(self):
        self.processes = {}
        self.master_send_conns = {}
        self.master_recv_conns = {}

    def connect(self, host_info_list: HostInfoList, workdir: str, env: dict) -> None:
        """
        Establish connections to a list of hosts

        Args:
            host_info_list (HostInfoList): a list of HostInfo objects
            workdir (str): the directory where command is executed
            env (dict): environment variables to propagate to hosts
        """
        for hostinfo in host_info_list:
            master_send_conn, worker_recv_conn = Pipe()
            master_recv_conn, worker_send_conn = Pipe()
            p = Process(target=run_on_host, args=(hostinfo, workdir, worker_recv_conn, worker_send_conn, env))
            p.start()
            self.processes[hostinfo.hostname] = p
            self.master_recv_conns[hostinfo.hostname] = master_recv_conn
            self.master_send_conns[hostinfo.hostname] = master_send_conn

    def send(self, hostinfo: HostInfo, cmd: str) -> None:
        """
        Send a command to a local/remote host.

        Args:
            hostinfo (HostInfo): host information
            cmd (str): the command to execute
        """

        assert hostinfo.hostname in self.master_send_conns, f"{hostinfo} is not found in the current connections"
        conn = self.master_send_conns[hostinfo.hostname]
        conn.send(cmd)

    def stop_all(self) -> None:
        """
        Stop connections to all hosts.
        """

        for hostname, conn in self.master_send_conns.items():
            conn.send("exit")

    def recv_from_all(self) -> dict:
        """
        Receive messages from all hosts

        Returns:
            msg_from_node (dict): a dictionary which contains messages from each node
        """

        msg_from_node = dict()
        for hostname, conn in self.master_recv_conns.items():
            msg_from_node[hostname] = conn.recv()
        return msg_from_node
