#!/usr/bin/python3

##
# @file
# @ingroup rtctk_clients_configTool
#
# @brief Entry point for rtctkConfigTool where the CLI arguments are also defined.
#
# @copyright
#   SPDX-FileCopyrightText: 2023-2026 European Southern Observatory (ESO) @n
#   SPDX-License-Identifier: LGPL-3.0-only

"""
This is the entry point for the RTC Toolkit Configuration Tool that provides a CLI and GUI interface
as an engineering tool for manipulating the Persistent and Runtime Configuration Repositories.
"""

# The following is a workaround to prevent Taurus from hijacking the logging system.
# We trigger the setup once in Taurus, as early as possible in the process startup, by creating a
# dummy Logger object so that Taurus thinks the setup has already been done. The logging system will
# then be reconfigured later properly by the ConfigurationManager instance.
from rtctk.common.gui.logging import setup_dummy_taurus_logger

setup_dummy_taurus_logger()

# The following is needed to skip unit tests that are failing due to problems in DevEnv.
# See https://jira.eso.org/browse/ELTDEV-1172
import pytest

_FRAMEWORK_MODULE_ = pytest.importorskip("rtctk.framework")

import os
import sys
import click
from elt.log import CiiLogManager, CiiLogMessageBuilder
from rtctk.common.cli.config import ConfigurationManager
from rtctk.common.cli.exceptions import format_error_message  # pylint: disable=E0611
from rtctk.common.cli.argtypes import (
    LogLevel,
    RepositoryType,
    UriParamType,
    DpPathParamType,
    DpTypeParamType,
)
from rtctk.configtool.comms.config import ConfigurationParameters
from rtctk.configtool.cli.operations import Operations
from rtctk.configtool.grafana.grafana_widget_tool import (
    rtctk_grafana_import,
    rtctk_grafana_export,
    rtctk_grafana_cleanup,
)
from rtctk.framework import (  # pylint: disable=E0611,E0401
    DataPointPath,
    RtctkException,
)


__version__ = "6.0.0"


_config_manager = ConfigurationManager(ConfigurationParameters, DataPointPath("/rtctk_config_tool"))
_defaults = _config_manager.get_config_defaults()


# Prevent Doxygen from trying to produce API documentation for the click CLI handling methods.
# The docstring comments will not be appropriate. Click's formatting tags can clash with Doxygen.
# And these methods anyway do not form part of the API.
## @cond __doxygen_ignore__


# IMPORTANT: Do not add parameter or return descriptions in the docstrings for click annotated
#            methods. The docstrings are used by click to generate help output for the CLI.
@click.group()
@click.version_option(version=__version__, message="%(version)s")
@click.option(
    "--config",
    type=UriParamType(),
    help="URI indicating the location of this tool's configuration. This can either be a URI to a"
    " location within the CII configuration service or to local files.",
)
@click.option(
    "--service-discovery-endpoint",
    "--sde",
    type=UriParamType(),
    help="URI endpoint to the service discovery. This overrides any"
    f" {_defaults.service_discovery_endpoint.path} setting found in the configuration."
    f" Default: {_defaults.service_discovery_endpoint.value}",
)
@click.option(
    "--log-level",
    type=click.Choice(LogLevel),
    help="Sets the global logging level for the tool. This overrides any"
    f" {_defaults.log_level.path} setting found in the configuration."
    f" Default: {_defaults.log_level.value}",
)
@click.option(
    "--log-to-file",
    is_flag=True,
    flag_value=True,
    help=f"Makes the tool log to file. This overrides any {_defaults.log_to_file.path} setting"
    f" found in the configuration. Default: {_defaults.log_to_file.value}",
)
@click.option(
    "--runtime-repo-endpoint",
    "--rtr",
    type=UriParamType(),
    help="URI for the Runtime Configuration Repository. This overrides the value from the service"
    " discovery, and any"
    f" {_defaults.runtime_repo_endpoint.path} setting found in the configuration.",
)
@click.option(
    "--persistent-repo-endpoint",
    "--psr",
    type=UriParamType(),
    help="URI for the Persistent Configuration Repository. This overrides the"
    "value from the service discovery, and any"
    f" {_defaults.persistent_repo_endpoint.path} setting found in the configuration.",
)
@click.pass_context
def main(
    ctx,
    config,
    service_discovery_endpoint,
    log_level,
    log_to_file,
    runtime_repo_endpoint,
    persistent_repo_endpoint,
):
    """
    RTC Configuration Tool.

    This is used to view or manipulate the Runtime Configuration Repository and/or
    Persistent Configuration Repository.
    """
    # The only thing we do here is update the configuration with the arguments parsed by click.
    # Note: Do not perform any logic that might log at this stage. We are still parsing the command
    # line here and should only be bookkeeping, e.g. recording what arguments were used.
    # This is important to avoid polluting the command line help messages or slowing down the
    # program when only the help or version were requested. Real operation logic should be deferred
    # to the leaf sub-commands when we are done with command line parsing.
    _config_manager.update_config_from_cli(**ctx.params)

    if "CONFIG_CLIENT_INI" in os.environ:
        CiiLogManager.get_logger().debug(f"CONFIG_CLIENT_INI={os.environ['CONFIG_CLIENT_INI']}")


@main.command()
def gui():
    """
    Start the graphical application.

    Start the graphical application to visually explore and possibly modify the configuration
    repositories.
    """
    # We perform the import here instead of the top-level to avoid the overhead if we are not
    # actually running the GUI.
    import rtctk.configtool.gui

    rtctk.configtool.gui.gui_main(_config_manager, __version__)


@main.command()
@click.option(
    "--repo",
    default=RepositoryType.RUNTIME,
    type=click.Choice(RepositoryType),
    help="Indicates which repository to access. Default: runtime",
)
def shell(repo):
    """
    Enter an interactive shell.
    """
    Operations(_config_manager).run_shell(repo)


@main.command()
@click.argument("repo", type=click.Choice(RepositoryType))
@click.argument("path", type=DpPathParamType())
@click.option(
    "--recursive/--no-recursive",
    default=False,
    help="Will list all datapoints found under the given path recursively.",
)
def list(repo, path, recursive):
    """
    List all available datapoint paths under a given path hierarchy.

    PATH indicates the path to query.
    """
    Operations(_config_manager).list_datapoints(repo, path, recursive)


@main.command()
@click.argument("repo", type=click.Choice(RepositoryType))
@click.argument("path", type=DpPathParamType())
@click.option(
    "--type",
    default=False,
    is_flag=True,
    help="If this flag is given then the datapoint type is printed instead.",
)
@click.option(
    "--size",
    default=False,
    is_flag=True,
    help="If this flag is given then the datapoint size is printed instead.",
)
@click.option(
    "--use-repr",
    default=False,
    is_flag=True,
    help="If this flag is given then the value is printed using the 'repr' function that produces"
    " output compatible with Python literal syntax.",
)
def get(repo, path, type, size, use_repr):
    """
    Get a datapoint value.

    Reads a datapoint from the repository indicated by 'runtime' or 'persistent' and prints the
    value to the terminal. If the datapoint is a large vector or matrix then the output will be
    truncated.

    PATH indicates the datapoint path to read from.
    """
    Operations(_config_manager).get_datapoint(repo, path, type, size, use_repr)


@main.command(context_settings={"ignore_unknown_options": True})  # Allows negative integer VALUEs
@click.argument("repo", type=click.Choice(RepositoryType))
@click.argument("path", type=DpPathParamType())
@click.argument("value", type=str)
@click.option(
    "--type", default=None, type=DpTypeParamType(), help="The type to use for the datapoint."
)
def set(repo, path, value, type):
    """
    Set a datapoint value.

    If the datapoint does not exist it will be created. If a type is provided then that type is
    used for the new datapoint. Otherwise the type is deduced from the Python literal value
    provided.

    PATH indicates the datapoint path to write to.

    VALUE the new value for the datapoint given as a Python literal expression.
    """
    Operations(_config_manager).set_datapoint(repo, path, value, type)


@main.command()
@click.argument("repo", type=click.Choice(RepositoryType))
@click.argument("path", type=DpPathParamType())
@click.argument("output", type=click.Path())
def read(repo, path, output):
    """
    Read a datapoint or hierarchy into local file(s).

    PATH the datapoint or hierarchy path to read from.

    OUTPUT the path of the YAML file, FITS file or directory that will contain the fetched data.
    """
    Operations(_config_manager).read_hierarchy(path, output, repo_name=repo)


@main.command()
@click.argument("repo", type=click.Choice(RepositoryType))
@click.argument("path", type=DpPathParamType())
@click.argument("input", type=click.Path())
def write(repo, path, input):
    """
    Write a datapoint or hierarchy with data from local file(s).

    PATH the datapoint or hierarchy path to write to.

    INPUT the path of the YAML file, FITS file or directory that contains the data to write.
    """
    Operations(_config_manager).write_hierarchy(path, input, repo_name=repo)


@main.command()
@click.argument("repo", type=click.Choice(RepositoryType))
@click.argument("path", type=DpPathParamType())
@click.option(
    "--recursive/--no-recursive",
    default=False,
    help="Flag indicating if the all datapoints found under the indicated path should"
    " be deleted recursively.",
)
def delete(repo, path, recursive):
    """
    Delete an existing datapoint or hierarchy.

    PATH the datapoint or hierarchy path to delete.
    """
    Operations(_config_manager).delete_datapoints(repo, path, recursive)


@main.command()
@click.option("--uri", type=str, help="URI endpoint to the running Grafana instance.")
@click.option("--token", type=str, help="Security access token for Grafana")
@click.option(
    "--data-dir", type=str, help="Directory from which should be Grafana resources loaded"
)
@click.option("--secrets", type=click.Path(exists=True), help="JSON file with datasource secrets")
def grafana_import(uri, token, data_dir, secrets):
    """
    Import provisioned resources to running Grafana instance.
    """
    rtctk_grafana_import(uri, token, data_dir, secrets)


@main.command()
@click.option("--uri", type=str, help="URI endpoint to the running Grafana instance.")
@click.option("--token", type=str, help="Security access token for Grafana")
@click.option("--data-dir", type=str, help="Directory where should be Grafana resources stored")
def grafana_export(uri, token, data_dir):
    """
    Export grafana resources as provisioned JSON files to certain directory.
    """
    rtctk_grafana_export(uri, token, data_dir)


@main.command()
@click.option("--uri", type=str, help="URI endpoint to the running Grafana instance.")
@click.option("--token", type=str, help="Security access token for Grafana")
@click.option(
    "--data-dir", type=str, help="Directory from which resources to be deleted are loaded"
)
def grafana_cleanup(uri, token, data_dir):
    """
    Delete the resources that were uploaded using the import command (those that were provisioned).
    """
    rtctk_grafana_cleanup(uri, token, data_dir)


@main.command()
@click.option(
    "--no-validation",
    default=False,
    is_flag=True,
    help="Flag indicating that the validation shall not be done before the population.",
)
def populate(no_validation):
    """
    Populates the Runtime Configuration Repository by loading data from the Persistent Configuration
    Repository.

    By default it also first validates the configuration, unless the --no-validate option is given.
    """
    if not no_validation:
        Operations(_config_manager).validate_persistent_repository()
    Operations(_config_manager).populate_runtime_repository()


@main.command()
def validate():
    """
    Validates the structure and contents of the Persistent Configuration Repository.
    """
    Operations(_config_manager).validate_persistent_repository()


## @endcond


if __name__ == "__main__":
    # Note that we do not setup the logging subsystem here. We defer this step to the
    # ConfigurationManager instance (_config_manager) that takes care of any intricacies of setting
    # up the logging system and loading the applicable configuration. By the time the logger is
    # fetched in the exception handling code below, the logging system should already be correctly
    # setup by _config_manager.
    try:
        main(default_map=_config_manager.get_config_defaults_as_dict())  # pylint: disable=E1120
    except (RuntimeError, RtctkException) as err:
        logger = CiiLogManager.get_logger("rtctk")
        logger.error(f"{format_error_message(err)}")
        print(f"ERROR: {format_error_message(err)}", file=sys.stderr)
        sys.exit(1)
    except Exception as err:
        # Unexpected exceptions, i.e. anything that is not explicitly a RuntimeError, are assumed to
        # be code bugs, so we log it and rethrow.
        logger = CiiLogManager.get_logger("rtctk")
        logger.fatal(
            CiiLogMessageBuilder.create_and_build_with_exception(
                "An unexpected exception occurred", err
            )
        )
        raise
