"""
CLI entrypoint & parser configuration.

Builds on top of Invoke's core functionality for same.
"""

import getpass
from pathlib import Path

from paramiko import Agent
from invoke import Argument, Collection, Exit, Program
from invoke import __version__ as invoke
from paramiko._version import __version__ as paramiko

from . import __version__ as fabric
from . import Config, Executor


class Fab(Program):
    def print_version(self):
        super().print_version()
        print("Paramiko {}".format(paramiko))
        print("Invoke {}".format(invoke))

    def core_args(self):
        core_args = super().core_args()
        my_args = [
            Argument(
                names=("H", "hosts"),
                help="Comma-separated host name(s) to execute tasks against.",
            ),
            Argument(
                names=("i", "identity"),
                kind=list,  # Same as OpenSSH, can give >1 key
                # TODO: automatically add hint about iterable-ness to Invoke
                # help display machinery?
                help="Path to runtime SSH identity (key) file. May be given multiple times.",  # noqa
            ),
            Argument(
                names=("list-agent-keys",),
                kind=bool,
                help="Display ssh-agent key list, and exit.",
            ),
            # TODO: worth having short flags for these prompt args?
            Argument(
                names=("prompt-for-login-password",),
                kind=bool,
                help="Request an upfront SSH-auth password prompt.",
            ),
            Argument(
                names=("prompt-for-passphrase",),
                kind=bool,
                help="Request an upfront SSH key passphrase prompt.",
            ),
            Argument(
                names=("S", "ssh-config"),
                help="Path to runtime SSH config file.",
            ),
            Argument(
                names=("t", "connect-timeout"),
                kind=int,
                help="Specifies default connection timeout, in seconds.",
            ),
        ]
        return core_args + my_args

    @property
    def _remainder_only(self):
        # No 'unparsed' (i.e. tokens intended for task contexts), and remainder
        # (text after a double-dash) implies a contextless/taskless remainder
        # execution of the style 'fab -H host -- command'.
        # NOTE: must ALSO check to ensure the double dash isn't being used for
        # tab completion machinery...
        return (
            not self.core.unparsed
            and self.core.remainder
            and not self.args.complete.value
        )

    def load_collection(self):
        # Stick in a dummy Collection if it looks like we were invoked w/o any
        # tasks, and with a remainder.
        # This isn't super ideal, but Invoke proper has no obvious "just run my
        # remainder" use case, so having it be capable of running w/o any task
        # module, makes no sense. But we want that capability for testing &
        # things like 'fab -H x,y,z -- mycommand'.
        if self._remainder_only:
            # TODO: hm we're probably not honoring project-specific configs in
            # this branch; is it worth having it assume CWD==project, since
            # that's often what users expect? Even tho no task collection to
            # honor the real "lives by task coll"?
            self.collection = Collection()
        else:
            super().load_collection()

    def no_tasks_given(self):
        # As above, neuter the usual "hey you didn't give me any tasks, let me
        # print help for you" behavior, if necessary.
        if not self._remainder_only:
            super().no_tasks_given()

    def create_config(self):
        # Create config, as parent does, but with lazy=True to avoid our own
        # SSH config autoload. (Otherwise, we can't correctly load _just_ the
        # runtime file if one's being given later.)
        self.config = self.config_class(lazy=True)
        # However, we don't really want the parent class' lazy behavior (which
        # skips loading system/global invoke-type conf files) so we manually do
        # that here to match upstream behavior.
        self.config.load_base_conf_files()
        # And merge again so that data is available.
        # TODO: really need to either A) stop giving fucks about calling
        # merge() "too many times", or B) make merge() itself determine whether
        # it needs to run and/or just merge stuff that's changed, so log spam
        # isn't as bad.
        self.config.merge()

    def update_config(self):
        # Note runtime SSH path, if given, and load SSH configurations.
        # NOTE: must do parent before our work, in case users want to disable
        # SSH config loading within a runtime-level conf file/flag.
        super().update_config(merge=False)
        self.config.set_runtime_ssh_path(self.args["ssh-config"].value)
        self.config.load_ssh_config()
        # Load -i identity file, if given, into connect_kwargs, at overrides
        # level.
        connect_kwargs = {}
        paths = self.args["identity"].value
        if paths:
            connect_kwargs["key_filename"] = paths
            # New, non-sshclient based config location
            # Also new: Path! (which we couldn't use above until paramiko knew
            # about it)
            self.config._overrides["authentication"] = dict(
                identities=[Path(x) for x in paths]
            )
        # Ditto for connect timeout
        timeout = self.args["connect-timeout"].value
        if timeout:
            connect_kwargs["timeout"] = timeout
        # Secrets prompts that want to happen at handoff time instead of
        # later/at user-time.
        # TODO: should this become part of Invoke proper in case other
        # downstreams have need of it? E.g. a prompt Argument 'type'? We're
        # already doing a similar thing there for sudo password...
        if self.args["prompt-for-login-password"].value:
            prompt = "Enter login password for use with SSH auth: "
            connect_kwargs["password"] = getpass.getpass(prompt)
        if self.args["prompt-for-passphrase"].value:
            prompt = "Enter passphrase for use unlocking SSH keys: "
            connect_kwargs["passphrase"] = getpass.getpass(prompt)
        # TODO: this (directly manipulating _overrides) feels a little gross,
        # but since the parent has already called load_overrides, this is best
        # we can do for now w/o losing data. Still feels correct; just might be
        # cleaner to have even more Config API members around this sort of
        # thing. Shrug.
        self.config._overrides["connect_kwargs"] = connect_kwargs
        # Since we gave merge=False above, we must do it ourselves here. (Also
        # allows us to 'compile' our overrides manipulation.)
        self.config.merge()

    # TODO: make this an explicit hookpoint in Invoke, i.e. some default-noop
    # method called at the end of parse_core() that we can override here
    # instead of doing this.
    def parse_core(self, *args, **kwargs):
        super().parse_core(*args, **kwargs)
        if self.args["list-agent-keys"].value:
            keys = Agent().get_keys()
            for key in keys:
                tpl = "{} {} {} ({})"
                # TODO: _could_ use new PKey.__repr__ but I like the mimicry of
                # OpenSSH ssh-add -l for now...
                print(
                    tpl.format(
                        key.get_bits(),
                        key.fingerprint,
                        key.comment,
                        key.algorithm_name,
                    )
                )
            raise Exit


# Mostly a concession to testing.
def make_program():
    return Fab(
        name="Fabric",
        version=fabric,
        executor_class=Executor,
        config_class=Config,
    )


program = make_program()
