import re
from contextlib import contextmanager
from tempfile import NamedTemporaryFile

from plumbum.commands import CommandNotFound, ConcreteCommand, shquote
from plumbum.lib import ProcInfo
from plumbum.machines.base import BaseMachine
from plumbum.machines.env import BaseEnv
from plumbum.machines.local import LocalPath
from plumbum.path.remote import RemotePath, RemoteWorkdir, StatRes


class RemoteEnv(BaseEnv):
    """The remote machine's environment; exposes a dict-like interface"""

    __slots__ = ["_orig", "remote"]

    def __init__(self, remote):
        session = remote._session
        # GNU env has a -0 argument; use it if present. Otherwise,
        # fall back to calling printenv on each (possible) variable
        # from plain env.
        env0 = session.run("env -0; echo")
        if env0[0] == 0 and not env0[2].rstrip():
            _curr = dict(
                line.split("=", 1) for line in env0[1].split("\x00") if "=" in line
            )
        else:
            lines = session.run("env; echo")[1].splitlines()
            split = (line.split("=", 1) for line in lines)
            keys = (line[0] for line in split if len(line) > 1)
            runs = ((key, session.run(f'printenv "{key}"; echo')) for key in keys)
            _curr = {
                key: run[1].rstrip("\n")
                for (key, run) in runs
                if run[0] == 0 and run[1].rstrip("\n") and not run[2]
            }

        super().__init__(remote.path, ":", _curr=_curr)
        self.remote = remote
        self._orig = self._curr.copy()

    def __delitem__(self, name):
        BaseEnv.__delitem__(self, name)
        self.remote._session.run(f"unset {name}")

    def __setitem__(self, name, value):
        BaseEnv.__setitem__(self, name, value)
        self.remote._session.run(f"export {name}={shquote(value)}")

    def pop(self, name, *default):
        BaseEnv.pop(self, name, *default)
        self.remote._session.run(f"unset {name}")

    def update(self, *args, **kwargs):
        BaseEnv.update(self, *args, **kwargs)
        self.remote._session.run(
            "export " + " ".join(f"{k}={shquote(v)}" for k, v in self.getdict().items())
        )

    def expand(self, expr):
        """Expands any environment variables and home shortcuts found in ``expr``
        (like ``os.path.expanduser`` combined with ``os.path.expandvars``)

        :param expr: An expression containing environment variables (as ``$FOO``) or
                     home shortcuts (as ``~/.bashrc``)

        :returns: The expanded string"""
        return self.remote.expand(expr)

    def expanduser(self, expr):
        """Expand home shortcuts (e.g., ``~/foo/bar`` or ``~john/foo/bar``)

        :param expr: An expression containing home shortcuts

        :returns: The expanded string"""
        return self.remote.expanduser(expr)

    # def clear(self):
    #    BaseEnv.clear(self, *args, **kwargs)
    #    self.remote._session.run("export %s" % " ".join("%s=%s" % (k, v) for k, v in self.getdict()))

    def getdelta(self):
        """Returns the difference between the this environment and the original environment of
        the remote machine"""
        self._curr["PATH"] = self.path.join()

        delta = {}
        for k, v in self._curr.items():
            if k not in self._orig:
                delta[k] = str(v)
        for k, v in self._orig.items():
            if k not in self._curr:
                delta[k] = ""
            else:
                if v != self._curr[k]:
                    delta[k] = self._curr[k]

        return delta


class RemoteCommand(ConcreteCommand):
    __slots__ = ["remote", "executable"]
    QUOTE_LEVEL = 1

    def __init__(self, remote, executable, encoding="auto"):
        self.remote = remote
        ConcreteCommand.__init__(
            self, executable, remote.custom_encoding if encoding == "auto" else encoding
        )

    @property
    def machine(self):
        return self.remote

    def __repr__(self):
        return f"RemoteCommand({self.remote!r}, {self.executable!r})"

    def popen(self, args=(), **kwargs):
        return self.remote.popen(self[args], **kwargs)

    def nohup(self, cwd=".", stdout="nohup.out", stderr=None, append=True):
        """Runs a command detached."""
        return self.machine.daemonic_popen(self, cwd, stdout, stderr, append)


class ClosedRemoteMachine(Exception):
    pass


class ClosedRemote:
    __slots__ = ["_obj", "__weakref__"]

    def __init__(self, obj):
        self._obj = obj

    def close(self):
        pass

    def __getattr__(self, name):
        raise ClosedRemoteMachine(f"{self._obj!r} has been closed")


class BaseRemoteMachine(BaseMachine):
    """Represents a *remote machine*; serves as an entry point to everything related to that
    remote machine, such as working directory and environment manipulation, command creation,
    etc.

    Attributes:

    * ``cwd`` - the remote working directory
    * ``env`` - the remote environment
    * ``custom_encoding`` - the remote machine's default encoding (assumed to be UTF8)
    * ``connect_timeout`` - the connection timeout


    There also is a _cwd attribute that exists if the cwd is not current (del if cwd is changed).
    """

    # allow inheritors to override the RemoteCommand class
    RemoteCommand = RemoteCommand

    @property
    def cwd(self):
        if not hasattr(self, "_cwd"):
            self._cwd = RemoteWorkdir(self)
        return self._cwd

    def __init__(self, encoding="utf8", connect_timeout=10, new_session=False):
        self.custom_encoding = encoding
        self.connect_timeout = connect_timeout
        self._session = self.session(new_session=new_session)
        self.uname = self._get_uname()
        self.env = RemoteEnv(self)
        self._python = None
        self._program_cache = {}

    def _get_uname(self):
        rc, out, _ = self._session.run("uname", retcode=None)
        if rc == 0:
            return out.strip()

        rc, out, _ = self._session.run(
            "python3 -c 'import platform;print(platform.uname()[0])'", retcode=None
        )
        if rc == 0:
            return out.strip()

        # all POSIX systems should have uname. make an educated guess it's Windows
        return "Windows"

    def __repr__(self):
        return f"<{self.__class__.__name__} {self}>"

    def __enter__(self):
        return self

    def __exit__(self, t, v, tb):
        self.close()

    def close(self):
        """closes the connection to the remote machine; all paths and programs will
        become defunct"""
        self._session.close()
        self._session = ClosedRemote(self)

    def path(self, *parts):
        """A factory for :class:`RemotePaths <plumbum.path.remote.RemotePath>`.
        Usage: ``p = rem.path("/usr", "lib", "python2.7")``
        """
        parts2 = [str(self.cwd)]
        for p in parts:
            if isinstance(p, LocalPath):
                raise TypeError(f"Cannot construct RemotePath from {p!r}")
            parts2.append(self.expanduser(str(p)))
        return RemotePath(self, *parts2)

    def which(self, progname)
:
        """Looks up a program in the ``PATH``. If the program is not found, raises
        :class:`CommandNotFound <plumbum.commands.CommandNotFound>`

        :param progname: The program's name. Note that if underscores (``_``) are present
                         in the name, and the exact name is not found, they 
will be replaced
                         in turn by hyphens (``-``) then periods (``.``), and the name will
                         be looked up again for each alternative

        :returns: A :class:`RemotePath <plumbum.path.local.RemotePath>`
        """
        key = (progname, self.env.get("PATH", ""))

        try:
            return self._program_cache[key]
        except KeyError:
            pass

        alternatives = [progname]
        if "_" in progname:
            alternatives.append(progname.replace("_", "-"))
            alternatives.append(progname.replace("_", "."))
        for name in alternatives:
            for p in self.env.path:
                fn = p / name
                if f