#!/usr/bin/python3
"""Install Caddy, the router's HTTPS proxy, from the source https.caddy_source names.

status shows the configured source, the source in effect and the installed and candidate versions of Caddy. apply
installs Caddy when it is missing: from the Ubuntu archive, or with upstream from Caddy's own repository, which it
configures first (vpnrouter.CADDY_APT_SOURCE, with the key in vpnrouter.CADDY_KEYRING). When upstream is configured
and the repository is not, it configures the repository and replaces the installed Caddy with upstream's. It then
restarts vpn-router-setup without waiting, which serves with the new Caddy; vpn-router-caddy.service runs it for
vpn-router-setup. use-upstream sets https.caddy_source to upstream and applies it, printing each step.

Once the repository is configured Caddy stays upstream's: apt keeps the higher upstream version, and ubuntu has no
effect. A failure is a message on standard error and the exit status 1. -h or --help shows the commands.

  vpn-router-caddy [--json] status
  vpn-router-caddy apply
  vpn-router-caddy use-upstream
"""

import argparse
import json
import os
import subprocess
import sys
import urllib.request

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

import vpnrouter

#: apt-get, waiting up to ten minutes for dpkg's lock, which a package installation that runs setup holds.
APT = ['apt-get', '-o', 'DPkg::Lock::Timeout=600']

#: The options of apt-get install: no questions, and the configuration files of the installed Caddy kept.
INSTALL = ['-y', '-o', 'Dpkg::Options::=--force-confdef', '-o', 'Dpkg::Options::=--force-confold', 'install']

#: Seconds the key of Caddy's repository may take to download.
KEY_TIMEOUT = 60


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

    def error(self, message):
        print(f'{self.prog}: {message}; see {self.prog} --help', file=sys.stderr)
        sys.exit(2)


def step(message):
    print(message, flush=True)


def installed():
    """The installed version of caddy, or None when it is not installed."""
    result = subprocess.run(['dpkg-query', '-W', '-f=${Status}\t${Version}', 'caddy'], capture_output=True, text=True)
    status, _, version = result.stdout.partition('\t')
    return version if result.returncode == 0 and status == 'install ok installed' else None


def candidate():
    """The version of caddy apt would install, or None when it knows none."""
    result = subprocess.run(['apt-cache', 'policy', 'caddy'], capture_output=True, text=True)
    for line in result.stdout.splitlines():
        if line.strip().startswith('Candidate:'):
            value = line.split(':', 1)[1].strip()
            return None if value == '(none)' else value
    return None


def effective():
    """The source Caddy comes from: upstream once its repository is configured, ubuntu otherwise."""
    return 'upstream' if vpnrouter.CADDY_APT_SOURCE.is_file() else 'ubuntu'


def command(args):
    """Run a command with its output shown; SystemExit when it fails."""
    step(f'$ {" ".join(args)}')
    result = subprocess.run(args, env=dict(os.environ, DEBIAN_FRONTEND='noninteractive'))
    if result.returncode != 0:
        sys.exit(f'vpn-router-caddy: {args[0]} failed with status {result.returncode}')


def apply():
    """Install Caddy, or replace it with upstream's, as https.caddy_source asks (see the module)."""
    source = vpnrouter.load_config()['https']['caddy_source']
    repository = source == 'upstream' and not vpnrouter.CADDY_APT_SOURCE.is_file()
    if installed() and not repository:
        if source == 'ubuntu' and effective() == 'upstream':
            step("https.caddy_source is ubuntu, but Caddy's repository is configured, so Caddy stays upstream's.")
        else:
            step(f'Caddy {installed()} is installed from the {source} source.')
        return
    if repository:
        step(f"Downloading the key of Caddy's repository from {vpnrouter.CADDY_KEY_URL}")
        with urllib.request.urlopen(vpnrouter.CADDY_KEY_URL, timeout=KEY_TIMEOUT) as response:
            key = response.read()
        if not key.lstrip().startswith(b'-----BEGIN PGP PUBLIC KEY BLOCK-----'):
            sys.exit(f'vpn-router-caddy: {vpnrouter.CADDY_KEY_URL} did not answer with an OpenPGP public key')
        vpnrouter.CADDY_KEYRING.parent.mkdir(mode=0o755, parents=True, exist_ok=True)
        vpnrouter.CADDY_KEYRING.write_bytes(key)
        os.chmod(vpnrouter.CADDY_KEYRING, 0o644)
        vpnrouter.CADDY_APT_SOURCE.write_text(
            f'Types: deb\nURIs: {vpnrouter.CADDY_REPOSITORY}\nSuites: any-version\nComponents: main\n'
            f'Signed-By: {vpnrouter.CADDY_KEYRING}\n', encoding='utf-8')
        os.chmod(vpnrouter.CADDY_APT_SOURCE, 0o644)
        step(f"Caddy's repository is configured in {vpnrouter.CADDY_APT_SOURCE}.")
    command([*APT, 'update'])
    command([*APT, *INSTALL, 'caddy'])
    step(f'Caddy {installed()} is installed from the {effective()} source.')
    subprocess.run(['systemctl', 'restart', '--no-block', 'vpn-router-setup.service'], capture_output=True, text=True)
    step('vpn-router-setup restarted.')


def main(argv):
    parser = Parser(prog='vpn-router-caddy', description='Install Caddy from the source https.caddy_source names.')
    parser.add_argument('--json', action='store_true', help='answer status in JSON')
    commands = parser.add_subparsers(dest='command', required=True, parser_class=Parser)
    commands.add_parser('status', help='show the sources and the versions of Caddy')
    commands.add_parser('apply', help='install Caddy, or replace it with upstream\'s, as https.caddy_source asks')
    commands.add_parser('use-upstream', help="switch to Caddy's own repository and replace Caddy with upstream's")
    options = parser.parse_args(argv)

    try:
        if options.command == 'status':
            document = {'configured': vpnrouter.load_config()['https']['caddy_source'], 'source': effective(),
                        'installed': installed(), 'candidate': candidate()}
            if options.json:
                print(json.dumps(document, indent=2))
            else:
                print(f'Configured source: {document["configured"]}\nSource in effect: {document["source"]}\n'
                      f'Installed: {document["installed"] or "no"}\nCandidate: {document["candidate"] or "none"}')
            return 0
        if options.command == 'use-upstream':
            with vpnrouter.config_lock():
                data = vpnrouter.read_settings()
                current = data.get('https')
                data['https'] = {**(current if isinstance(current, dict) else {}), 'caddy_source': 'upstream'}
                vpnrouter.validate(vpnrouter.with_defaults(data))
                vpnrouter.save_config(data)
            step('https.caddy_source is upstream.')
        apply()
    except vpnrouter.ConfigError as exc:
        print(f'vpn-router-caddy: {exc}', file=sys.stderr)
        return 1
    except OSError as exc:
        print(f'vpn-router-caddy: {exc}', file=sys.stderr)
        return 1
    return 0


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