"""
Ported to Python 3.
"""

__all__ = [
    "RunOptions",
    "run",
]

import os, sys
from allmydata.scripts.common import BasedirOptions
from twisted.scripts import twistd
from twisted.python import usage
from twisted.python.filepath import FilePath
from twisted.python.reflect import namedAny
from twisted.python.failure import Failure
from twisted.internet.defer import maybeDeferred, Deferred
from twisted.internet.protocol import Protocol
from twisted.internet.stdio import StandardIO
from twisted.internet.error import ReactorNotRunning
from twisted.application.service import Service

from allmydata.scripts.default_nodedir import _default_nodedir
from allmydata.util.encodingutil import listdir_unicode, quote_local_unicode_path
from allmydata.util.configutil import UnknownConfigError
from allmydata.util.deferredutil import HookMixin
from allmydata.util.pid import (
    parse_pidfile,
    check_pid_process,
    cleanup_pidfile,
    ProcessInTheWay,
    InvalidPidFile,
)
from allmydata.storage.crawler import (
    MigratePickleFileError,
)
from allmydata.storage_client import (
    MissingPlugin,
)
from allmydata.node import (
    PortAssignmentRequired,
    PrivacyError,
)


def get_pidfile(basedir):
    """
    Returns the path to the PID file.
    :param basedir: the node's base directory
    :returns: the path to the PID file
    """
    return os.path.join(basedir, u"running.process")


def get_pid_from_pidfile(pidfile):
    """
    Tries to read and return the PID stored in the node's PID file

    :param pidfile: try to read this PID file
    :returns: A numeric PID on success, ``None`` if PID file absent or
              inaccessible, ``-1`` if PID file invalid.
    """
    try:
        pid, _ = parse_pidfile(pidfile)
    except EnvironmentError:
        return None
    except InvalidPidFile:
        return -1

    return pid


def identify_node_type(basedir):
    """
    :return unicode: None or one of: 'client' or 'introducer'.
    """
    tac = u''
    try:
        for fn in listdir_unicode(basedir):
            if fn.endswith(u".tac"):
                tac = fn
                break
    except OSError:
        return None

    for t in (u"client", u"introducer"):
        if t in tac:
            return t
    return None


class RunOptions(BasedirOptions):
    subcommand_name = "run"

    optParameters = [
        ("basedir", "C", None,
         "Specify which Tahoe base directory should be used."
         " This has the same effect as the global --node-directory option."
         " [default: %s]" % quote_local_unicode_path(_default_nodedir)),
        ]

    optFlags = [
        ("allow-stdin-close", None,
         'Do not exit when stdin closes ("tahoe run" otherwise will exit).'),
    ]

    def parseArgs(self, basedir=None, *twistd_args):
        # This can't handle e.g. 'tahoe run --reactor=foo', since
        # '--reactor=foo' looks like an option to the tahoe subcommand, not to
        # twistd. So you can either use 'tahoe run' or 'tahoe run NODEDIR
        # --TWISTD-OPTIONS'. Note that 'tahoe --node-directory=NODEDIR run
        # --TWISTD-OPTIONS' also isn't allowed, unfortunately.

        BasedirOptions.parseArgs(self, basedir)
        self.twistd_args = twistd_args

    def getSynopsis(self):
        return ("Usage:  %s [global-options] %s [options]"
                " [NODEDIR [twistd-options]]"
                % (self.command_name, self.subcommand_name))

    def getUsage(self, width=None):
        t = BasedirOptions.getUsage(self, width) + "\n"
        twistd_options = str(MyTwistdConfig()).partition("\n")[2].partition("\n\n")[0]
        t += twistd_options.replace("Options:", "twistd-options:", 1)
        t += """

Note that if any twistd-options are used, NODEDIR must be specified explicitly
(not by default or using -C/--basedir or -d/--node-directory), and followed by
the twistd-options.
"""
        return t


class MyTwistdConfig(twistd.ServerOptions):
    subCommands = [("DaemonizeTahoeNode", None, usage.Options, "node")]

    stderr = sys.stderr


class DaemonizeTheRealService(Service, HookMixin):
    """
    this HookMixin should really be a helper; our hooks:

    - 'running': triggered when startup has completed; it triggers
        with None of successful or a Failure otherwise.
    """
    stderr = sys.stderr

    def __init__(self, nodetype, basedir, options):
        super(DaemonizeTheRealService, self).__init__()
        self.nodetype = nodetype
        self.basedir = basedir
        # setup for HookMixin
        self._hooks = {
            "running": None,
        }
        self.stderr = options.parent.stderr
        self._close_on_stdin_close = False if options["allow-stdin-close"] else True

    def startService(self):

        from twisted.internet import reactor

        def start():
            node_to_instance = {
                u"client": lambda: maybeDeferred(namedAny("allmydata.client.create_client"), self.basedir),
                u"introducer": lambda: maybeDeferred(namedAny("allmydata.introducer.server.create_introducer"), self.basedir),
            }

            try:
                service_factory = node_to_instance[self.nodetype]
            except KeyError:
                raise ValueError("unknown nodetype %s" % self.nodetype)

            def handle_config_error(reason):
                if reason.check(UnknownConfigError):
                    self.stderr.write("\nConfiguration error:\n{}\n\n".format(reason.value))
                elif reason.check(PortAssignmentRequired):
                    self.stderr.write("\ntub.port cannot be 0: you must choose.\n\n")
                elif reason.check(PrivacyError):
                    self.stderr.write("\n{}\n\n".format(reason.value))
                elif reason.check(MigratePickleFileError):
                    self.stderr.write(
                        "Error\nAt least one 'pickle' format file exists.\n"
                        "The file is {}\n"
                        "You must either delete the pickle-format files"
                        " or migrate them using the command:\n"
                        "    tahoe admin migrate-crawler --basedir {}\n\n"
                        .format(
                            reason.value.args[0].path,
                            self.basedir,
                        )
                    )
                elif reason.check(MissingPlugin):
                    self.stderr.write(
                        "Missing Plugin\n"
                        "The configuration requests a plugin:\n"
                        "\n    {}\n\n"
                        "...which cannot be found.\n"
                        "This typically means that some software hasn't been installed or the plugin couldn't be instantiated.\n\n"
                        .format(
                            reason.value.plugin_name,
                        )
                    )
                else:
                    self.stderr.write("\nUnknown error, here's the traceback:\n")
                    reason.printTraceback(self.stderr)
                reactor.stop()

            d = service_factory()

            def created(srv):
                if self.parent is not None:
                    srv.setServiceParent(self.parent)
                # exiting on stdin-closed facilitates cleanup when run
                # as a subprocess
                if self._close_on_stdin_close:
                    on_stdin_close(reactor, reactor.stop)
            d.addCallback(created)
            d.addErrback(handle_config_error)
            d.addBoth(self._call_hook, 'running')
            return d

        reactor.callWhenRunning(start)


class DaemonizeTahoeNodePlugin(object):
    tapname = "tahoenode"
    def __init__(self, nodetype, basedir, allow_stdin_close):
        self.nodetype = nodetype
        self.basedir = basedir
        self.allow_stdin_close = allow_stdin_close

    def makeService(self, so):
        so["allow-stdin-close"] = self.allow_stdin_close
        return DaemonizeTheRealService(self.nodetype, self.basedir, so)


def on_stdin_close(reactor, fn):
    """
    Arrange for the function `fn` to run when our stdin closes
    """
    when_closed_d = Deferred()

    class WhenClosed(Protocol):
        """
        Notify a Deferred when our connection is lost .. as this is passed
        to twisted's StandardIO class, it is used to detect our parent
        going away.
        """

        def connectionLost(self, reason):
            when_closed_d.callback(None)

    def on_close(arg):
        try:
            fn()
        except ReactorNotRunning:
            pass
        except Exception:
            # for our "exit" use-case failures will _mostly_ just be
            # ReactorNotRunning (because we're already shutting down
            # when our stdin closes) but no matter what "bad thing"
            # happens we just want to ignore it .. although other
            # errors might be interesting so we'll log those
            print(Failure())
        return arg

    when_closed_d.addBoth(on_close)
    # we don't need to do anything with this instance because it gets
    # hooked into the reactor and thus remembered .. but we return it
    # for Windows testing purposes.
    return StandardIO(
        proto=WhenClosed(),
        reactor=reactor,
    )


def run(reactor, config, runApp=twistd.runApp):
    """
    Runs a Tahoe-LAFS node in the foreground.

    Sets up the IService instance corresponding to the type of node
    that's starting and uses Twisted's twistd runner to disconnect our
    process from the terminal.
    """
    out = config.stdout
    err = config.stderr
    basedir = config['basedir']
    quoted_basedir = quote_local_unicode_path(basedir)
    print("'tahoe {}' in {}".format(config.subcommand_name, quoted_basedir), file=out)
    if not os.path.isdir(basedir):
        print("%s does not look like a directory at all" % quoted_basedir, file=err)
        return 1
    nodetype = identify_node_type(basedir)
    if not nodetype:
        print("%s is not a recognizable node directory" % quoted_basedir, file=err)
        return 1

    twistd_args = [
        # ensure twistd machinery does not daemonize.
        "--nodaemon",
        "--rundir", basedir,
    ]
    if sys.platform != "win32":
        # turn off Twisted's pid-file to use our own -- but not on
        # windows, because twistd doesn't know about pidfiles there
        twistd_args.extend(["--pidfile", None])
    twistd_args.extend(config.twistd_args)
    twistd_args.append("DaemonizeTahoeNode") # point at our DaemonizeTahoeNodePlugin

    twistd_config = MyTwistdConfig()
    twistd_config.stdout = out
    twistd_config.stderr = err
    try:
        twistd_config.parseOptions(twistd_args)
    except usage.error as ue:
        # these arguments were unsuitable for 'twistd'
        print(config, file=err)
        print("tahoe %s: usage error from twistd: %s\n" % (config.subcommand_name, ue), file=err)
        return 1
    twistd_config.loadedPlugins = {
        "DaemonizeTahoeNode": DaemonizeTahoeNodePlugin(nodetype, basedir, config["allow-stdin-close"])
    }

    # our own pid-style file contains PID and process creation time
    pidfile = FilePath(get_pidfile(config['basedir']))
    try:
        check_pid_process(pidfile)
    except (ProcessInTheWay, InvalidPidFile) as e:
        print("ERROR: {}".format(e), file=err)
        return 1
    else:
        reactor.addSystemEventTrigger(
            "after", "shutdown",
            lambda: cleanup_pidfile(pidfile)
        )

    # We always pass --nodaemon so twistd.runApp does not daemonize.
    print("running node in %s" % (quoted_basedir,), file=out)
    runApp(twistd_config)
    return 0
