import subprocess
import pandas as pd
import xml.etree.ElementTree as ET
import xmlformatter
import logging
import os
import variables_test
from utils import save_backup, remove_file_if_exists, remove_duplicates, convert_to_dict, synchronize_with_VM, format_kwargs, run_command,remove_duplicates_from_list

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

############################
# HERE : Useful dictionaries 
############################

# Define the tags for each section
# INFO : ORDER MATTERS ! - > Define the order of concatenation in the .conf file
SECTION_TAGS = {
    'global': ['jsonout_output', 'alerts_log', 'logall', 'logall_json', 'email_notification', 'smtp_server', 'email_from', 'email_to', 'email_maxperhour', 'email_log_source', 'agents_disconnection_time', 'agents_disconnection_alert_time', 'update_check', 'white_list'],
    'client': ['server', 'config-profile', 'notify_time', 'time-reconnect', 'auto_restart', 'crypto_method'],
    'client_buffer': ['disabled', 'queue_size', 'events_per_second'],
    'alerts': ['log_alert_level', 'email_alert_level'],
    'logging': ['log_format'],
    'remote': ['connection', 'port', 'protocol', 'queue_size'],
    'rootcheck': ['disabled', 'check_files', 'check_trojans', 'check_dev', 'check_sys', 'check_pids', 'check_ports', 'check_if', 'frequency', 'rootkit_files', 'rootkit_trojans', 'skip_nfs'],
    'wodle': ['name', 'disabled', 'timeout', 'interval', 'scan-on-start', 'java_path', 'ciscat_path', 'run_daemon', 'log_path', 'config_path', 'add_labels'],
    'sca': ['enabled', 'scan_on_start', 'interval', 'skip_nfs'],
    'vulnerability-detection': ['enabled', 'index-status', 'feed-update-interval'],
    'indexer': ['enabled', 'hosts', 'ssl'],
    'auth': ['disabled', 'port', 'use_source_ip', 'purge', 'use_password', 'ciphers', 'ssl_verify_host', 'ssl_manager_cert', 'ssl_manager_key', 'ssl_auto_negotiate'],
    'cluster': ['name', 'node_name', 'node_type', 'key', 'port', 'bind_addr', 'nodes', 'hidden', 'disabled'],
    'syscheck': ['disabled', 'frequency', 'scan_on_start', 'alert_new_files', 'auto_ignore', 'directories', 'ignore', 'nodiff', 'skip_nfs', 'skip_dev', 'skip_proc', 'skip_sys', 'process_priority', 'max_eps', 'synchronization'],
    'command': ['name', 'executable', 'timeout_allowed'],
    'localfile': ['log_format', 'command', 'frequency', 'location', 'alias'],
    'ruleset': ['group', 'decoder', 'list', 'decoder_dir', 'rule_dir', 'rule_exclude'],
    'rule_test': ['id', 'level', 'description', 'group', 'decoder', 'list', 'enabled', 'threads', 'max_sessions', 'session_timeout'],
    'active-response': ['command', 'location', 'rules_id', 'timeout','disabled','ca_store','ca_verification'],
}


# Define section attributes
SECTION_ATTRIBUTES = {
    'global': {'tags': SECTION_TAGS['global'], 'copy': True},
    'client': {'tags': SECTION_TAGS['client'], 'copy': True},
    'client_buffer': {'tags': SECTION_TAGS['client_buffer'], 'copy': True},
    'alerts': {'tags': SECTION_TAGS['alerts'], 'copy': True},
    'logging': {'tags': SECTION_TAGS['logging'], 'copy': True},
    'remote': {'tags': SECTION_TAGS['remote'], 'copy': True},
    'rootcheck': {'tags': SECTION_TAGS['rootcheck'], 'copy': True},
    'wodle': {'tags': SECTION_TAGS['wodle'], 'copy': True},
    'sca': {'tags': SECTION_TAGS['sca'], 'copy': True},
    'vulnerability-detection': {'tags': SECTION_TAGS['vulnerability-detection'], 'copy': True},
    'indexer': {'tags': SECTION_TAGS['indexer'], 'copy': True},
    'auth': {'tags': SECTION_TAGS['auth'], 'copy': False},
    'cluster': {'tags': SECTION_TAGS['cluster'], 'copy': False},
    'syscheck': {'tags': SECTION_TAGS['syscheck'], 'copy': False},
    'command': {'tags': SECTION_TAGS['command'], 'copy': False},
    'localfile': {'tags': SECTION_TAGS['localfile'], 'copy': False},
    'ruleset': {'tags': SECTION_TAGS['ruleset'], 'copy': False},
    'rule_test': {'tags': SECTION_TAGS['rule_test'], 'copy': False},
    'active-response': {'tags': SECTION_TAGS['active-response'], 'copy': False},
}


# Define tags to compress for specific sections
TAG_TO_COMPRESS = {
    'syscheck': {'tags': ['ignore','directories']},
    'ruleset': {'tags': ['list', 'decoder_dir', 'rule_dir', 'rule_exclude']},
    'global': {'tags': ['white_list']},  # NOT USED
}

############################
# HERE : Conf to dataframe 
############################

def process_element(elem):
    """
    Recursively process an element to extract its text, attributes, and nested elements.

    Args:
        elem (ET.Element): The element to process.

    Returns:
        dict: Dictionary containing the extracted data.
    """
    result = {'text': elem.text.strip() if elem.text else None}
    if elem.attrib:
        result.update(elem.attrib)
    for child in elem:
        if child.tag in result:
            if isinstance(result[child.tag], list):
                result[child.tag].append(process_element(child))
            else:
                result[child.tag] = [result[child.tag], process_element(child)]
        else:
            result[child.tag] = process_element(child)
    return result

def extract_section(root, section_name, tags):
    """
    Extracts section data, including nested daughter tags.

    Args:
        root (ET.Element): The root element of the XML tree.
        section_name (str): The name of the section to extract.
        tags (list): List of tags to extract.
        TAG_TO_COMPRESS (dict): Dictionary specifying tags to compress and their attributes.

    Returns:
        pd.DataFrame: DataFrame containing the extracted section data.
    """
    data = []
    elements = root.findall(f'.//{section_name}')
    global TAG_TO_COMPRESS
    for elem in elements:
        # Compress elements if needed
        if section_name in TAG_TO_COMPRESS:
            compress_elements(elem, TAG_TO_COMPRESS[section_name]['tags'])

        row = {}
        for tag in tags:
            tag_elems = elem.findall(tag)
            if tag_elems:
                row[tag] = [process_element(tag_elem) for tag_elem in tag_elems]
            else:
                row[tag] = None
        data.append(row)

    return pd.DataFrame(data).dropna(how='all')

def extract_wodle_section(root):
    """
    Extracts wodle section data.

    Args:
        root (ET.Element): The root element of the XML tree.

    Returns:
        pd.DataFrame: DataFrame containing the extracted wodle section data.
    """
    data = []
    for elem in root.findall('.//wodle'):
        row = {'name': elem.attrib.get('name')}
        for tag in SECTION_TAGS['wodle'][1:]:  # Skip the 'name' tag
            row[tag] = elem.find(tag).text if elem.find(tag) is not None else None
        data.append(row)
    return pd.DataFrame(data).dropna(how='all')

def dataframe_from_conf(file_path: str) -> dict:
    """
    Convert the .conf root element to dataframes for each section, adding position information.

    Args:
        file_path (str): Path to the .conf file.

    Returns:
        dict: Dictionary containing the dataframes for each section.
    """
    logging.info("Running dataframe_from_conf")

    root = load_ossec_conf(file_path)

    # Dictionary to hold dataframes for each section
    dict_dfs = {}

    for section, attributes in SECTION_ATTRIBUTES.items():
        if attributes['copy']:
            continue

        if section == 'wodle':
            dict_dfs[section] = extract_wodle_section(root)
        else:
            tags = attributes['tags']
            dict_dfs[section] = extract_section(root, section, tags)

            # Remove duplicate rows based on the 'text' tag
            if section == 'command':
                dict_dfs[section] = remove_duplicates(dict_dfs[section], subset=['name'])
            else:
                dict_dfs[section] = remove_duplicates(dict_dfs[section], subset=tags)

    # Remove any empty dataframes
    dict_dfs = {section: df for section, df in dict_dfs.items() if not df.empty}

    return dict_dfs

def load_ossec_conf(file_path: str) -> ET.Element:
    """
    Load the ossec.conf XML file and return the root element.

    Args:
        file_path (str): Path to the ossec.conf file.

    Returns:
        ET.Element: The root element of the XML tree.
    """
    logging.info("Running load_ossec_conf")

    # Ensure the file has an .xml extension
    if not file_path.endswith('.xml'):
        new_file = os.path.splitext(file_path)[0] + '.xml'
        subprocess.run(['cp', file_path, new_file], check=True)
        file_path = new_file

    try:
        tree = ET.parse(file_path)
        root = tree.getroot()
        return root
    except Exception as e:
        logging.error(f"Error loading ossec.conf: {e}")
        raise

############################
# HERE : MODIFY DATAFRAMES 
############################

def add_command(commands_df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Add a new command to the commands dataframe if it does not already exist.

    Args:
        commands_df (pd.DataFrame): The commands dataframe.
        **kwargs: Keyword arguments representing the command attributes.

    Returns:
        pd.DataFrame: The updated commands dataframe.
    """
    logging.info("Running add_command")
    try:
        required_keys = SECTION_TAGS['command']
       # for key in required_keys:
          #  assert key in kwargs, f"{key} cannot be None"
        kwargs = format_kwargs(kwargs)
        # Check if the command already exists
        condition = commands_df.apply(
            lambda row: any(item['text'] == kwargs['name'] for item in row['name']) if isinstance(row['name'], list) and row['name'] is not None else row['name']['text'] == kwargs['name'],
            axis=1
        )

        if condition.any():
            logging.debug(f"Command with {kwargs} already exists.")
            return commands_df

        new_command = pd.DataFrame([kwargs])
        commands_df = pd.concat([commands_df, new_command], ignore_index=True)
        return commands_df
    except Exception as e:
        logging.error(f"Error adding command: {e}")
        raise

def add_local_file(localfiles_df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Add a new localfile to the localfiles dataframe if it does not already exist.

    Args:
        localfiles_df (pd.DataFrame): The localfiles dataframe.
        **kwargs: Keyword arguments representing the localfile attributes.

    Returns:
        pd.DataFrame: The updated localfiles dataframe.
    """
    logging.info("Running add_local_file")
    try:
        required_keys = SECTION_TAGS['localfile']
       # for key in required_keys:
          #  assert key in kwargs, f"{key} cannot be None"
        kwargs = format_kwargs(kwargs)

        # Check if the localfile already exists
        condition = localfiles_df.apply(
            lambda row: all(
                any(item['text'] == kwargs[key] for item in row[key]) if isinstance(row[key], list) and row[key] is not None else row[key] and row[key]['text'] == kwargs[key]
                for key in required_keys
            ), axis=1
        )

        if condition.any():
            logging.debug(f"Localfile with {kwargs} already exists.")
            return localfiles_df

        new_localfile = pd.DataFrame([kwargs])
        localfiles_df = pd.concat([localfiles_df, new_localfile], ignore_index=True)
        return localfiles_df
    except Exception as e:
        logging.error(f"Error adding local file: {e}")
        raise
def add_active_response(active_responses_df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Add a new active response to the active_responses dataframe. If the response already exists, update the rules_id.

    Args:
        active_responses_df (pd.DataFrame): The active_responses dataframe.
        **kwargs: Keyword arguments representing the active response attributes.

    Returns:
        pd.DataFrame: The updated active_responses dataframe.
    """
    logging.info("Running add_active_response")
    try:
        required_keys = SECTION_TAGS['active-response']
       # for key in required_keys:
          #  assert key in kwargs, f"{key} cannot be None"
        kwargs = format_kwargs(kwargs)

        # Check if the active response already exists
        condition = active_responses_df.apply(
            lambda row: all(
                any(item['text'] == kwargs[key] for item in row[key] if item is not None) if isinstance(row[key], list) and row[key] is not None else row[key] and row[key]['text'] == kwargs[key]
                for key in required_keys if key != 'rules_id'
            ), axis=1
        )

        existing_ar = active_responses_df[condition]
        if not existing_ar.empty:
            existing_ar_index = existing_ar.index[0]
            current_rules_id = set(existing_ar['rules_id'].iloc[0]['text'].split(','))
            new_rules_id = set(kwargs['rules_id']['text'].split(','))
            if not new_rules_id.issubset(current_rules_id):
                active_responses_df.at[existing_ar_index, 'rules_id'] = {'text': ','.join(current_rules_id.union(new_rules_id))}
                logging.debug(f"Updated active response with new rules_id '{kwargs['rules_id']}'.")
            return active_responses_df

        new_ar = pd.DataFrame([kwargs])
        active_responses_df = pd.concat([active_responses_df, new_ar], ignore_index=True)
        return active_responses_df
    except Exception as e:
        logging.error(f"Error adding active response: {e}")
        raise

def add_fim(syscheck_df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Add a new FIM configuration to the FIM dataframe if it does not already exist.

    Args:
        syscheck_df (pd.DataFrame): The FIM dataframe.
        **kwargs: Keyword arguments representing the FIM configuration attributes.

    Returns:
        pd.DataFrame: The updated FIM dataframe.
    """
    logging.info("Running add_fim")
    try:
        # Define the required keys for FIM configuration
        required_keys = ["directories"]
       # for key in required_keys:
          #  assert key in kwargs, f"{key} cannot be None"
        kwargs = format_kwargs(kwargs)

        # Check if the FIM configuration already exists
        condition = syscheck_df.apply(
            lambda row: all(
                any(item['text'] == kwargs[key] for item in row[key] if item is not None) if isinstance(row[key], list) and row[key] is not None else row[key] and row[key]['text'] == kwargs[key]
                for key in required_keys
            ), axis=1
        )
        if condition.any():
            logging.debug(f"FIM configuration with {kwargs} already exists.")
            return syscheck_df

         # If synchronization key is not given, add the dictionaries to the first row
        if 'synchronization' not in kwargs:
            if not syscheck_df.empty:
                first_row_index = syscheck_df.index[0]
                if 'directories' in syscheck_df.columns and isinstance(syscheck_df.at[first_row_index, 'directories'], list):
                    syscheck_df.at[first_row_index, 'directories'].extend(kwargs['directories'])
                else:
                    syscheck_df.at[first_row_index, 'directories'] = kwargs['directories']
            else:
                # Add the new FIM configuration as a new row
                new_fim_entry = pd.DataFrame([kwargs])
                syscheck_df = pd.concat([syscheck_df, new_fim_entry], ignore_index=True)
        else:
            # Add the new FIM configuration as a new row
            new_fim_entry = pd.DataFrame([kwargs])
            syscheck_df = pd.concat([syscheck_df, new_fim_entry], ignore_index=True)
        return syscheck_df
    except Exception as e:
        logging.error(f"Error adding FIM configuration: {e}")
        raise

def generate_ossec_conf(dict_dfs: dict = None, base_config_path: str = variables_test.PATH_TO_OSSEC_BASE) -> ET.Element:
    """
    Generate the ossec.conf XML from the dataframes using attributes for sections,
    including nested daughter tags.

    Args:
        dict_dfs (dict): Dictionary containing the dataframes for each section.
        base_config_path (str): Path to the base configuration file / Careful to select whether agent or manager base config !
        

    Returns:
        ET.Element: The root element of the generated XML tree.
    """
    logging.info("Running generate_ossec_conf")

    # Load base configuration
    base_root = load_ossec_conf(base_config_path)

    # Create the root element
    ossec_conf = ET.Element('ossec_config')

    # Process sections based on attributes
    for section, attributes in SECTION_ATTRIBUTES.items():
        base_sections = base_root.findall(f'.//{section}')

        if attributes['copy']:
            for base_section in base_sections:
                ossec_conf.append(base_section)
        else:
            # Handle sections with additional monitoring logic
            if section in dict_dfs.keys():
                for _, row in dict_dfs[section].iterrows():
                    section_elem = ET.SubElement(ossec_conf, section)

                    for tag in SECTION_TAGS[section]:
                        value = row.get(tag)
            
                        if isinstance(value, list):
                            for item in value:
                                    tag_elem = ET.SubElement(section_elem, tag)
                                    if isinstance(item, dict):

                                        create_nested_elements(tag_elem, item)
                            
                        elif pd.notna(value):
                            ET.SubElement(section_elem, tag).text = str(value)
            # Compress elements within the section
            if section in TAG_TO_COMPRESS:
                compress_elements(section_elem, TAG_TO_COMPRESS[section]['tags'])

    return ossec_conf

def create_nested_elements(parent_elem: ET.Element, value_dict:dict):
    """
    Recursively create nested elements for a given dictionary.

    Args:
        parent_elem (ET.Element): The parent element.
        value_dict (dict): The dictionary to process.
    """
    for key, val in value_dict.items():
        if val is not None :
            if key == 'text':
                parent_elem.text = val
            elif isinstance(val, dict):
                child_elem = ET.SubElement(parent_elem, key)
                create_nested_elements(child_elem, val)
            else:
                parent_elem.attrib[key] = val
############################
# HERE : SAVE OSSEC
############################

def format_save_ossec_conf_to_xml(file_path: str):
    """
    Format the ossec.conf XML file.

    Args:
        file_path (str): Path to the ossec.conf file.
    """
    logging.info("Running format_save_ossec_conf_to_xml")
    try:
        formatter = xmlformatter.Formatter(indent="1", indent_char="\t", encoding_output="ISO-8859-1", preserve=["literal"])
        formatted_file = formatter.format_file(file_path)
        xmlformatter.save_formatter_result(formatted_file, formatter, True, input_file=file_path, outfile=file_path)
    except Exception as e:
        logging.error(f"Error formatting ossec.conf: {e}")
        raise

def save_ossec_conf(ossec_conf: ET.Element, file_path: str):
    """
    Save the ossec_conf XML to a conf file providing the saving path (.conf)

    Args:
        ossec_conf (ET.Element): The root element of the ossec.conf XML tree.
        file_path (str): Path to save the ossec.conf file.
    """
    logging.info("Running save_ossec_conf")
    try:
        assert(file_path.endswith('.conf'))

        tree = ET.ElementTree(ossec_conf)
        file_path_xml = os.path.splitext(file_path)[0] + '.xml'

        tree.write(file_path_xml, encoding='utf-8', xml_declaration=False)
        format_save_ossec_conf_to_xml(file_path_xml)
        subprocess.run(['cp', file_path_xml, file_path], check=True)

    except Exception as e:
        logging.error(f"Error saving ossec.conf: {e}")
        raise

def save_dataframes(dict_dfs: dict, file_path: str):
    """
    Save the dataframes to an Excel file with multiple sheets,
    and create a backup of the existing file before saving.

    Args:
        dict_dfs (dict): Dictionary containing the dataframes for each section.
        file_path (str): Path to save the Excel file.
    """
    logging.info("Running save_dataframes")

    try:
        # Ensure the backup folder exists & remove current save.
        save_backup(file_path, variables_test.PATH_TO_BACKUP_CONF_FOLDER)
        remove_file_if_exists(file_path)

        # Save the new dataframes
        with pd.ExcelWriter(file_path) as writer:
            for section, df in dict_dfs.items():
                # Ensure the dataframe is not empty
                if isinstance(df, pd.DataFrame) and not df.empty:
                    # Convert dictionary columns to strings
                    df.to_excel(writer, sheet_name=section, index=False)
                else:
                    logging.warning(f"DataFrame for section '{section}' is empty or not a DataFrame. Skipping.")

    except Exception as e:
        logging.error(f"Error saving dataframes to Excel: {e}")
        raise

import ast
def load_dataframes(file_path: str) -> dict:
    """
    Load the dataframes from an Excel file with multiple sheets.

    Args:
        file_path (str): Path to the Excel file.

    Returns:
        dict: Dictionary containing the dataframes for each section.
    """
    logging.info("Running load_dataframes")

    try:
        dict_dfs = {}
        with pd.ExcelFile(file_path) as xls:
            for section in SECTION_TAGS.keys():
                if not SECTION_ATTRIBUTES[section]['copy']:
                    df = pd.read_excel(xls, sheet_name=section)
                    # Convert string columns back to dictionaries and remove duplicates
                    for col in df.columns:
                        df[col] = df[col].apply(lambda x: remove_duplicates_from_list([convert_to_dict(item) for item in ast.literal_eval(x)]) if pd.notna(x) else x)
                    dict_dfs[section] = df
        return dict_dfs
    except Exception as e:
        logging.error(f"Error loading dataframes from Excel: {e}")
        raise


def print_node_text(root: ET.Element):
    """
    Print the text content of each node in the XML tree.

    Args:
        root (ET.Element): The root element of the XML tree.
    """
    logging.info("Running print_node_text")
    for child in root:
        print(f"Tag: {child.tag}, Attributes: {child.attrib}")
        for subchild in child:
            print(f"  Subtag: {subchild.tag}, Text: {subchild.text}")

############################
# HERE : TEST OSSEC
############################
def verify_conf_file_via_bash(remote_host: str, conf_path: str, do_synchronize_with_VM: bool = False, is_agent: bool = False, VM_conf_path: str = None):
    """
    Verify conf file consistency via a bash script.

    Args:
        remote_host (str): The hostname or IP address of the remote machine.
        conf_path (str): The relative (from src) path to the input conf file.
        is_agent (bool): Whether the configuration file is for an agent.
    """
    logging.info(f"Running bash script to verify conf file on {remote_host}")

    try:
        is_agent_flag = "true" if is_agent else "false"
        # Ensure the conf_path is correctly passed to the remote host
        conf_path_remote = os.path.abspath(conf_path)
        
        # NOTE Careful with the relative path
        # Find the part of the path starting with '/vagrant'
        if "/vagrant" in conf_path_remote:
            conf_path_remote = conf_path_remote[conf_path_remote.index("/vagrant"):]

        command = [variables_test.PATH_TO_RUN_SSH_FUNCTION, remote_host, 'test_conf_file', conf_path_remote, is_agent_flag]

        

        result = run_command(command)
        



        if result.returncode == 0:
            logging.info(f"Verification successful:\n{result.stdout}")
            if do_synchronize_with_VM:
                try:
                    synchronize_with_VM(conf_path, remote_host, VM_conf_path)
                except Exception as e:
                    logging.error(f"Error updating the configuration file on the VM: {e}")
                    raise  # Re-raise the exception after logging
                logging.info("Synchronization process completed successfully.")

        else:
            logging.error(f"Verification failed with error:\n{result.stderr}")
            raise ValueError(f"Verification failed for configuration file. Error: {result.stderr}")

    except subprocess.CalledProcessError as e:
        logging.error(f"Error running bash script: {e}")
        raise  # Re-raise the exception to maintain the error flow
    except Exception as e:
        logging.error(f"Unexpected error: {e}")
        raise  # Re-raise for any other unforeseen errors


def synchronize_df_with_conf_file(dict_dfs: dict, conf_path: str, do_synchronize_with_VM: bool = False, remote_host: str = None, VM_conf_path: str = None, base_config_path:str = None):
    """
    Synchronize the dictionaries with the desired configuration file.

    Args:
        dict_dfs (dict): Dictionaries containing modifications.
        conf_path (str): Configuration file to modify.
        do_synchronize_with_VM (bool): Whether to synchronize with the VM.
        remote_host (str): Remote host address for synchronization.
        VM_conf_path (str): Path to the configuration file on the VM.

    Raises:
        AssertionError: If required inputs are missing or invalid.
    """
    logging.info("Starting synchronization process.")

    # Assertions for input validation
    assert isinstance(dict_dfs, dict), "dict_dfs must be a dictionary."
    assert isinstance(conf_path, str) and conf_path.endswith(".conf"), "conf_path must be a string ending with '.conf'."
    assert os.path.isfile(conf_path), f"Configuration file {conf_path} does not exist."


    logging.debug("Generating ossec.conf from provided dictionaries.")

    # NOTE ORDER MATTERS ! CHANGE THE ORDER ONLY FOR DEBUGGING PURPOSES

    ossec_conf = generate_ossec_conf(dict_dfs,base_config_path=base_config_path)


     # Save the dataframes to an Excel file
    try:
        base, _ = os.path.splitext(conf_path)
        excel_path = f"{base}.xlsx"
        logging.debug(f"Saving dataframes to Excel file: {excel_path}.")
        save_dataframes(dict_dfs, excel_path)
    except Exception as e:
        logging.error(f"Error saving dataframes to Excel file: {e}")
        raise

    # Save the new configuration
    try:
        logging.debug(f"Saving ossec.conf to {conf_path}.")
        save_ossec_conf(ossec_conf, conf_path)
    except Exception as e:
        logging.error(f"Error saving ossec.conf: {e}")
        raise

    
    if do_synchronize_with_VM:
        try:
            synchronize_with_VM(conf_path, remote_host, VM_conf_path)
        except Exception as e:
            logging.error(f"Error updating the configuration file on the VM: {e}")
            raise
    logging.info("Synchronization process completed successfully.")



from typing import Union, List, Dict, Any
def add_fim_configuration(conf_path: str, monitored_path: Union[str, Dict[str, Any], List[Dict[str, Any]]], do_synchronize_with_VM: bool = False, remote_host: str = None, VM_conf_path: str = None,base_config_path = variables_test.PATH_TO_OSSEC_AGENT_BASE):
    """
    Add FIM configuration for a specific path in ossec.conf.

    Args:
        conf_path (str): Path to the ossec.conf file.
        monitored_path (str): Path to monitor for FIM or dictionnary containing tag configuration.
        do_synchronize_with_VM (bool): Whether to synchronize with the VM.
        remote_host (str): Remote host address for synchronization.
        VM_conf_path (str): Path to the configuration file on the VM.
    """
    # Load existing configuration
    dict_dfs = dataframe_from_conf(conf_path)
    # Modify the dataframes
    dict_dfs['syscheck'] = add_fim(dict_dfs['syscheck'], directories=monitored_path)

    synchronize_df_with_conf_file(dict_dfs, conf_path, do_synchronize_with_VM, remote_host, VM_conf_path,base_config_path=base_config_path)


def compress_tree(tree, TAG_TO_COMPRESS):
    root = tree.getroot()

    # Find all tags of the specified type (TAG_TO_COMPRESS)
    elements_to_compress = root.findall(f'.//{TAG_TO_COMPRESS}')

    # Filter the elements to keep only those without attributes
    elements_to_compress = [elem for elem in elements_to_compress if len(elem.attrib) == 0]

    # If there are elements to compress
    if elements_to_compress:
        # Collect the text content of all matching elements, join with commas
        compressed_text = ",".join(elem.text.strip() for elem in elements_to_compress if elem.text)

        # Create a mapping of elements to their parent
        parent_map = {c: p for p in root.iter() for c in p}
        parent = parent_map[elements_to_compress[0]]

        # Get the index of the first element to compress
        first_elem_index = list(parent).index(elements_to_compress[0])

        # Safely remove the elements to be compressed
        for elem in elements_to_compress:
            parent_map[elem].remove(elem)

        # Create a new compressed element and insert it where the original tags were
        compressed_element = ET.Element(TAG_TO_COMPRESS)
        compressed_element.text = compressed_text
        parent.insert(first_elem_index, compressed_element)  # Insert at the same location

        logging.debug(f"Successfully compressed {TAG_TO_COMPRESS} tags.")
    else:
        logging.debug(f"No {TAG_TO_COMPRESS} tags found to compress.")

def compress_xml(input_file, output_file, TAG_TO_COMPRESS):
    """
    Compresses all XML tags of the specified type (TAG_TO_COMPRESS) by merging their text content
    into a single tag at the same position in the XML structure.

    Args:
        input_file (str): Path to the input XML file to be compressed.
        output_file (str): Path to the output XML file where the compressed XML will be saved.
        TAG_TO_COMPRESS (str): The name of the XML tag to compress (e.g., 'ignore').
    """
    try:
        logging.debug(f"Running compress_xml with input file: {input_file}, output file: {output_file}, tag to compress: {TAG_TO_COMPRESS}")

        # Parse the XML file
        tree = ET.parse(input_file)

        compress_tree(tree, TAG_TO_COMPRESS)
        # Write the updated XML to the output file
        tree.write(output_file, encoding='utf-8', xml_declaration=False)
        # Optionally, you can call a function to format the output XML (if needed)
        format_save_ossec_conf_to_xml(output_file)
        logging.debug(f"Compressed XML saved to {output_file}")

    except ET.ParseError as e:
        logging.error(f"Error parsing the XML file: {e}")
    except FileNotFoundError as e:
        logging.error(f"File not found: {e}")
    except Exception as e:
        logging.error(f"An unexpected error occurred: {e}")

from collections import defaultdict

def compress_elements(parent: ET.Element, TAG_TO_COMPRESS: list):
    """
    Compress elements within a parent based on the tags to compress.

    Args:
        parent (ET.Element): The parent element.
        TAG_TO_COMPRESS (list): List of tags to compress.
    """
    for tag in TAG_TO_COMPRESS:
        elements_to_compress = parent.findall(f'.//{tag}')

        # Group elements by their attributes
        grouped_elements = defaultdict(list)
        for elem in elements_to_compress:
            attributes = frozenset(elem.attrib.items())
            grouped_elements[attributes].append(elem)

        # Dictionary to keep track of compressed elements
        compressed_elements = {}

        for attributes, elements in grouped_elements.items():
            all_texts = []
            for elem in elements:
                if elem.text:
                    all_texts.extend(elem.text.strip().split(','))

            # Remove duplicates and log if found
            unique_texts = sorted(set(all_texts))
            if len(unique_texts) < len(all_texts):
                logging.warning(f"Redundant entries found in tag '{tag}' with attributes {attributes}. Removed duplicates.")

            compressed_text = ",".join(unique_texts)
            if compressed_text:
                # Check for redundancy
                if compressed_text in compressed_elements:
                    continue

                first_elem_index = list(parent).index(elements[0])

                for elem in elements:
                    parent.remove(elem)

                compressed_element = ET.Element(tag, attrib=dict(attributes))
                compressed_element.text = compressed_text
                parent.insert(first_elem_index, compressed_element)

                # Add the compressed text to the dictionary to track redundancy
                compressed_elements[compressed_text] = compressed_element
