#!/usr/bin/python3
"""Write a candidate vpn-router.json to the path given on the command line.

Called from postinst, which hands the result to ucf(1). ucf compares it with
/etc/vpn-router/vpn-router.json and decides what to do about local changes, so
this script never writes to /etc and never has to worry about destroying an
administrator's edits.

The candidate is the current /etc/vpn-router/vpn-router.json as it is, or the
shipped defaults when there is none, with every debconf answer that was given
written over it. Without an answer that changes it, the candidate is the current
file byte for byte, so
an upgrade or a reconfiguration that changes nothing gives ucf nothing to ask;
defaults the file leaves out are applied by vpnrouter.load_config() when the
file is read. An empty answer means "leave alone", which is why clearing the
pre-shared key from the debconf database after installation does not blank it
on the next dpkg-reconfigure.

Answers arrive as VPN_ROUTER_* environment variables and are converted to the
type of the setting they fill: comma-separated lists become arrays, "true" and
"false" become booleans and digits become integers. The site-to-site answers
fill the connection vpnrouter.FIRST_CONNECTION of site_to_site; the local
subnets fill both its local_cidrs and local.cidrs.
"""

import json
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))

import vpnrouter

FIRST = ('site_to_site', vpnrouter.FIRST_CONNECTION)

# environment variable -> the settings it fills, each (section, option) or (site_to_site, name, option)
ANSWERS = {
    'VPN_ROUTER_PLATFORM':        (('general', 'platform'),),
    'VPN_ROUTER_MODE':            (('general', 'mode'),),
    'VPN_ROUTER_INT_ADDR':        (('local', 'int_addr'),),
    'VPN_ROUTER_EXTERNAL_INTERFACE': (('interfaces', 'external'),),
    'VPN_ROUTER_INTERNAL_INTERFACE': (('interfaces', 'internal'),),
    'VPN_ROUTER_LOCAL_FQDN':      (('wan', 'local_fqdn'),),
    'VPN_ROUTER_LOCAL_ID_MODE':   ((*FIRST, 'local_id_mode'),),
    'VPN_ROUTER_LOCAL_CIDRS':     (('local', 'cidrs'), (*FIRST, 'local_cidrs')),
    'VPN_ROUTER_INT_GATEWAY_IP':  (('local', 'int_gateway_ip'),),
    'VPN_ROUTER_REMOTE_ADDRS':    ((*FIRST, 'addrs'),),
    'VPN_ROUTER_REMOTE_ID':       ((*FIRST, 'id'),),
    'VPN_ROUTER_REMOTE_CIDRS':    ((*FIRST, 'cidrs'),),
    'VPN_ROUTER_PSK':             ((*FIRST, 'psk'),),
    'VPN_ROUTER_P2S_ENABLED':     (('p2s', 'enabled'),),
    'VPN_ROUTER_P2S_ADDRESS_POOL': (('p2s', 'address_pool'),),
    'VPN_ROUTER_P2S_CA_NAME':     (('p2s', 'ca_name'),),
    'VPN_ROUTER_WG_ENABLED':      (('wireguard', 'enabled'),),
    'VPN_ROUTER_WG_ADDRESS':      (('wireguard', 'address'),),
    'VPN_ROUTER_WG_LISTEN_PORT':  (('wireguard', 'listen_port'),),
}


def main():
    if len(sys.argv) != 2:
        print('usage: generate-config OUTPUT', file=sys.stderr)
        return 2
    output = sys.argv[1]

    try:
        current = vpnrouter.CONFIG_FILE.read_text(encoding='utf-8')
    except FileNotFoundError:
        current = None
    if current is None:
        cfg = vpnrouter.load_config()
    else:
        try:
            cfg = json.loads(current)
        except ValueError as exc:
            vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {exc}')
        if not isinstance(cfg, dict):
            vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: the top level is not an object')

    answered = False
    for variable, paths in ANSWERS.items():
        answer = os.environ.get(variable)
        if not answer:
            continue
        for path in paths:
            # A value that does not convert is kept as a string for validate() to report.
            default = vpnrouter.CONNECTION[path[2]] if path[0] == 'site_to_site' else vpnrouter.SCHEMA[path[0]][path[1]]
            value = answer
            if isinstance(default, bool):
                value = value == 'true'
            elif isinstance(default, int):
                value = int(value) if value.isdigit() else value
            elif isinstance(default, list):
                value = [item.strip() for item in value.split(',') if item.strip()]
            target = cfg
            for key in path[:-1]:
                if not isinstance(target.setdefault(key, {}), dict):
                    vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {".".join(path[:-1])} is not an object')
                target = target[key]
            if target.get(path[-1]) != value:
                target[path[-1]] = value
                answered = True

    # Without a change the candidate is the current file byte for byte, as ucf compares checksums.
    with open(output, 'w', encoding='utf-8') as handle:
        if current is not None and not answered:
            handle.write(current)
        else:
            json.dump(cfg, handle, indent=2)
            handle.write('\n')
    os.chmod(output, 0o600)
    return 0


if __name__ == '__main__':
    sys.exit(main())
