#!/usr/bin/env python3
"""
Add keys to SSH agent

Usage:
    addsshkeys [options] [<config>]

Options:
    -v, --verbose    list the keys as they are being added to ssh agent
    -l, --list       list the keys without adding them to ssh agent

Configurations can be found in: {settings_dir}.
They are Python files.  The default configuration is {config_filename}.

Currently there is only one setting available: ssh_keys.  It contains
a dictionary of dictionaries that contains information about each key.  The
primary dictionary contains a name and the values for each key. The values are
held in a dictionary that may contain three fields: paths, account, and
passphrase.  The first, paths, is required and contains the paths to one or more
SSH private key files.  It may be a list of strings, or a single string that is
split.  If a relative path is given, it is relative to ~/.ssh.  The second,
account, gives the name of the avendesora account that holds passphrase for the
keys. If present, Avendesora will be queried for the passphrase. The third,
passphrase is required if account is not given, otherwise it is optional.  If
account is given, it is the name of the passphrase field in Avendesora, which
defaults to 'passcode'. If account is not given, it is the passphrase itself. In
this case, the settings file should only be readable by the user.

A description of how to configure and use this program can be found at
`<https://avendesora.readthedocs.io/en/latest/api.html#example-add-ssh-keys>_.
"""

from pathlib import Path

import pexpect
from appdirs import user_config_dir
from avendesora import PasswordError, PasswordGenerator
from docopt import docopt
from inform import (
    Error, Inform, codicil, comment, display, error, warn, os_error
)
import nestedtext as nt

__version__ = "0.4.0"
__released__ = "2020-10-19"

# Settings
# These cannot be overridden in ~/.config/addsshkeys/config
prog_name = "addsshkeys"
config_filename = "config"
settings_dir = user_config_dir(prog_name)

# These can be overridden in ~/.config/addsshkeys/config
ssh_add = "ssh-add"
ssh_keys = {}
config_file_mask = 0o077

# read command line
cmdline = docopt(__doc__.format(**locals()))
verbose = cmdline["--verbose"]
list_keys = cmdline["--list"]
config = cmdline["<config>"]
if not config:
    config = config_filename

try:
    # initialization
    Inform(verbose=verbose)
    settings_dir = Path(settings_dir)
    pw = None

    # read settings file
    config_filepath = Path(settings_dir, config)
    if config_filepath.exists():
        settings = nt.load(config_filepath, top='dict')
    else:
        # try again, this time with a .nt suffix
        config_filepath = config_filepath.with_suffix('.nt')
        if config_filepath.exists():
            settings = nt.load(config_filepath, top='dict')
        else:
            raise Error("config file not found.", culprit=config_filepath)
    settings['config_file_mask'] = int(
        settings.get('config_file_mask', config_file_mask),
        base = 8
    )
    locals().update(settings)

    if not ssh_keys:
        raise Error("no keys found.", culprit=config_filepath)

    # load keys
    for key, values in ssh_keys.items():
        try:
            paths = values["paths"].split()
        except AttributeError:
            paths = values["paths"]
        except KeyError:
            raise Error("missing paths.", culprit=key)
        account_name = values.get("account")
        passphrase = values.get("passphrase")
        if list_keys:
            display(f"{key}:")
            for path in paths:
                display(f"    {path}")
            continue
        try:
            if account_name:
                if not pw:
                    pw = PasswordGenerator()
                account = pw.get_account(account_name)
                if passphrase:
                    passphrase = str(account.get_value(passphrase).value)
                else:
                    passphrase = str(account.get_passcode().value)
            elif not passphrase:
                raise Error("missing passphrase.", culprit=key)
            else:
                # config file contains the passphrase, check its permissions
                permissions = config_filepath.stat().st_mode & 0o777
                violation = permissions & config_file_mask
                if violation:
                    recommended = permissions & ~config_file_mask
                    warn("file permissions are too loose.", culprit=config_filepath)
                    codicil(
                        "Recommend running: chmod {:o} {}".format(
                            recommended, config_filepath
                        )
                    )

            for path in paths:
                comment(f"{key:>15}: adding {path}")
                path = Path(path).expanduser()
                path = Path("~/.ssh", path).expanduser()
                try:
                    sshadd = pexpect.spawn(ssh_add, [str(path)])
                    sshadd.expect("Enter passphrase for %s: " % (path), timeout=4)
                    sshadd.sendline(passphrase)
                    sshadd.expect(pexpect.EOF)
                    sshadd.close()
                    response = sshadd.before.decode("utf-8")
                    if "identity added" in response.lower():
                        continue
                except (pexpect.EOF, pexpect.TIMEOUT):
                    pass
                error("failed.", culprit=key)
                codicil("response:", sshadd.before.decode("utf8"), culprit=ssh_add)
                codicil("exit status:", sshadd.exitstatus, culprit=ssh_add)
        except PasswordError as e:
            e.report(culprit=key)

except (Error, PasswordError) as e:
    e.report()
except OSError as e:
    error(os_error(e))
except KeyboardInterrupt:
    comment("Killed by user.")
