#!/usr/bin/python3
"""Manage the router's S3-compatible object storage from the command line, with vpnrouter_s3.

The storage is versitygw, which runs while s3.enabled is true (vpn-config-edit set --restart s3.enabled true). Each
user or service account of the directory has at most one S3 account, whose secret is shown once, when it is made or
replaced; a bucket belongs to one account. 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, account or bucket does not exist, 4 it
exists or the state does not allow it, 1 anything else. -h or --help shows the commands and their options.

  vpn-router-s3 [--json] status
  vpn-router-s3 [--json] account list [--owner USER]
  vpn-router-s3 [--json] account add USER
  vpn-router-s3 [--json] account rotate USER
  vpn-router-s3 [--json] account remove USER
  vpn-router-s3 [--json] bucket list
  vpn-router-s3 [--json] bucket add NAME --owner USER
  vpn-router-s3 [--json] bucket owner NAME USER
  vpn-router-s3 [--json] bucket remove NAME
"""

import argparse
import json
import sys

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

import ldap

import vpnrouter
import vpnrouter_s3
import vpnrouter_users
from vpnrouter_s3 import Failure


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-s3', description="Manage the router's object storage.")
    parser.add_argument('--json', action='store_true', help='answer in JSON')
    commands = parser.add_subparsers(dest='command', required=True, metavar='command', parser_class=Parser)
    commands.add_parser('status', help='show whether the storage is on, installed and running, and its endpoint')
    account = commands.add_parser('account', help="the users' S3 accounts")
    accounts = account.add_subparsers(dest='action', required=True, metavar='action', parser_class=Parser)
    accounts.add_parser('list', help='list the accounts').add_argument('--owner', metavar='USER', help="only this user's")
    for action, text in (('add', "make the user's account and show its secret once"),
                         ('rotate', "replace the secret of the user's account and show it once"),
                         ('remove', "remove the user's account; its buckets keep it as their owner")):
        accounts.add_parser(action, help=text).add_argument('user', help='the user or service account')
    bucket = commands.add_parser('bucket', help='the buckets')
    buckets = bucket.add_subparsers(dest='action', required=True, metavar='action', parser_class=Parser)
    buckets.add_parser('list', help='list the buckets with their owners, sizes and objects')
    adder = buckets.add_parser('add', help='make a bucket owned by a user with an S3 account')
    adder.add_argument('name', help='the bucket name')
    adder.add_argument('--owner', required=True, metavar='USER', help='the owner')
    owner = buckets.add_parser('owner', help='give the bucket to another user')
    owner.add_argument('name', help='the bucket name')
    owner.add_argument('user', help='the new owner')
    buckets.add_parser('remove', help='remove the bucket, which must be empty').add_argument('name', help='the bucket name')
    return parser.parse_args(argv)


def run(options):
    """The answer of the command the options name."""
    if options.command == 'status':
        cfg = vpnrouter.load_config()
        return {'enabled': cfg['s3']['enabled'], 'version': vpnrouter_s3.VERSION, 'installed': vpnrouter_s3.installed(),
                'running': vpnrouter_s3.running(), 'endpoint': f'https://{cfg["wan"]["local_fqdn"]}:{cfg["s3"]["port"]}',
                'region': cfg['s3']['region']}
    if options.command == 'account':
        if options.action == 'list':
            return {'accounts': vpnrouter_s3.list_accounts(options.owner)}
        return {'add': vpnrouter_s3.account_add, 'rotate': vpnrouter_s3.account_rotate,
                'remove': vpnrouter_s3.account_remove}[options.action](options.user)
    if options.action == 'list':
        return {'buckets': vpnrouter_s3.buckets()}
    if options.action == 'add':
        return vpnrouter_s3.bucket_add(options.name, options.owner)
    if options.action == 'owner':
        return vpnrouter_s3.bucket_owner(options.name, options.user)
    return vpnrouter_s3.bucket_remove(options.name)


def table(head, rows):
    """Rows as text for people, in columns under the head."""
    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 text_of(answer):
    """The answer as text for people."""
    if 'endpoint' in answer:
        state = 'running' if answer['running'] else 'not running'
        return (f'Enabled: {"yes" if answer["enabled"] else "no"}\nversitygw: {answer["installed"] or "not installed"}'
                f' (pinned {answer["version"]}), {state}\nEndpoint: {answer["endpoint"]}\nRegion: {answer["region"]}')
    if 'secret_key' in answer:
        return f'owner       {answer["owner"]}\naccess key  {answer["access_key"]}\nsecret key  {answer["secret_key"]}'
    if 'accounts' in answer:
        rows = [(item['owner'], item['access_key'], item['created']) for item in answer['accounts']]
        return table(('OWNER', 'ACCESS KEY', 'CREATED'), rows) if rows else 'no accounts'
    if 'buckets' in answer:
        rows = [(item['name'], item['owner'] or f'({item["access_key"]})', str(item['size']), str(item['objects']))
                for item in answer['buckets']]
        return table(('BUCKET', 'OWNER', 'BYTES', 'OBJECTS'), rows) if rows else 'no buckets'
    if answer.get('removed'):
        return f'{answer.get("name") or answer.get("owner")} removed'
    return f'bucket {answer["name"]} owned by {answer["owner"]}'


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
        answer = run(options)
    except (Failure, vpnrouter_users.Failure) as failure:
        message, status = str(failure), failure.status
    except (ldap.LDAPError, OSError) as exc:
        message, status = f'the object storage failed: {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-s3: {message}', file=sys.stderr)
    return status


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