#!/usr/bin/python3
"""Manage the users of the router's directory from the command line, with vpnrouter_users.

Passwords and TOTP secrets are read from the first line of standard input, never from the
command line. The
answer is text for people on standard output, and a failure is a message on standard error.
With --json the answer is JSON on standard output, and a failure is {"error": ...} there too.
The exit status says what failed: 2 the request is invalid, 3 the user does not exist, 4 the
user exists, 1 anything else. -h or --help shows the commands and their options.

  vpn-router-user [--json] list
  vpn-router-user [--json] add <name> --first-name F --last-name L --email E   (password on stdin)
  vpn-router-user [--json] add <name> [--email E]   (a Manager-only account; password on stdin)
  vpn-router-user [--json] update <name> [--first-name F] [--last-name L] [--email E]
  vpn-router-user [--json] passwd <name>       (password on stdin)
  vpn-router-user [--json] disable <name>
  vpn-router-user [--json] enable <name>
  vpn-router-user [--json] unlock <name>
  vpn-router-user [--json] remove <name>
  vpn-router-user [--json] profile <name>
  vpn-router-user [--json] otp-new <name>
  vpn-router-user [--json] otp-enrol <name> <code>   (secret on stdin)
  vpn-router-user [--json] otp-remove <name>
"""

import argparse
import json
import sys

sys.path.insert(0, '/usr/lib/vpn-router')

import ldap

import vpnrouter
import vpnrouter_auth
import vpnrouter_users
from vpnrouter_users import Failure

#: The help line of each command.
HELP = {
    'list': 'list the users',
    'add': 'add a VPN user with the three person options and issue the certificate, or without the names a '
           'Manager-only account, with an optional e-mail address; the password is read from standard input',
    'update': "change the first name, the last name or the e-mail address; a Manager-only account has only the "
              "e-mail address, which '' removes",
    'passwd': 'set the password, read from standard input',
    'disable': 'lock the user out of OpenVPN',
    'enable': 'let the user in again',
    'unlock': 'end a lockout after failed logins',
    'remove': 'revoke the certificate and remove the user',
    'profile': 'print the OpenVPN client profile',
    'otp-new': 'make a TOTP secret and its otpauth URI; nothing is stored',
    'otp-enrol': 'store the TOTP secret read from standard input, when the code is a code of it',
    'otp-remove': 'remove the TOTP token',
}

#: The help of the person options.
PERSON_HELP = {'first_name': 'the first name', 'last_name': 'the last name', 'email': 'the e-mail address'}


class Parser(argparse.ArgumentParser):
    """An argument parser whose errors are Failures with status 2, so that --json answers them as JSON."""

    def error(self, message):
        raise Failure(f'{message}; see {self.prog} --help', 2)


def parse(argv):
    parser = Parser(prog='vpn-router-user', description='Manage the users of the router.')
    parser.add_argument('--json', action='store_true', help='answer in JSON')
    commands = parser.add_subparsers(dest='command', required=True, metavar='command', parser_class=Parser)
    for command, text in HELP.items():
        sub = commands.add_parser(command, help=text, description=text[0].upper() + text[1:] + '.')
        if command != 'list':
            sub.add_argument('name', help='the user name')
        if command in ('add', 'update'):
            for key, _ in vpnrouter_users.PERSON:
                sub.add_argument(f'--{key.replace("_", "-")}', dest=key, metavar='TEXT', help=PERSON_HELP[key])
        if command == 'otp-enrol':
            sub.add_argument('code', help='a code of the secret from the authenticator')
    return parser.parse_args(argv)


def password():
    """The password or the TOTP secret from the first line of standard input."""
    return sys.stdin.readline().rstrip('\n')


def run(connection, options, secret):
    """The answer of the command the options name; secret is the password of add and passwd, the TOTP secret of otp-enrol."""
    command = options.command
    if command == 'list':
        return vpnrouter_users.list_users(connection)
    values = {key: getattr(options, key, None) for key, _ in vpnrouter_users.PERSON}
    if command == 'add':
        return vpnrouter_users.add(connection, options.name, secret, values)
    if command == 'update':
        return vpnrouter_users.update(connection, options.name, values)
    if command == 'passwd':
        return vpnrouter_users.passwd(connection, options.name, secret)
    if command == 'otp-enrol':
        return vpnrouter_users.otp_enrol(connection, options.name, secret, options.code)
    return getattr(vpnrouter_users, command.replace('-', '_'))(connection, options.name)


def text_of(answer):
    """The answer as text for people."""
    if 'profile' in answer:
        return answer['profile'].rstrip('\n')
    if 'secret' in answer:
        return f'secret  {answer["secret"]}\nuri     {answer["uri"]}'
    if answer.get('removed'):
        return f'user {answer["name"]} removed'
    users = answer['users'] if 'users' in answer else [answer]
    if not users:
        return 'no users'
    head = ('USER', 'KIND', 'STATE', 'CERTIFICATE', 'OTP', 'FIRST NAME', 'LAST NAME', 'E-MAIL')
    rows = [(user['name'], 'service' if user['service'] else 'manager' if user['manager_only'] else 'vpn',
             'disabled' if not user['enabled'] else 'locked' if user['locked'] else 'enabled',
             'issued' if user['certificate'] else 'missing',
             'enrolled' if user['otp'] else '-', user['first_name'] or '-', user['last_name'] or '-', user['email'] or '-')
            for user in users]
    widths = [max(len(row[column]) for row in (head, *rows)) for column in range(len(head))]
    return '\n'.join('  '.join(cell.ljust(width) for cell, width in zip(row, widths)).rstrip() for row in (head, *rows))


def main(argv):
    # A request that cannot be parsed is answered in JSON when --json is among the arguments.
    as_json = '--json' in argv
    try:
        options = parse(argv)
        as_json = options.json
        if options.command != 'list':
            vpnrouter_users.check_name(options.name)
        secret = password() if options.command in ('add', 'passwd', 'otp-enrol') else None
        with vpnrouter_users.session() as connection:
            answer = run(connection, options, secret)
        if options.command in vpnrouter_auth.REVOKING:
            vpnrouter_auth.revoke(vpnrouter.load_config(), options.name)
    except Failure as failure:
        message, status = str(failure), failure.status
    except (ldap.LDAPError, OSError) as exc:
        message, status = f'the user directory failed: {exc}', 1
    except vpnrouter_auth.DatabaseError as exc:
        message, status = str(exc), 1
    else:
        print(json.dumps(answer) if as_json else text_of(answer))
        return 0
    if as_json:
        print(json.dumps({'error': message}))
    else:
        print(f'vpn-router-user: {message}', file=sys.stderr)
    return status


if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
