#!/usr/bin/python3
"""
@file
@ingroup aliceGui
@copyright ESO - European Southern Observatory
@author Diego del Valle <ddelvall@eso.org>

@brief ALICE GUI main entry point

This module provides the command-line interface for the ALICE Wavefront sensor Camera
GUI application. It extends the DCF GUI with ALICE-specific configuration and initialization.

Key responsibilities:
    - Parse and validate command-line arguments
    - Initialize Taurus application framework
    - Configure logging using WdglibLogger
    - Retrieve configuration from NGC server via MAL
    - Resolve service URI from Consul or direct input
    - Launch the ALICE GUI main window

The application retrieves ALICE-specific configuration parameters from the NGC server, including:
    - OPC UA server URI for telemetry
    - FCS OLDB prefix
    - IODEV device suffix
    - Logging configuration

Example usage:
    # Connect via Consul service name
    aliceGui --name alice-req --log-level INFO

    # Connect via direct URI
    aliceGui --uri zpb.rr://127.0.0.1:12081 --log-level DEBUG
"""
import sys
import click
import logging
import re
import taurus
import yaml
from datetime import timedelta
from typing import Optional, Callable

from taurus.core.util.log import Logger
from taurus.qt.qtgui.application import TaurusApplication

from ifw.wdglib.log.wdglib_logger import WdglibLogger
from alice.gui.aliceGui.aliceguimainwindow import AliceGuiMainWindow
from ifw.core.utils.utils import find_file

import ifw.core.stooUtils.consul as consul_utils

import elt.pymal as mal
from ModDcsif.Dcsif.DcsCmds import DcsCmdsSync

@click.command('aliceGui')
@click.option('-l', '--log-level', default='ERROR', help='Debugging Level',
               type=click.Choice(["TRACE", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], 
                                 case_sensitive=False))
@click.option('-u', '--uri', default=None, help='Service URI, e.g. "zpb.rr://127.0.0.1:12081/"')
@click.option('-n', '--name', default=None, help='Registered name in Consul, e.g. alice-req')
@click.pass_context
def main(ctx: click.Context, log_level: str, uri: Optional[str], name: Optional[str]) -> None:
    """ALICE GUI application entry point.
    
    Initializes the Taurus application, configures logging, retrieves configuration
    from the NGC server, validates command-line arguments, and launches the ALICE GUI window.
    
    This function serves as the primary entry point for the ALICE GUI application. It handles:
    - Application initialization with Taurus framework
    - Logging configuration from JSON config file
    - URI resolution from Consul or direct input
    - Configuration retrieval from NGC server via MAL
    - Validation of required configuration parameters
    - Creation and display of the ALICE GUI main window
    
    Args:
        ctx: Click context object containing command-line parameters.
        log_level: Logging level for application output (TRACE, CRITICAL, ERROR, WARNING, INFO, DEBUG).
        uri: Optional direct Req/Rep URI endpoint (e.g., "zpb.rr://127.0.0.1:12081").
        name: Optional Consul service name for service discovery (e.g., "alice-req").
    
    Raises:
        SystemExit: If neither --name nor --uri is provided.
        SystemExit: If configuration cannot be retrieved from NGC server.
        SystemExit: If required configuration parameters are missing.
    
    Example:
        >>> main(ctx, log_level="INFO", uri="zpb.rr://127.0.0.1:12081", name=None)
    
    @ingroup aliceGui
    """

    # Initialize Taurus application
    app = TaurusApplication(
        app_name="aliceGui",
        app_version="0.2.0",
        org_name="ESO",
        org_domain="eso.org",
        cmd_line_parser=None,
    )

    logger = Logger(app.applicationName())
    logger.setLogLevel(getattr(taurus, log_level.title()))
    logger.info(f"Application LogLevel=|{log_level}|")

    # Validate that either Consul service name or direct URI is provided
    if name is None and uri is None:
        logger.error(
            "ALICE GUI needs a consul request service reference (command line option --name) or a Req/Rep URI endpoint (command line option --uri) to start"
        )
        print(
            "ALICE GUI needs a consul request service reference (command line option --name) or a Req/Rep URI endpoint (command line option --uri) to start"
        )
        sys.exit()

    # Resolve and clean URI
    uri = get_uri(ctx, logger=logger)
    
    if uri is None:
        sys.exit()
    
    uri = clean_uri(uri)
    logger.info(f'URI - {uri}')
    
    # Retrieve configuration from NGC server
    config_file = get_config(uri)

    if config_file is None:
        logger.error(
            "Unable to retrieve config file from NGC"
        )
        print(
            "Unable to retrieve config file from NGC"
        )
        sys.exit()
    
    # --- DCF GUI related config ---
    if not is_config_in_file('cfg.oldb.uri_prefix', config_file, "ERROR: The oldb_prefix defined by cfg.oldb.uri_prefix parameter is missing in the configuration file", logger):
        sys.exit()

    if 'cfg.procname' not in config_file:
        logger.info("cfg.procname not in config, set procname to default - dcf")
        procname = 'dcf'
    else:
        procname = config_file['cfg.procname']
        logger.info(f"Set procname to {procname}")
        
    dcs_db_prefix = f"{config_file['cfg.oldb.uri_prefix']}/{procname}"

    # --- ALICE related config ---
    if not is_config_in_file('cfg.gui.logfile', config_file, "ERROR: Logging config file, defined by cfg.gui.logfile parameter for ALICE GUI is missing", logger):
        sys.exit()

    logger_config_file_path = find_file(config_file['cfg.gui.logfile'])
    if logger_config_file_path is None:
        logger.error("Log file not found")
        sys.exit()

    if not is_config_in_file('cfg.gui.opc_server', config_file, "ERROR: The OPC server address defined by cfg.gui.opc_server parameter is missing in the configuration file", logger):
        sys.exit()

    if not is_config_in_file('cfg.gui.iodev_suffix', config_file, "ERROR: The IODEV device name defined by cfg.gui.iodev_suffix parameter is missing in the configuration file", logger):
        sys.exit()

    if not is_config_in_file('cfg.gui.fcs_db_prefix', config_file, "ERROR: The fcs_db_prefix defined by cfg.gui.fcs_db_prefix parameter missing in the configuration file", logger):
        sys.exit()

    # Initialize WdglibLogger
    WdglibLogger.init(logger_config_file_path)
    WdglibLogger.set_application_name('Alice')

    # Create and show ALICE GUI main window
    window = AliceGuiMainWindow(ctx, 
                                base_uri=uri, 
                                opc_server=config_file['cfg.gui.opc_server'], 
                                dcs_db_prefix=dcs_db_prefix,
                                fcs_db_prefix=config_file['cfg.gui.fcs_db_prefix'],
                                iodev_suffix=config_file['cfg.gui.iodev_suffix'],
                                )
    
    window.setWindowTitle('Alice GUI')
    window.show()
    sys.exit(app.exec_())

def clean_uri(uri: str) -> str:
    """Clean URI by removing quotes and trailing slashes.
    
    Removes any quotes added by the user and trailing slashes that cause
    issues with MAL communication.
    
    Args:
        uri: URI string that may contain quotes or trailing slashes.
             Example: '"zpb.rr://127.0.0.1:12081/"'
    
    Returns:
        Cleaned URI string without quotes or trailing slashes.
        Example: "zpb.rr://127.0.0.1:12081"
    
    Example:
        >>> clean_uri('"zpb.rr://127.0.0.1:12081/"')
        'zpb.rr://127.0.0.1:12081'
    """
    uri = uri.strip(' " " ')
    uri = uri.strip(' \' \' ')
    if uri.endswith('/'):
        uri = uri[:-1]
    return uri

def is_config_in_file(config_key: str, config_file: dict, error_message: str, logger: Logger) -> bool:
    """Check if a configuration key exists in the config file.
    
    Logs an info message if the key is found, or an error message if it's missing.
    
    Args:
        config_key: The configuration key to check for.
                    Example: "cfg.oldb.uri_prefix"
        config_file: Dictionary containing configuration parameters.
        error_message: Error message to log if the key is not found.
        logger: Logger instance for logging messages.
    
    Returns:
        True if the key exists in config_file, False otherwise.
    
    Example:
        >>> config = {"cfg.oldb.uri_prefix": "cii.oldb:///elt/dcf"}
        >>> is_config_in_file("cfg.oldb.uri_prefix", config, "Key missing", logger)
        True
    """
    if config_key in config_file:
        logger.info(f'key {config_key} found in config')
    else:
        logger.error(error_message)
    return config_key in config_file

def parse_string_config(json_yaml_config: str, content_keyw: str = "cfg.filename.content") -> dict:
    """Parse JSON/YAML configuration string from NGC server.
    
    Extracts the YAML content from a JSON-wrapped string returned by the NGC server
    and parses it into a dictionary.
    
    Args:
        json_yaml_config: JSON string containing YAML configuration.
                          Expected format: '{"cfg.filename.content": "yaml_content_here"}'
        content_keyw: Key name for the YAML content in the JSON string.
                      Default: "cfg.filename.content"
    
    Returns:
        Parsed configuration dictionary from the 'app' key in the YAML content.
    
    Raises:
        AttributeError: If the regex match fails or 'app' key is missing.
    
    Example:
        >>> config_str = '{"cfg.filename.content": "app:\\n  key: value"}'
        >>> parse_string_config(config_str)
        {'key': 'value'}
    """
    match = re.search(rf'{{"{content_keyw}": "(.*)"}}\s*$', json_yaml_config, re.DOTALL)
    yaml_content = match.group(1)
    config_dict = yaml.safe_load(yaml_content)

    return config_dict['app']

def get_uri(cmd_line_ctx: click.Context, logger: Logger = logging.getLogger('Null')) -> Optional[str]:
    """Resolve and clean the Req/Rep URI from command-line parameters.
    
    Resolves the service URI either by:
    - Querying Consul using the provided service name
    - Using the directly provided URI (with cleanup)
    
    Args:
        cmd_line_ctx: Click context containing 'name' and 'uri' parameters.
        logger: Logger instance for error reporting. Defaults to null logger.
    
    Returns:
        Cleaned URI string if successful, None otherwise.
        Example: "zpb.rr://127.0.0.1:12081"
    
    Example:
        >>> ctx.params = {"name": "alice-req", "uri": None}
        >>> get_uri(ctx)
        'zpb.rr://192.168.1.100:12081'  # Resolved from Consul
        
        >>> ctx.params = {"name": None, "uri": "zpb.rr://127.0.0.1:12081/"}
        >>> get_uri(ctx)
        'zpb.rr://127.0.0.1:12081'  # Trailing slash removed
    """
    name = cmd_line_ctx.params.get("name")
    uri = cmd_line_ctx.params.get("uri")

    if uri is None:
        try:
            cons = consul_utils.ConsulClient()
            uri = cons.get_uri(name)
        except Exception as e:
            logger.error(f"Unable to retrieve uri via Consul")
            logger.error(f"{e}")
            uri = None
    else:
        try:
            uri = uri.strip(' " " ')
            uri = uri.strip(' \' \' ')
            if uri.endswith('/'):
                uri = uri[:-1]
        except Exception as e:
            logger.error(f"Unable to clean provided uri")
            logger.error(f"{e}")
            uri = None
    
    return uri

def get_config(base_uri: str, 
               config_key: str = "cfg.filename.content", 
               logger: Logger = logging.getLogger('Null'),
               parser: Callable[[str], dict] = parse_string_config,
               ) -> Optional[dict]:
    """Retrieve configuration from NGC server via MAL interface.
    
    Connects to the DcsCmds interface to fetch the configuration file that was
    used to start the NGC2 server. The MAL connection is closed immediately after
    retrieving the configuration.
    
    Args:
        base_uri: Base Req/Rep URI for DCF server communication.
                  Example: "zpb.rr://127.0.0.1:12081"
        config_key: Configuration key to retrieve from DcsCmds.
                    Default: "cfg.filename.content"
        logger: Logger instance for error reporting. Defaults to null logger.
        parser: Callable to parse the configuration string into a dictionary.
                Default: parse_string_config
    
    Returns:
        Parsed configuration dictionary if successful, None otherwise.
        Example: {'cfg.oldb.uri_prefix': 'cii.oldb:///elt/dcf', ...}
    
    Example:
        >>> get_config("zpb.rr://127.0.0.1:12081")
        {'cfg.oldb.uri_prefix': 'cii.oldb:///elt/dcf', 'cfg.gui.opc_server': 'opc.ua.tcp://127.0.0.1:4840'}
    """        
    ciiFactory = mal.CiiFactory.getInstance()
    wait_time = timedelta(seconds=10)
    qos = mal.rr.qos.ReplyTime(wait_time)

    mal_properties = {}
    zpbmal = mal.loadMal('zpb', mal_properties) 

    ciiFactory.registerMal('zpb.rr', zpbmal)

    logger.info(f"{base_uri}/DcsCmds")
    with ciiFactory.getClient(f"{base_uri}/DcsCmds", DcsCmdsSync, qos, {}) as client:
        try:
            reply_json = client.GetConfig(config_key)
            parsed_config = parser(reply_json)
            return parsed_config
            
        except Exception as e:
            logger.error(f"Unable to get client {base_uri}/DcsCmds - Reason: {e}")
            return None
    logger.error(f"Unable to get client {base_uri}/DcsCmds")
    return None


if __name__ == "__main__":
    main()
