#!/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, 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. The configuration with the
new value is checked as vpn-router-setup checks it, and only then written, indented, with mode
0600; vpn-router-setup applies it at its next run, which set reminds of. A failure is a message
on standard error; the exit status is 2 for an invalid request and 1 for any other failure.
-h or --help shows the options.

  vpn-config-edit show
  vpn-config-edit set SECTION.OPTION VALUE
"""

import argparse
import json
import os
import sys
import tempfile

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:
        data = json.loads(vpnrouter.CONFIG_FILE.read_text(encoding='utf-8'))
    except FileNotFoundError:
        return {}
    except ValueError as exc:
        raise Failure(f'cannot parse {vpnrouter.CONFIG_FILE}: {exc}')
    if not isinstance(data, dict) or not all(isinstance(options, dict) for options in data.values()):
        raise Failure(f'cannot parse {vpnrouter.CONFIG_FILE}: it is not an object of sections')
    return data


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


def write(data):
    """Write the settings, indented, with mode 0600, replacing the file in one step."""
    directory = vpnrouter.CONFIG_FILE.parent
    descriptor, name = tempfile.mkstemp(dir=directory, prefix='.vpn-router.json.')
    try:
        with os.fdopen(descriptor, 'w', encoding='utf-8') as handle:
            handle.write(json.dumps(data, indent=2) + '\n')
        os.chmod(name, 0o600)
        os.replace(name, vpnrouter.CONFIG_FILE)
    except OSError:
        os.unlink(name)
        raise


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')
        setter.add_argument('value', metavar='VALUE', help='its value; a list separated by commas')
        options = parser.parse_args(argv)

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

        section, _, option = options.key.partition('.')
        value = converted(options.key, options.value)
        data.setdefault(section, {})[option] = value
        # The configuration as vpn-router-setup reads it: the defaults with the file's settings.
        candidate = {name: dict(defaults) for name, defaults in vpnrouter.SCHEMA.items()}
        for name, settings in data.items():
            candidate.setdefault(name, {}).update(settings)
        try:
            vpnrouter.validate(candidate)
        except vpnrouter.ConfigError as exc:
            raise Failure(str(exc), 2)
        write(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
    print(f'{options.key} set to {json.dumps(value)} in {vpnrouter.CONFIG_FILE}.')
    print('Apply it with: systemctl restart vpn-router-setup')
    return 0


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