#!/usr/bin/python3
"""Serve the status of the router on its internal address, its external address or both, as web.listen sets.

Run by vpn-router-web.service. Users are managed with vpnrouter_users, the module that
vpn-router-user uses too.

The paths it serves and the modules that answer them are described in the package vpnrouter_web.

The service first initializes, without listening: it makes the signing key, the OpenID Provider's key
(vpnrouter_oidc), the refresh tokens' database (vpnrouter_auth) and the web sites' database (vpnrouter_sites), retrying every INIT_RETRY_SECONDS until all are ready and
logging each failure. Then it listens. Its unit starts it only once wait-for-libpq has found libpq
in the linker cache, where psycopg looks it up.

The service needs nothing else to start and does not end on its own. Every DISCOVERY_SECONDS it
works out from /etc/vpn-router/vpn-router.json the addresses of the interfaces web.listen names,
internal, external or both, as general.mode directs, with web.port and web.enabled
(vpnrouter_net.web_listen), and runs a server on each (vpnrouter_serve.serve): it starts them,
moves them when an address or the port changes, and stops them while the service is disabled or
no address is known. Every request reads what it needs when it is made: the signing
key, the configuration, and the features of the router and the facts shown by /config from
/run/vpn-router/web.json, which vpn-router-setup writes. Before setup has written that file
the router has no features, and /config shows no configuration or interfaces.
"""

import os
import signal
import sys
import threading
import time

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

import vpnrouter
import vpnrouter_auth
import vpnrouter_net
import vpnrouter_oidc
import vpnrouter_serve
import vpnrouter_sites
from vpnrouter_web.common import CONFIG_FILE, log, read_json
from vpnrouter_web.handler import Handler
from vpnrouter_web.shell import editor_listen

#: Seconds between two attempts of the initialization.
INIT_RETRY_SECONDS = 15

#: The port when web.port is not set.
DEFAULT_PORT = 7081

#: Seconds between two discoveries of where the service should listen.
DISCOVERY_SECONDS = 30


def initialize():
    """Make the signing key, the OpenID Provider's key, the refresh tokens' database and the web sites' database,
    retrying every INIT_RETRY_SECONDS until all are ready."""
    reported = None
    while True:
        phase = 'reading the configuration'
        try:
            cfg = vpnrouter.load_config()
            phase = 'making the signing key'
            vpnrouter_auth.ensure_key()
            phase = 'making the OpenID Provider key'
            vpnrouter_oidc.ensure_key()
            phase = 'creating the token database'
            vpnrouter_auth.provision(cfg)
            phase = 'connecting to the token database'
            with vpnrouter_auth.database(cfg):
                pass
            phase = 'creating the database of the web sites'
            vpnrouter_sites.provision()
            phase = 'connecting to the database of the web sites'
            with vpnrouter_sites.database():
                pass
        except (OSError, SystemExit, vpnrouter_auth.DatabaseError, vpnrouter_sites.DatabaseError) as exc:
            error = f'{phase} failed: {exc.code if isinstance(exc, SystemExit) else exc}'
            if error != reported:
                log(f'{error}; retrying every {INIT_RETRY_SECONDS} seconds')
                reported = error
            time.sleep(INIT_RETRY_SECONDS)
            continue
        log('ready')
        return


def main():
    signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
    initialize()
    threading.Thread(target=editor_listen, name='editor socket', daemon=True).start()
    try:
        vpnrouter_serve.serve(lambda: vpnrouter_net.web_listen(read_json(CONFIG_FILE), DEFAULT_PORT), Handler, log,
                              DISCOVERY_SECONDS)
    except KeyboardInterrupt:
        pass
    return 0


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