#!/usr/bin/python3
"""Show and change the router's configuration, /etc/vpn-router/vpn-router.json.

show prints the file as indented JSON; the file is not changed. set takes a setting as
section.option, such as web.port, or as site_to_site.NAME.option for a site-to-site connection,
which it creates with the defaults (vpnrouter.CONNECTION) when it does not exist, and a value,
which it converts to the type the package's schema (vpnrouter.SCHEMA) gives the setting: a whole
number, true or false, a list of strings separated by commas (an empty value is an empty list),
or a string. remove takes site_to_site.NAME and removes that connection. The configuration with
the change is checked as vpn-router-setup checks it, and only then written with
vpnrouter.save_config, holding vpnrouter.config_lock; vpn-router-setup applies it at its next
run, which set and remove remind of. With --restart they restart vpn-router-setup and wait until
it has applied the change. A failure is a message on standard error; the exit status is 2 for an
invalid request and 1 for any other failure, a failed run of vpn-router-setup included. -h or
--help shows the options.

  vpn-config-edit show
  vpn-config-edit set [--restart] SECTION.OPTION VALUE
  vpn-config-edit set [--restart] site_to_site.NAME.OPTION VALUE
  vpn-config-edit remove [--restart] site_to_site.NAME
"""

import argparse
import copy
import json
import subprocess
import sys

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

import vpnrouter


class Failure(Exception):
    """A request that cannot be done; status is 2 invalid, 1 other."""

    def __init__(self, message, status=1):
        super().__init__(message)
        self.status = status


class Parser(argparse.ArgumentParser):
    """An argument parser whose errors are Failures with status 2."""

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


def read():
    """The settings in the configuration file, without the defaults; {} when the file does not exist."""
    try:
        return vpnrouter.read_settings()
    except vpnrouter.ConfigError as exc:
        raise Failure(str(exc))


def converted(key, text):
    """The path of the setting key, section.option or site_to_site.NAME.option, and text as the type the schema gives it."""
    path = key.split('.')
    if path[0] == 'site_to_site' and len(path) == 3 and path[2] in vpnrouter.CONNECTION:
        kind = type(vpnrouter.CONNECTION[path[2]])
    elif path[0] != 'site_to_site' and len(path) == 2 and path[0] in vpnrouter.SCHEMA and path[1] in vpnrouter.SCHEMA[path[0]]:
        kind = type(vpnrouter.SCHEMA[path[0]][path[1]])
    else:
        raise Failure(f'{key}: unknown setting; settings are section.option, such as web.port, '
                      'or site_to_site.NAME.option', 2)
    if kind is bool:
        if text not in ('true', 'false'):
            raise Failure(f'{key} takes true or false', 2)
        return path, text == 'true'
    if kind is int:
        try:
            return path, int(text)
        except ValueError:
            raise Failure(f'{key} takes a whole number', 2)
    if kind is list:
        return path, [item.strip() for item in text.split(',') if item.strip()]
    return path, text


def main(argv):
    try:
        parser = Parser(prog='vpn-config-edit', description='Show and change the router configuration.')
        commands = parser.add_subparsers(dest='command', required=True, parser_class=Parser)
        commands.add_parser('show', help='print the configuration as indented JSON')
        setter = commands.add_parser('set', help='set a setting and write the configuration')
        setter.add_argument('key', metavar='SECTION.OPTION',
                            help='the setting, such as web.port or site_to_site.NAME.option')
        setter.add_argument('value', metavar='VALUE', help='its value; a list separated by commas')
        remover = commands.add_parser('remove', help='remove a site-to-site connection and write the configuration')
        remover.add_argument('key', metavar='site_to_site.NAME', help='the connection')
        for changer in (setter, remover):
            changer.add_argument('--restart', action='store_true',
                                 help='restart vpn-router-setup and wait until it has applied the change')
        options = parser.parse_args(argv)

        if options.command == 'show':
            print(json.dumps(read(), indent=2))
            return 0

        with vpnrouter.config_lock():
            data = read()
            if options.command == 'set':
                path, value = converted(options.key, options.value)
                if path[0] == 'site_to_site':
                    connections = data.setdefault('site_to_site', {})
                    connection = connections.setdefault(path[1], copy.deepcopy(vpnrouter.CONNECTION))
                    if not isinstance(connection, dict):
                        raise Failure(f'site_to_site.{path[1]} is not an object')
                    connection[path[2]] = value
                else:
                    data.setdefault(path[0], {})[path[1]] = value
            else:
                section, _, name = options.key.partition('.')
                if section != 'site_to_site' or not name or name not in data.get('site_to_site', {}):
                    raise Failure(f'{options.key}: no such site-to-site connection', 2)
                del data['site_to_site'][name]
            try:
                vpnrouter.validate(vpnrouter.with_defaults(data))
            except vpnrouter.ConfigError as exc:
                raise Failure(str(exc), 2)
            vpnrouter.save_config(data)
    except Failure as failure:
        print(f'vpn-config-edit: {failure}', file=sys.stderr)
        return failure.status
    except OSError as exc:
        print(f'vpn-config-edit: {exc}', file=sys.stderr)
        return 1
    if options.command == 'set':
        print(f'{options.key} set to {json.dumps(value)} in {vpnrouter.CONFIG_FILE}.')
    else:
        print(f'{options.key} removed from {vpnrouter.CONFIG_FILE}.')
    if not options.restart:
        print('Apply it with: systemctl restart vpn-router-setup, or --restart')
        return 0
    print('Restarting vpn-router-setup...', flush=True)
    if subprocess.run(['systemctl', 'restart', 'vpn-router-setup.service']).returncode != 0:
        print('vpn-config-edit: vpn-router-setup failed; see journalctl -u vpn-router-setup', file=sys.stderr)
        return 1
    print('vpn-router-setup applied it.')
    return 0


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