import subprocess
import pandas as pd
import xml.etree.ElementTree as ET
import xmlformatter
import os
import variables
from utils import save_backup, remove_file_if_exists, remove_duplicates, convert_to_dict, format_kwargs, remove_duplicates_from_list
from typing import Union, List, Dict, Any
from .xml_handler import Xml_handler
import ast
import shutil
from variables import PATH_TO_SRC
############################
# HERE : Useful dictionaries 
############################

#TODO General function to update ossec regarding a dictionnary.
# Define the tags for each section
# INFO : ORDER MATTERS ! - > Define the order of concatenation in the .conf file
SECTION_TAGS = {
     
    # HERE Manager ossec.conf 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','extra_args'],
    'ruleset': ['group', 'decoder', 'list', 'decoder_dir', 'rule_dir', 'rule_exclude'],
    'rule_test': ['id', 'level', 'description', 'group', 'decoder', 'list', 'enabled', 'threads', 'max_sessions', 'session_timeout'],
    'localfile': ['log_format', 'command', 'frequency', 'location', 'alias'],
    '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},
     'ruleset': {'tags': SECTION_TAGS['ruleset'], 'copy': False},
    'rule_test': {'tags': SECTION_TAGS['rule_test'], 'copy': False},
    'localfile': {'tags': SECTION_TAGS['localfile'], 'copy': False},
    'active-response': {'tags': SECTION_TAGS['active-response'], 'copy': False},
}


# Define tags to compress for specific sections
# NOTE CAREFUL WITH THIS AS IT MAY NOT BE PARSED CORRECTLY
# INFO DO NOT COMPRESS ruleset (rules won't be find)
TAG_TO_COMPRESS = {
    'syscheck': {'tags': ['ignore','directories']},
    'global': {'tags': ['white_list']},  # NOT USED
}


class Ossec_conf(Xml_handler):

    """
    A utility class for managing ossec.conf configurations.
    """

    def __init__(self, ossec_conf_path: str, base_conf_path: str, **kwargs):
        """
        Initialize the Ossec_conf class with the path to the ossec.conf file and the base configuration path.

        Args:
            ossec_conf_path (str): Path to the ossec.conf file.
            base_conf_path (str): Path to the base configuration file.
        """

        super().__init__(**kwargs)
        
        # self.ossec_conf_path = os.path.join(PATH_TO_SRC,ossec_conf_path)
        # self.base_conf_path = os.path.join(PATH_TO_SRC,base_conf_path)
        # self.ossec_conf_xlsx_path = os.path.join(PATH_TO_SRC,f"{ossec_conf_path.rsplit('.', 1)[0]}.xlsx")
        self.ossec_conf_path = ossec_conf_path
        self.base_conf_path = base_conf_path
        self.ossec_conf_xlsx_path = f"{ossec_conf_path.rsplit('.', 1)[0]}.xlsx"

    def extract_section(self, 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.

        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:
                self.compress_elements(elem, TAG_TO_COMPRESS[section_name]['tags'])

            row = {}
            for tag in tags:
                tag_elems = elem.findall(tag)
                if tag_elems:
                    row[tag] = [self.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(self, 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 _get_dict_from_root(self, root: ET) -> dict:
        """
        Generate the dictionnary relatively to the root element
        Args:
            root (ET): Root tree of the xml file

        Returns:
            dict: Associated dictionaries
        """

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

        for section, attributes in SECTION_ATTRIBUTES.items():
            if attributes['copy']:
                continue
            
            #DRAFT#
            # if section == 'wodle':
            #     dict_dfs[section] = self.extract_wodle_section(root)
            else:
                tags = attributes['tags']
                dict_dfs[section] = self.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
        return {section: df for section, df in dict_dfs.items() if not df.empty}

    # HERE Main public function to use to update the ossec.conf !

    def add_section_tag_to_conf_file(self, section_tag: str, file_path: str = None):
        conf_path = file_path or self.ossec_conf_path

        # Generate dataframe of the new section
        dict_dfs_to_add = self.dataframe_from_xml_tag(section_tag)

        # Load existing configuration
        dict_dfs = self.dataframe_from_conf(conf_path)

        # Focus on copyable sections
        if 'command' in dict_dfs_to_add.keys():
            if 'command' not in dict_dfs:
                dict_dfs['command'] = pd.DataFrame()
            dict_dfs['command'] = self.add_commands(commands_df=dict_dfs['command'], new_commands_df=dict_dfs_to_add['command'])

        if 'localfile' in dict_dfs_to_add.keys():
            if 'localfile' not in dict_dfs:
                dict_dfs['localfile'] = pd.DataFrame()
            dict_dfs['localfile'] = self.add_local_files(localfiles_df=dict_dfs['localfile'], new_localfiles_df=dict_dfs_to_add['localfile'])

        if 'active-response' in dict_dfs_to_add.keys():
            if 'active-response' not in dict_dfs:
                dict_dfs['active-response'] = pd.DataFrame()
            dict_dfs['active-response'] = self.add_active_responses(active_responses_df=dict_dfs['active-response'], new_active_responses_df=dict_dfs_to_add['active-response'])

        # Modify the dataframes
        # dict_dfs['syscheck'] = self.add_fim(dict_dfs['syscheck'], directories=monitored_paths)

        self.synchronize_df_with_conf_file(dict_dfs, conf_path, do_synchronize_with_VM=False)


    

    def dataframe_from_xml_tag(self, xml_str: str) -> dict:
        """
        Convert the XML string to dataframes for each section, adding position information.

        Args:
            xml_str (str): XML string containing the configuration.

        Returns:
            dict: Dictionary containing the dataframes for each section.
        """

        # Parse the XML string
        xml_str = f"<root>{xml_str}</root>"
        root = ET.fromstring(xml_str)

        dict_dfs = self._get_dict_from_root(root)
        return dict_dfs

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

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

        Returns:
            dict: Dictionary containing the dataframes for each section.
        """

        # NOTE ossec_conf_path should be the same as the current ossec_conf_path after a first inizialisation.
        
        file_path = file_path or self.ossec_conf_path
        
        root = self.load_xml_file(file_path)

        dict_dfs = self._get_dict_from_root(root)

        return dict_dfs

        
    
    # HERE Adding sections methods #
    # Ossec conf
    

    def add_commands(self, commands_df: pd.DataFrame, new_commands_df: pd.DataFrame) -> pd.DataFrame:
        """
        Add new commands to the commands dataframe if they do not already exist.

        Args:
            commands_df (pd.DataFrame): The existing commands dataframe.
            new_commands_df (pd.DataFrame): The new commands dataframe to concatenate.

        Returns:
            pd.DataFrame: The updated commands dataframe.
        """
        # Check if any command in new_commands_df already exists in commands_df
        for _, new_command in new_commands_df.iterrows():
            condition = commands_df.apply(
                lambda row: any(item['text'] == new_command['name'][0]['text'] for item in row['name']) if isinstance(row['name'], list) and row['name'] is not None else row['name']['text'] == new_command['name'][0]['text'],
                axis=1
            )

            if condition.any():
                self.logger.debug(f"Command with {new_command.to_dict()} already exists.")
                new_commands_df = new_commands_df.drop(new_command.name)

        # Concatenate the remaining new commands
        commands_df = pd.concat([commands_df, new_commands_df], ignore_index=True)
        return commands_df
    
    @DeprecationWarning
    def add_command(self, 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.
        """
        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():
            self.logger.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
    

    def add_local_files(self, localfiles_df: pd.DataFrame, new_localfiles_df: pd.DataFrame) -> pd.DataFrame:
        """
        Add new localfiles to the localfiles dataframe if they do not already exist.

        Args:
            localfiles_df (pd.DataFrame): The existing localfiles dataframe.
            new_localfiles_df (pd.DataFrame): The new localfiles dataframe to concatenate.

        Returns:
            pd.DataFrame: The updated localfiles dataframe.
        """
        # Check if any localfile in new_localfiles_df already exists in localfiles_df
        for _, new_localfile in new_localfiles_df.iterrows():
            condition = localfiles_df.apply(
                lambda row: all(
                    any(item['text'] == new_localfile[key][0]['text'] for item in row[key]) if isinstance(row[key], list) and row[key] is not None else row[key] and row[key]['text'] == new_localfile[key][0]['text']
                    for key in SECTION_TAGS['localfile']
                ), axis=1
            )

            if condition.any():
                self.logger.debug(f"Localfile with {new_localfile.to_dict()} already exists.")
                new_localfiles_df = new_localfiles_df.drop(new_localfile.name)

        # Concatenate the remaining new localfiles
        localfiles_df = pd.concat([localfiles_df, new_localfiles_df], ignore_index=True)
        return localfiles_df

    @DeprecationWarning
    def add_local_file(self, 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.
        """
        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 SECTION_TAGS['localfile']
            ), axis=1
        )

        if condition.any():
            self.logger.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


    def add_active_responses(self, active_responses_df: pd.DataFrame, new_active_responses_df: pd.DataFrame) -> pd.DataFrame:
        """
        Add new active responses to the active_responses dataframe. If the response already exists, update the rules_id.

        Args:
            active_responses_df (pd.DataFrame): The existing active_responses dataframe.
            new_active_responses_df (pd.DataFrame): The new active_responses dataframe to concatenate.

        Returns:
            pd.DataFrame: The updated active_responses dataframe.
        """
        # Check if any active response in new_active_responses_df already exists in active_responses_df
        for _, new_ar in new_active_responses_df.iterrows():
            condition = active_responses_df.apply(
                lambda row: any(item['text'] == new_ar['command'][0]['text'] for item in row['command']) if isinstance(row['command'], list) and row['command'] is not None else row['command']['text'] == new_ar['command'][0]['text'],
                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][0]['text'].split(','))
                new_rules_id = set(new_ar['rules_id'][0]['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))}]
                    self.logger.debug(f"Updated active response with new rules_id '{new_ar['rules_id']}'.")
                else:
                    self.logger.debug(f"Active response {new_ar.name} with this rules ids {new_rules_id} already exist.")
                    new_active_responses_df = new_active_responses_df.drop(new_ar.name)

        # Concatenate the remaining new active responses
        active_responses_df = pd.concat([active_responses_df, new_active_responses_df], ignore_index=True)
        return active_responses_df

    def add_active_response(self, 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.
        """
        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 SECTION_TAGS['active-response'] 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))}
                self.logger.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



    def add_fims(self, syscheck_df: pd.DataFrame, new_fim_df: pd.DataFrame) -> pd.DataFrame:
        """
        Add new FIM configurations to the FIM dataframe if they do not already exist.

        Args:
            syscheck_df (pd.DataFrame): The existing FIM dataframe.
            new_fim_df (pd.DataFrame): The new FIM configurations dataframe to concatenate.

        Returns:
            pd.DataFrame: The updated FIM dataframe.
        """
        # Check if any FIM configuration in new_fim_df already exists in syscheck_df
        for _, new_fim in new_fim_df.iterrows():
            condition = syscheck_df.apply(
                lambda row: all(
                    any(item['text'] == new_fim[key][0]['text'] 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'] == new_fim[key][0]['text']
                    for key in ['directories']
                ), axis=1
            )

            if condition.any():
                self.logger.debug(f"FIM configuration with {new_fim.to_dict()} already exists.")
                new_fim_df = new_fim_df.drop(new_fim.name)
            else:
                # If synchronization key is not given, add the dictionaries to the first row
                if 'synchronization' not in new_fim:
                    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(new_fim['directories'])
                        else:
                            syscheck_df.at[first_row_index, 'directories'] = new_fim['directories']
                    else:
                        # Add the new FIM configuration as a new row
                        syscheck_df = pd.concat([syscheck_df, pd.DataFrame([new_fim])], ignore_index=True)
                else:
                    # Add the new FIM configuration as a new row
                    syscheck_df = pd.concat([syscheck_df, pd.DataFrame([new_fim])], ignore_index=True)

        return syscheck_df

    def add_fim(self, 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.
        """
        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 ['directories']
            ), axis=1
        )
        if condition.any():
            self.logger.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
    

    def generate_ossec_conf(self, dict_dfs: dict = None, base_conf_path: str = None) -> 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_conf_path (str): Path to the base configuration file.

        Returns:
            ET.Element: The root element of the generated XML tree.
        """

        base_conf_path = base_conf_path or self.base_conf_path
        
        # Load base configuration
        base_root = self.load_xml_file(base_conf_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):
                                        self.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:
                        self.compress_elements(section_elem, TAG_TO_COMPRESS[section]['tags'])

        return ossec_conf

    def create_nested_elements(self, 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)
                    self.create_nested_elements(child_elem, val)
                else:
                    parent_elem.attrib[key] = val

    

    def save_dataframes(self, dict_dfs: dict, file_path: str = None):
        """
        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.
        """

        file_path = file_path or self.ossec_conf_xlsx_path
        # Ensure the file path ends with .xlsx
        if not file_path.endswith('.xlsx'):
            file_path = f"{file_path.rsplit('.', 1)[0]}.xlsx"


        # Ensure the backup folder exists & remove current save.
        save_backup(file_path, variables.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:
                    self.logger.warning(f"DataFrame for section '{section}' is empty or not a DataFrame. Skipping.")

    def load_dataframes(self, file_path: str = None) -> 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.
        """
        # NOTE file_paht en premier , self.path en second ! Sinon problème de logique.
        file_path = file_path or self.ossec_conf_xlsx_path
        
        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

   
    def verify_conf_file_via_bash(self,conf_path:str = None,  do_synchronize_with_VM: bool = False, is_agent: bool = False):
        """
        Verify conf file consistency via a bash script.

        Args:
            do_synchronize_with_VM (bool): Synchronize with VM ? 
            conf_path (str): The relative (from src) path to the input conf file.
            is_agent (bool): Whether the configuration file is for an Agent (ie not a manager).
        """

        remote_host = self.remote_name
        conf_path = conf_path or self.ossec_conf_path


        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.PATH_TO_RUN_SSH_FUNCTION, remote_host, 'test_conf_file', conf_path_remote, is_agent_flag]

        result = self.run_command(command)

        if result.returncode == 0:
            self.logger.debug(f"Verification successful:\n{result.stdout}")
            if do_synchronize_with_VM:
                self.logger.debug("Synchronizing with VM...")
                self.synchronize_with_VM(conf_path)
        else:
            self.logger.warning(f"Verification failed with error:\n{result.stderr}")
            raise ValueError(f"Verification failed for configuration file. Error: {result.stderr}")

    def synchronize_df_with_conf_file(self, dict_dfs: dict, conf_path: str = None, do_synchronize_with_VM: bool = True):
        """
        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.
        """

        conf_path = conf_path or self.ossec_conf_path


        # 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."

        self.logger.debug("Generating ossec.conf from provided dictionaries.")

        ossec_conf = self.generate_ossec_conf(dict_dfs)

        # Save the dataframes to an Excel file
        base, _ = os.path.splitext(conf_path)
        excel_path = f"{base}.xlsx"
        self.logger.debug(f"Saving dataframes to Excel file: {excel_path}.")
        self.save_dataframes(dict_dfs, excel_path)

        # Save the new configuration
        self.logger.debug(f"Saving ossec.conf to {conf_path}.")
        self.save_xml_file(ossec_conf, conf_path)

        # Verify configuration file & synchronize
        self.logger.debug("Synchronizing with VM...")

        self.verify_conf_file_via_bash(conf_path = conf_path,do_synchronize_with_VM=do_synchronize_with_VM,is_agent=self.is_agent)

    def add_fim_configuration(self, monitored_paths: Union[str, Dict[str, Any], List[Dict[str, Any]]], do_synchronize_with_VM: bool = False,conf_path:str = None):
        """
        Add FIM configuration for a specific path in ossec.conf.

        Args:
            monitored_paths (list): Lists of path to monitor for FIM or dictionary containing tag configuration.
            do_synchronize_with_VM (bool): Whether to synchronize with the VM.
            conf_path (str): Path to the ossec.conf file.

        """

        conf_path = conf_path or self.ossec_conf_path

        # Load existing configuration
        dict_dfs = self.dataframe_from_conf(conf_path)
        # Modify the dataframes
        dict_dfs['syscheck'] = self.add_fim(dict_dfs['syscheck'], directories=monitored_paths)

        self.synchronize_df_with_conf_file(dict_dfs, conf_path, do_synchronize_with_VM)

    
     
