"""Script to prepare the legacy PPTP based VPN (as used for andcharge.com)

WARNING: The Point-to-Point Tunneling Protocol (PPTP) is considered unsafe since the late 1990s and should not be used!

This script reads the relevant keys for the PPTP configuration from the ECP configuration
and populates a template file for the ppp daemon.

The ppp daemon must then be called, pointing to that file.

   `pppd file /var/run/ppp-vpn`

A default name is derived from device serial information, e.g. '22049099_10691.0A'.
"""

from logging import INFO
from pathlib import Path
from string import Template
from typing import Dict, Union

from Utility import getConfigAndSetupLogging

CONFIG_YEAR = "Device/Serial/Year"
CONFIG_BATCH = "Device/Serial/Batch"
CONFIG_SERIAL = "Device/Serial/Serial"
CONFIG_ART = "Device/Serial/Art"
CONFIG_MAJ_REF = "Device/Serial/MajRev"
CONFIG_MIN_REF = "Device/Serial/MinRev"

CONFIG_HOST = "Interfaces/VPN/Host"
CONFIG_NAME = "Interfaces/VPN/Name"
CONFIG_PASSWORD = "Interfaces/VPN/Password"
CONFIG_ENABLED = "Interfaces/VPN/Enable"

CONFIG_DEFAULTS = {
    CONFIG_YEAR: 0,
    CONFIG_BATCH: 0,
    CONFIG_SERIAL: 0,
    CONFIG_ART: 10000,
    CONFIG_MAJ_REF: 0,
    CONFIG_MIN_REF: 0,
    CONFIG_HOST: "127.0.0.1",
    CONFIG_NAME: None,
    CONFIG_PASSWORD: "VPN_PW",
    CONFIG_ENABLED: False,
}

HOST = "HOST"
NAME = "NAME"
PASSWORD = "PASSWORD"
LOG_NAME = "PPTP"

PPPD_FILE_PATH = "/var/run/ppp-vpn"

PPP_TEMPLATE = f"""
pty "pptp ${HOST} --nolaunchpppd --debug"
remotename PPTP
unit 1 # interface has always the name ppp1
lcp-echo-interval 30
lcp-echo-failure 10
#require-mppe-128
#require-mschap-v2
refuse-eap
#refuse-pap
#refuse-chap
#refuse-mschap
noauth
debug
maxfail 0
nodefaultroute
#defaultroute
#replacedefaultroute
nodetach
#usepeerdns
name ${NAME}
password ${PASSWORD}
"""


def _parseConfig(config: dict) -> dict[str, str | int | bool]:
    """Parse the PPTP relevant keys from ECP configuration

    Config file location is determined by environment variable `ECP_WORK_FILES_DIR`.

    :return: parsed VPN config params for template substitution
    """
    parsedConf = {
        key: _getConfigKey(config, key) or CONFIG_DEFAULTS.get(key)
        for key in (
            CONFIG_YEAR,
            CONFIG_BATCH,
            CONFIG_SERIAL,
            CONFIG_ART,
            CONFIG_MAJ_REF,
            CONFIG_MIN_REF,
            CONFIG_HOST,
            CONFIG_NAME,
            CONFIG_PASSWORD,
            CONFIG_ENABLED,
        )
    }
    # Empty if disabled
    if not parsedConf.get(CONFIG_ENABLED):
        return {}
    # Create the default name
    if not parsedConf.get(CONFIG_NAME):
        parsedConf[CONFIG_NAME] = (
            f"{parsedConf[CONFIG_YEAR]:0>2}{parsedConf[CONFIG_BATCH]:0>2}{parsedConf[CONFIG_SERIAL]:0>4}"
            f"_{parsedConf[CONFIG_ART]:0>5}.{parsedConf[CONFIG_MAJ_REF]}{parsedConf[CONFIG_MIN_REF]}"
        )
    # Create template mapping
    return {
        HOST: parsedConf[CONFIG_HOST],
        NAME: parsedConf[CONFIG_NAME],
        PASSWORD: parsedConf[CONFIG_PASSWORD],
    }


def _prepareVpnConfig(params: Dict[str, str], file: Path) -> None:
    """Write config file for pppd

    :param params: parsed VPN config params
    :param file: file path to write VPN config file
    """
    output = Template(PPP_TEMPLATE).substitute(params)
    file.write_text(output)


def _getConfigKey(config: Dict, key: str) -> Union[str, bool, int, None]:
    """Get the configuration key for a given slash separated config path

    :param config: the configuration dictionary
    :param key: the slash separated config path
    :return: config value or `None` if the key is missing
    """
    if not config:
        return None
    subkey, _, remainingKey = key.partition("/")
    return (
        _getConfigKey(config.get(subkey), remainingKey)
        if remainingKey
        else config.get(subkey)
    )


def main() -> None:
    """Parse ECP configuration, write ppp config file and call ppp

    The ppp config file is deleted, when VPN is disabled in configuration
    """
    workDir, config, logger = getConfigAndSetupLogging(LOG_NAME, INFO)
    logger.info(f"Using WorkDir: {workDir}")
    vpnParams = _parseConfig(config)
    loggableParams = vpnParams.copy()
    if PASSWORD in loggableParams:
        loggableParams[PASSWORD] = "*****"
    logger.info(f"Parsed ECP config: {loggableParams}")

    file = Path(PPPD_FILE_PATH)
    if not vpnParams:
        logger.info("Service is disabled -> exiting...")
        file.unlink(missing_ok=True)
        return  # VPN is disabled
    _prepareVpnConfig(vpnParams, file)

    logger.info("Config prepared, ready to call ppp daemon...")


if __name__ == "__main__":
    """Main entrypoint, any unhandled exceptions will lead to non zero exit code"""
    main()
