#!python

"""This script can be used to generate configuration files for all Krake components
(API and controllers for instance).

Several options are present to tweak the configuration files automatically. These
options can be extended in the future. They are used to get the values of variables
which are then set into the generated files.

The options in the arguments are not necessarily the ones given for substitution: a
handler can be added to modify the value taken from the command line with the
``register`` decorator. New options not present in the argument parser but to be
computed using the arguments can also be added manually the same way.

To select the files to use for generation, use the ``--src-files`` option. Each one of
the selected files will have a generated counterpart in the destination directory.

The template substitution engine is the :class:`string.Template` from the ``string``
module. Thus, each option should start with a "$" character.

The script can be used without any further installation.
"""

import sys
import os.path

from argparse import ArgumentParser
from string import Template


def register(option):
    """Add a handler to compute a newer option value, or replace the value given by
    the parser.

    Args:
        option (str): name of the option to register the handler for.

    Returns:
        callable: a decorator that registers the handler and the options to eject.

    """

    def decorator(func):
        register.option_handlers[option] = func
        return func

    return decorator


register.option_handlers = {}


@register("api_endpoint")
def create_api_endpoint(api_host=None, api_port=None, tls_enabled=None, **kwargs):
    """Generate the API endpoint from arguments.

    Args:
        api_host (str): The URL host of the API endpoint.
        api_port (int): The URL port of the API endpoint.
        tls_enabled (bool): True if TLS support should be enabled for the components

    Returns:
        str: the generated API endpoint

    """
    scheme = "https" if tls_enabled else "http"
    return f"{scheme}://{api_host}:{api_port}"


@register("external_complete_endpoint_key_and_value")
def format_complete_external_endpoint_key_and_value(external_endpoint=None, **kwargs):
    """Add a line for the optional "external_endpoint" parameter of the "complete" hook.
    If a value is given, the line is generated, with format: "key: <given_value>". If no
    value is given, the line is kept empty.

    Args:
        external_endpoint (str|None): the value of the external endpoint, as it should
            appear in the configuration. None if no external endpoint should be given.

    Returns:
        str: the line to add in the configuration if an endpoint has been defined, the
            empty string otherwise.

    """
    if external_endpoint:
        return f"external_endpoint: {external_endpoint}"
    return ""


@register("external_shutdown_endpoint_key_and_value")
def format_external_shutdown_endpoint_key_and_value(external_endpoint=None, **kwargs):
    """Add a line for the optional "external_endpoint" parameter of the "shutdown" hook.
    If a value is given, the line is generated, with format: "key: <given_value>". If no
    value is given, the line is kept empty.

    Args:
        external_endpoint (str|None): the value of the external endpoint, as it should
            appear in the configuration. None if no external endpoint should be given.

    Returns:
        str: the line to add in the configuration if an endpoint has been defined, the
            empty string otherwise.

    """
    if external_endpoint:
        return f"external_endpoint: {external_endpoint}"
    return ""


def generate_substitution(options):
    """From the options, apply the registered handler to modify the given values or
    add the newly defined ones.

    Args:
        options (dict): the options given as: <name>: <value>

    Returns:
        dict: the substitutions to use in the templates:
            <key in template>: <value to use for replacement>

    """
    substitutions = dict(options)

    # For each option, add the value given or the value computed by the handler.
    for name, value in substitutions.items():
        handler = register.option_handlers.get(name)
        if handler:
            value = handler(**options)
        substitutions[name] = value

    # Also add generated values, that were not in the options:
    for name, handler in register.option_handlers.items():
        if name in options:
            continue
        value = handler(**options)
        substitutions[name] = value

    return substitutions


def generate_file(src_path, dst_path, substitution):
    """Apply the given configuration to one template file and copy it to the
    destination directory with the given name.

    Args:
        src_path (str): Path of the source for the template file.
        dst_path (str): Complete path with the final name for the generated file.
        substitution (dict): the substitutions to use in the template.

    """
    # Read template file
    with open(src_path, "r") as file:
        content = file.read()

    # Apply the template
    template = Template(content)
    content = template.substitute(substitution)

    # Write to final file
    with open(dst_path, "w") as file:
        file.write(content)
        print(f"Generated file: {dst_path}")


def main(src_files=None, dst=None, **kwargs):
    """Select the files that will be used to generate configuration, and apply the
    template.

    Args:
        src_files (list, optional): List of paths to template source files.
        dst (str): Path to the destination directory where the generated files will
            be stored.
        kwargs (dict): all options given by the parser to add to the templates.

    """
    if not src_files:
        return "No source file has been given."

    substitution = generate_substitution(kwargs)

    for src_path in src_files:
        if not os.path.isfile(src_path):
            return f"The given path '{src_path}' should be a file"

        dst_file = os.path.basename(src_path).replace(".template", "")
        dst_path = os.path.join(dst, dst_file)
        generate_file(src_path, dst_path, substitution)


def add_option(parser, name, help, default=None, action=None, **kwargs):
    """Define a new option that is accepted by the parser. One of default or action
    parameter has to be given.

    Args:
        parser (ArgumentParser): argument parser on which the new option will be added
        name (str): the name of the newly added option.
        help (str): what will be printed in the help. The default value, if given,
            will be printed along.
        default (Any, optional): the default value that will be given to the option.
        action (str, optional): the argparse action to apply.
        kwargs (dict, optional): additional options given to the parser.

    """
    if not default and not action:
        sys.exit(f"For option {name}, both default and action cannot be empty")

    option = name.replace("_", "-")

    if default:
        help = f"{help} Default: '{default}'"
        # Prevent "None" to be added as default value if no default value is wanted
        kwargs["default"] = default

    parser.add_argument(option, action=action, help=help, **kwargs)


if __name__ == "__main__":
    parser = ArgumentParser(
        description="Generate all configuration files from the given source directory"
        " to the given destination directory, while applying the given"
        " configuration."
    )
    parser.add_argument(
        "--dst",
        default=".",
        help=(
            "Destination directory for generated files. Default is current "
            "working directory."
        ),
    )
    parser.add_argument(
        "src_files",
        nargs="+",
        help="Source files for template. Multiple files paths can be given",
    )

    # Add options here for the template
    add_option(
        parser,
        "--tls-enabled",
        "Enable TLS for communication between Krake components.",
        action="store_true",
    )
    add_option(
        parser,
        "--cert-dir",
        "Directory for the certificates of the Krake components.",
        default="tmp/pki",
    )
    add_option(
        parser,
        "--allow-anonymous",
        "Enable the acceptance of all requests which have not been authenticated.",
        action="store_true",
    )
    add_option(
        parser,
        "--keystone-authentication-enabled",
        "Enable the Keystone authentication as one of the authentication mechanisms.",
        action="store_true",
    )
    add_option(
        parser,
        "--keystone-authentication-endpoint",
        "Endpoint to connect to the keystone service.",
        default="http://localhost:5000/v3",
    )
    add_option(
        parser,
        "--keycloak-authentication-enabled",
        "Enable the Keystone authentication as one of the authentication mechanisms.",
        action="store_true",
    )
    add_option(
        parser,
        "--keycloak-authentication-endpoint",
        "Endpoint to connect to the Keycloak service.",
        default="http://localhost:9080",
    )
    add_option(
        parser,
        "--keycloak-authentication-realm",
        "Keycloak realm to use for authentication.",
        default="krake",
    )
    add_option(
        parser,
        "--static-authentication-enabled",
        "Enable the static authentication as one of the authentication mechanisms.",
        action="store_true",
    )
    add_option(
        parser,
        "--static-authentication-username",
        "Name of the user that will authenticate through static authentication.",
        default="system:admin",
    )
    add_option(
        parser,
        "--cors-origin",
        (
            "URL or wildcard for the 'Access-Control-Allow-Origin' of the CORS system"
            " on the API."
        ),
        default="*",
    )
    add_option(
        parser,
        "--authorization-mode",
        (
            "Authorization mode to use for the requests sent to the API. "
            "Only 'RBAC' should be used in production."
        ),
        default="always-allow",
        choices=["RBAC", "always-allow", "always-deny"],
    )
    add_option(
        parser,
        "--api-host",
        "API endpoint host for the controllers.",
        default="localhost",
    )
    add_option(
        parser, "--api-port", "API endpoint port for the controllers.", default=8080
    )
    add_option(
        parser, "--etcd-version", "The etcd database version.", default="v3.3.13"
    )
    add_option(
        parser, "--etcd-host", "Host for the etcd endpoint.", default="127.0.0.1"
    )
    add_option(parser, "--etcd-port", "Port for the etcd endpoint.", default=2379)
    add_option(
        parser, "--etcd-peer-port", "Peer port for the etcd endpoint.", default=2380
    )
    add_option(
        parser,
        "--docs-problem-base-url",
        "URL of the problem documentation.",
        default="https://rak-n-rok.readthedocs.io/projects/krake/en/latest/user/problem.html",
    )
    add_option(parser, "--docker-daemon-mtu", "The Docker daemon MTU.", default=1450)
    add_option(
        parser,
        "--worker-count",
        "Number of workers to start on each controller.",
        default=5,
    )
    add_option(
        parser,
        "--debounce",
        "Debounce time for the worker queue (seconds).",
        default=1.0,
    )
    add_option(
        parser,
        "--reschedule-after",
        "Time in seconds after which a resource will be rescheduled.",
        default=60,
    )
    add_option(
        parser,
        "--stickiness",
        (
            '"Stickiness" weight to express migration overhead'
            " in the normalized ranking computation."
        ),
        default=0.1,
    )
    add_option(
        parser,
        "--poll-interval",
        (
            "Time in seconds for the Magnum Controller to ask the Magnum client again"
            "after a modification of a cluster."
        ),
        default=30,
    )
    add_option(
        parser,
        "--complete-hook-user",
        (
            "For the 'complete' hook, the name of the user defined in the CN of the"
            " generated certificates. Should match the Roles in the database if RBAC is"
            " enabled."
        ),
        default="system:complete-hook",
    )
    add_option(
        parser,
        "--complete-hook-cert-dest",
        (
            "For the complete hook, set the path to the directory, where the"
            " certificates will be stored in the deployed Application."
        ),
        default="/etc/krake_cert",
    )
    add_option(
        parser,
        "--complete-hook-env-token",
        (
            "For the complete hook, set the name of the environment variable"
            " that contain the value of the token,"
            " which will be given to the Application."
        ),
        default="KRAKE_COMPLETE_TOKEN",
    )
    add_option(
        parser,
        "--complete-hook-env-url",
        (
            "For the complete hook, set the name of the environment variable"
            " that contain the URL of the Krake API,"
            " which will be given to the Application."
        ),
        default="KRAKE_COMPLETE_URL",
    )
    parser.add_argument(
        "--external-endpoint",
        help=(
            "If set, replaces the value of the URL host and port of the endpoint given"
            " to the Applications which have the 'complete' hook enabled."
        ),
        default="",
    )

    add_option(
        parser,
        "--shutdown-hook-user",
        (
            "For the 'shutdown' hook, the name of the user defined in the CN of the"
            " generated certificates. Should match the Roles in the database if RBAC is"
            " enabled."
        ),
        default="system:shutdown-hook",
    )
    add_option(
        parser,
        "--shutdown-hook-cert-dest",
        (
            "For the shutdown hook, set the path to the directory, where the"
            " certificates will be stored in the deployed Application."
        ),
        default="/etc/krake_cert",
    )
    add_option(
        parser,
        "--shutdown-hook-env-token",
        (
            "For the shutdown hook, set the name of the environment variable"
            " that contain the value of the token,"
            " which will be given to the Application."
        ),
        default="KRAKE_SHUTDOWN_TOKEN",
    )
    add_option(
        parser,
        "--shutdown-hook-env-url",
        (
            "For the shutdown hook, set the name of the environment variable"
            " that contain the URL of the Krake API,"
            " which will be given to the Application."
        ),
        default="KRAKE_SHUTDOWN_URL",
    )

    args = vars(parser.parse_args())
    sys.exit(main(**args))
