#!/usr/bin/env python3

import argparse
import configparser
import logging
import os
import mysql.connector
import requests
import base64

# Configure basic logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

def str_to_bool(value: str) -> bool:
    """Convert a string to boolean for argparse (accepts true/false, yes/no, 1/0)."""
    if isinstance(value, bool):
        return value
    val = str(value).strip().lower()
    if val in ('yes', 'y', 'true', 't', '1'):
        return True
    if val in ('no', 'n', 'false', 'f', '0'):
        return False
    raise argparse.ArgumentTypeError(f"Boolean value expected (true/false), got: {value}")

def decode_token(crypt_key: str, token: str) -> str:
    """Decode an encrypted token using the provided GLPI crypt key."""
    try:
        from nacl.bindings import crypto_aead_xchacha20poly1305_ietf_decrypt
        # Décoder base64
        encrypted = base64.b64decode(token)
        logger.info(f"Arg token: {token}")
        logger.info(f"b64decoded token: {encrypted}")
        # Séparer le nonce (24 bytes) et le ciphertext
        NONCE_SIZE = 24  # SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
        nonce = encrypted[:NONCE_SIZE]
        ciphertext = encrypted[NONCE_SIZE:]
        logger.info(f"nonce: {nonce}")
        logger.info(f"ciphertext: {ciphertext}")
        # Déchiffrer - le nonce est aussi utilisé comme AD (additional data)
        key = base64.b64decode(crypt_key)
        logger.info(f"Arg crypt_key: {crypt_key}")
        logger.info(f"b64decoded crypt_key: {key}")
        decrypted = crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, nonce, nonce, key)
        logger.info(f"decrypted: {decrypted}")
        logger.info(f"decrypted decoded: {decrypted.decode('utf-8')}")
        return decrypted.decode('utf-8')
    except Exception as e:
        logger.error(f"[x] Failed to decode token {token} using key {crypt_key}: {e}")
        return None

def get_medulla_db_settings():
    """Retrieve database connection settings for the Medulla instance from the configuration file."""
    logger.info("[=] Retrieving database connection settings for the Medulla instance from /etc/mmc/plugins/xmppmaster.ini...")
    dbhost = 'localhost'
    dbport = 3306
    dbuser = 'mmc'
    config = configparser.ConfigParser()
    if os.path.exists("/etc/mmc/plugins/xmppmaster.ini"):
        config.read("/etc/mmc/plugins/xmppmaster.ini")
        if os.path.exists("/etc/mmc/plugins/xmppmaster.ini.local"):
            config.read("/etc/mmc/plugins/xmppmaster.ini.local")
    if config.has_option("database", "dbhost"):
        dbhost = config.get('database', 'dbhost')
    if config.has_option("database", "dbport"):
        dbport = config.get('database', 'dbport')
    if config.has_option("database", "dbuser"):
        dbuser = config.get('database', 'dbuser')
    if config.has_option("database", "dbpasswd"):
        dbpass = config.get('database', 'dbpasswd')
    logger.info(f"[v] Retrieved database settings: host='{dbhost}', port='{dbport}', user='{dbuser}', password=xxxxxx.")
    return(dbhost, dbport, dbuser, dbpass)

def check_db_connection(db_host: str, db_port: int, db_name: str, db_user: str, db_pass: str) -> bool:
    """Check if the database connection can be established with the provided parameters."""
    logger.info("Checking database connection with provided parameters...")
    try:
        conn = mysql.connector.connect(
            host=db_host,
            port=db_port,
            user=db_user,
            password=db_pass,
            database=db_name
        )
        logger.info("[v] Database connection successful.")
        return True
    except mysql.connector.Error as err:
        logger.error(f"[x] Database connection failed: {err}")
        # If error 2003 (Can't connect to MySQL server), it may indicate that the database server is not reachable or not running, which could be a common issue. We can provide a more specific message for this case.
        if err.errno == 2003:
            logger.error(f"[x] Unable to connect to the database server at {db_host}:{db_port}. Please check if the database server is running and accessible.")
            logger.error("You may need to allow connections from the Medulla server's IP address in the database server's configuration (eg. 50-server.cnf for MySQL) or firewall settings.")
        # If error 1130 (Host '...' is not allowed to connect to this MySQL server), it may indicate that the database user does not have the necessary permissions to connect from the Medulla server's IP address. We can provide a more specific message for this case as well.
        elif err.errno == 1130:
            logger.error(f"[x] The database user '{db_user}' does not have permission to connect from the Medulla server's IP address. Please check the database user's host permissions and ensure it is allowed to connect from the Medulla server's IP.")
            logger.error(f"You may need to update the database user's host permissions in the database server's user management (eg. GRANT statement for MySQL: GRANT ALL PRIVILEGES ON {db_name}.* TO '{db_user}'@'%' IDENTIFIED BY '{db_pass}';).")
        return False
    finally:
        if 'conn' in locals() and conn.is_connected():
            conn.close()

def check_db_database_exists(db_host: str, db_port: int, db_name: str, db_user: str, db_pass: str) -> bool:
    """Check if the specified database exists and contains the required tables and views."""
    logger.info(f"Checking if database '{db_name}' exists and contains required tables and views...")
    required_tables = [
        'glpi_computers', 'glpi_users', 'glpi_profiles', 'glpi_entities', 'glpi_groups',
        'glpi_softwares', 'glpi_networkports', 'glpi_printers', 'glpi_monitors',
        'glpi_peripherals', 'glpi_tickets', 'glpi_changes', 'glpi_problems',
        'glpi_projects', 'glpi_locations', 'glpi_plugin_glpiinventory_collects'
    ]
    required_views = ['glpi_computers_pulse', 'glpi_view_computers_items_printer', 'glpi_view_computers_items_peripheral', 'glpi_view_peripherals_manufacturers']
    try:
        conn = mysql.connector.connect(
            host=db_host,
            port=db_port,
            user=db_user,
            password=db_pass
        )
        cursor = conn.cursor()
        cursor.execute(f"SHOW DATABASES LIKE '{db_name}'")
        if not cursor.fetchone():
            logger.error(f"[x] Database '{db_name}' does not exist.")
            return False
        cursor.execute(f"USE {db_name}")
        for table in required_tables:
            cursor.execute(f"SHOW TABLES LIKE '{table}'")
            if not cursor.fetchone():
                logger.error(f"[x] Required table '{table}' is missing in database '{db_name}'.")
                return False
        for view in required_views:
            cursor.execute(f"SHOW FULL TABLES IN {db_name} WHERE TABLE_TYPE LIKE 'VIEW' AND Tables_in_{db_name} LIKE '{view}'")
            if not cursor.fetchone():
                logger.error(f"[x] Required view '{view}' is missing in database '{db_name}'.")
                return False
        logger.info("[v] Database contains all required tables and views.")
        return True
    except mysql.connector.Error as err:
        logger.error(f"[x] Database connection failed: {err}")
        return False
    finally:
        if 'conn' in locals() and conn.is_connected():
            conn.close()

def get_tokens_from_db(db_host: str, db_port: int, db_user: str, db_pass: str, db_name: str, api_user: str):
    """Retrieve API tokens from the database."""
    logger.info("[=] Retrieving API tokens from the database...")
    try:
        conn = mysql.connector.connect(
            host=db_host,
            port=db_port,
            user=db_user,
            password=db_pass,
            database=db_name
        )
        cursor = conn.cursor()
        cursor.execute("SELECT app_token from glpi_apiclients WHERE name = 'MMC' LIMIT 1")
        apptoken = cursor.fetchone()
        logger.info("[v] API app token retrieved successfully from the database.")
        cursor.execute(f"SELECT api_token from glpi_users WHERE name = '{api_user}' LIMIT 1")
        usertoken = cursor.fetchone()
        logger.info("[v] API user token retrieved successfully from the database.")
        return apptoken[0], usertoken[0]
    except mysql.connector.Error as err:
        logger.error(f"[x] Error retrieving API tokens from the database: {err}")
        return None, None
    finally:
        if 'cursor' in locals():
            cursor.close()
        if 'conn' in locals() and conn.is_connected():
            conn.close()

def check_api_connection(api_url: str, api_user: str, api_pass: str, api_usertoken: str, api_apptoken: str) -> bool:
    """Check if the API connection can be established with the provided parameters."""
    logger.info("Checking API connection with provided parameters...")
    try:
        #First initialize session with user credentials and app token to verify that the API user credentials and token are correct, as well as to check if there are any IP restrictions configured on the API server that may block the connection from Medulla server.
        response = requests.get(f"{api_url}/initSession", auth=(api_user, api_pass), headers={"App-Token": f"{api_apptoken}"}, timeout=10)
        if response.status_code == 200:
            logger.info("[v] API connection successful using user credentials and app token.")
        else:
            logger.error(f"[x] API connection failed: Received status code {response.status_code}")
            logger.error("[x] Please check if the API endpoint is correct and if there are any IP restrictions configured on the API server.")
            # Give user the curl command to test API connection with provided parameters for easier troubleshooting
            logger.error(f"To test the API connection with the provided parameters, you can run the following curl command from the Medulla server:\n")
            logger.error(f"curl -u {api_user}:{api_pass} -H \"App-Token: {api_apptoken}\" {api_url}/initSession")
            return False
        
        # Now test the API connection using user token to verify that the user token is also correct and can be used for authentication, as it will be needed for the actual API calls made by Medulla to the ITSM provider. This will help ensure that both the app token and user token are valid and can be used for successful API communication.
        response = requests.get(f"{api_url}/initSession?get_full_session=true", headers={"App-Token": f"{api_apptoken}", "Authorization": f"user_token {api_usertoken}"}, timeout=10)
        if response.status_code == 200:
            logger.info("[v] API connection successful using user token authentication.")
            return True
        else:
            logger.error(f"[x] API connection failed with user token authentication: Received status code {response.status_code}")
            # Give user the curl command to test API connection with user token authentication for easier troubleshooting
            logger.error(f"To test the API connection with user token authentication, you can run the following curl command from the Medulla server:\n")
            logger.error(f"curl -H \"App-Token: {api_apptoken}\" -H \"Authorization: user_token {api_usertoken}\" {api_url}/initSession?get_full_session=true")
            return False
    except requests.exceptions.RequestException as e:
        logger.error(f"[x] API connection failed: {e}")
        return False
    
def check_forward_url(forward_url: str) -> bool:
    """Check if the forward URL is reachable."""
    logger.info(f"Checking if forward URL '{forward_url}' is reachable...")
    try:
        response = requests.get(forward_url, timeout=10)
        if response.status_code == 200 or response.status_code == 405:  # 405 Method Not Allowed is also acceptable as it means the URL is reachable but does not allow GET requests
            logger.info("[v] Forward URL is reachable.")
            return True
        else:
            logger.error(f"[x] Forward URL check failed: Received status code {response.status_code}")
            return False
    except requests.exceptions.RequestException as e:
        logger.error(f"[x] Forward URL check failed: {e}")
        return False

def backup_config_file(file_path: str = "/etc/mmc/plugins/glpi.ini.local") -> None:
    """Create a backup of the configuration file before making changes, except if backup already exists."""
    if os.path.exists(file_path):
        backup_path = f"{file_path}.bak"
        if os.path.exists(backup_path):
            logger.info(f"[i] Backup of '{file_path}' already exists at '{backup_path}'. Skipping backup.")
            return
        try:
            with open(file_path, 'r') as original_file:
                content = original_file.read()
            with open(backup_path, 'w') as backup_file:
                backup_file.write(content)
            logger.info(f"[v] Backup of '{file_path}' created at '{backup_path}'.")
        except Exception as e:
            logger.error(f"[x] Failed to create backup of '{file_path}': {e}")
    else:
        logger.warning(f"[i] Configuration file '{file_path}' does not exist. No backup created.")

def update_mmc_itsm_parameters(config_file_path: str = "/etc/mmc/plugins/glpi.ini.local", url: str = None, db_host: str = None, db_port: int = 3306, db_name: str = None, db_user: str = None, db_pass: str = None, api_url: str = None, api_user: str = None, api_pass: str = None, readonly: bool = False) -> None:
    """Update ITSM parameters in /etc/mmc/plugins/glpi.ini.local."""
    logger.info("Updating ITSM parameters in /etc/mmc/plugins/glpi.ini.local...")
    config = configparser.ConfigParser()
    if os.path.exists(config_file_path):
        config.read(config_file_path)
    else:
        logger.warning(f"[i] Configuration file '{config_file_path}' does not exist. A new file will be created.")
    if not config.has_section('main'):
        config.add_section('main')
    config.set('main', 'disable', '0')
    config.set('main', 'dbhost', db_host)
    config.set('main', 'dbport', str(db_port))
    config.set('main', 'dbname', db_name)
    config.set('main', 'dbuser', db_user)
    config.set('main', 'dbpasswd', db_pass)
    config.set('main', 'glpi_computer_uri', '%s/front/computer.form.php?id=' % url)
    if readonly:
        config.set('main', 'dbreadonly', '1')
    if not config.has_section('webservices'):
        config.add_section('webservices')
    config.set('webservices', 'purge_machine', '1')
    config.set('webservices', 'glpi_base_url', api_url)
    if not readonly:
        config.set('webservices', 'glpi_username', api_user)
        config.set('webservices', 'glpi_password', api_pass)
    if not config.has_section('authentication_glpi'):
        config.add_section('authentication_glpi')
    config.set('authentication_glpi', 'baseurl', url)
    config.set('authentication_glpi', 'doauth', '1')
    try:
        with open(config_file_path, 'w') as configfile:
            config.write(configfile)
        logger.info(f"[v] ITSM parameters updated successfully in '{config_file_path}'.")
    except Exception as e:
        logger.error(f"[x] Failed to update ITSM parameters in '{config_file_path}': {e}")

def create_federated_server(db_host: str = None, db_port: int = 3306, db_name: str = None, db_user: str = None, db_pass: str = None) -> None:
    """Create federated server in the database to allow querying ITSM provider database from Medulla."""
    logger.info("Creating federated server in the database to allow querying ITSM provider database from Medulla...")
    dbhost, dbport, dbuser, dbpass = get_medulla_db_settings()
    try:
        conn = mysql.connector.connect(
            host=dbhost,
            port=dbport,
            user=dbuser,
            password=dbpass,
            database='xmppmaster'
        )
        cursor = conn.cursor()
        create_server_query = f"CREATE OR REPLACE SERVER itsm_federated FOREIGN DATA WRAPPER 'mariadb' OPTIONS ( HOST '{db_host}', PORT {db_port}, USER '{db_user}', PASSWORD '{db_pass}', DATABASE '{db_name}' )"
        cursor.execute(create_server_query)
        conn.commit()
        logger.info("[v] Federated server created successfully.")
        create_table_query = f"CREATE OR REPLACE TABLE `local_glpi_configs` (`id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(150) DEFAULT NULL, `value` text DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci CONNECTION='itsm_federated/glpi_configs'"
        cursor.execute(create_table_query)
        conn.commit()
        logger.info("[v] Federated table 'local_glpi_configs' created successfully.")
    except mysql.connector.Error as err:
        logger.error(f"[x] Error creating federated server: {err}")
    finally:
        if cursor:
            cursor.close()
        if conn and conn.is_connected():
            conn.close()

def create_federated_views(db_host: str = None, db_port: int = 3306, db_name: str = None, db_user: str = None, db_pass: str = None) -> None:
    """Create federated views in the database to allow querying ITSM provider database from Medulla."""
    logger.info("Creating federated views in the database to allow querying ITSM provider database from Medulla...")
    dbhost, dbport, dbuser, dbpass = get_medulla_db_settings()
    try:
        conn = mysql.connector.connect(
            host=dbhost,
            port=dbport,
            user=dbuser,
            password=dbpass,
            database='xmppmaster'
        )
        cursor = conn.cursor()
        create_view_query = f"CREATE OR REPLACE TABLE `local_glpi_machines` (`id` int NOT NULL, primary key(id), `entities_id` int(10) NOT NULL DEFAULT 0, `name` varchar(255) NULL DEFAULT NULL, `is_template` tinyint(4) NOT NULL DEFAULT 0, `is_deleted` tinyint(4) NOT NULL DEFAULT 0)ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci CONNECTION='itsm_federated/glpi_computers';"
        cursor.execute(create_view_query)
        conn.commit()
        logger.info("[v] Federated view 'local_glpi_machines' created successfully.")
        create_view_query = f"CREATE OR REPLACE TABLE `local_glpi_filters` (`id` int NOT NULL, primary key(id), `states_id` int(10) NOT NULL DEFAULT 0, `entities_id` int(10) NOT NULL DEFAULT 0, `computertypes_id` int(10) NOT NULL DEFAULT 0, `autoupdatesystems_id` int(10) NOT NULL DEFAULT 0)ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci CONNECTION='itsm_federated/glpi_computers';"
        cursor.execute(create_view_query)
        conn.commit()
        logger.info("[v] Federated view 'local_glpi_filters' created successfully.")
        create_view_query = f"CREATE OR REPLACE TABLE `local_glpi_items_softwareversions` (`id` int NOT NULL, primary key(id), `items_id` int NOT NULL DEFAULT 0, `itemtype` varchar(100) NOT NULL, `softwareversions_id` int NOT NULL DEFAULT 0)ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci CONNECTION='itsm_federated/glpi_items_softwareversions';"
        cursor.execute(create_view_query)
        conn.commit()
        logger.info("[v] Federated view 'local_glpi_items_softwareversions' created successfully.")
        create_view_query = f"CREATE OR REPLACE TABLE `local_glpi_softwareversions` (`id` int NOT NULL, primary key(id), `softwares_id` int NOT NULL DEFAULT 0, `name` varchar(255) DEFAULT NULL, `comment` text DEFAULT NULL)ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci CONNECTION='itsm_federated/glpi_softwareversions';"
        cursor.execute(create_view_query)
        conn.commit()
        logger.info("[v] Federated view 'local_glpi_softwareversions' created successfully.")
        create_view_query = f"CREATE OR REPLACE TABLE `local_glpi_softwares` (`id` int NOT NULL, primary key(id), `name` varchar(255) NULL DEFAULT NULL, `comment` text NULL DEFAULT NULL)ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci CONNECTION='itsm_federated/glpi_softwares';"
        cursor.execute(create_view_query)
        conn.commit()
        logger.info("[v] Federated view 'local_glpi_softwares' created successfully.")
        create_view_query = f"CREATE OR REPLACE TABLE `local_glpi_entities` (`id` int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), `completename` text DEFAULT NULL, `name` varchar(255) DEFAULT NULL)ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci CONNECTION='itsm_federated/glpi_entities';"
        cursor.execute(create_view_query)
        conn.commit()
        logger.info("[v] Federated view 'local_glpi_entities' created successfully.")
    except mysql.connector.Error as err:
        logger.error(f"[x] Error creating federated views: {err}")
    finally:
        if cursor:
            cursor.close()
        if conn and conn.is_connected():
            conn.close()

def update_subsinv_parameters(config_file_path: str = "/etc/pulse-xmpp-agent-substitute/agent_master_substitute_inv.ini.local", inv_forward_url: str = None, inventory_plugin: str = 'glpiinventory', db_host: str = None, db_port: int = 3306, db_name: str = 'glpi', db_user: str = 'glpi', db_pass: str = None) -> None:
    """Update ITSM parameters in /etc/pulse-xmpp-agent-substitute/agent_master_substitute_inv.ini.local."""
    logger.info("Updating ITSM parameters in /etc/pulse-xmpp-agent-substitute/agent_master_substitute_inv.ini.local...")
    config = configparser.ConfigParser()
    if os.path.exists(config_file_path):
        config.read(config_file_path)
    else:
        logger.warning(f"[i] Configuration file '{config_file_path}' does not exist. A new file will be created.")
    if not config.has_section('glpi'):
        config.add_section('glpi')
    config.set('glpi', 'enable_forward', 'True')
    config.set('glpi', 'url_to_forward', inv_forward_url)
    config.set('glpi', 'inventory_plugin', inventory_plugin)
    if not config.has_section('glpidatabase'):
        config.add_section('glpidatabase')
    config.set('glpidatabase', 'glpi_dbhost', db_host)
    config.set('glpidatabase', 'glpi_dbport', str(db_port))
    config.set('glpidatabase', 'glpi_dbuser', db_user)
    config.set('glpidatabase', 'glpi_dbpasswd', db_pass)
    config.set('glpidatabase', 'glpi_dbname', db_name)
    try:
        with open(config_file_path, 'w') as configfile:
            config.write(configfile)
        logger.info(f"[v] ITSM parameters updated successfully in '{config_file_path}'.")
    except Exception as e:
        logger.error(f"[x] Failed to update ITSM parameters in '{config_file_path}': {e}")

def update_client_parameters(config_file_path: str = "/var/lib/pulse2/clients/config/inventory.ini", inventory_agent: str = 'glpiagent') -> None:
    """Update inventory agent parameter in /var/lib/pulse2/clients/config/inventory.ini."""
    logger.info("Updating inventory agent parameter in /var/lib/pulse2/clients/config/inventory.ini...")
    config = configparser.ConfigParser()
    if os.path.exists(config_file_path):
        config.read(config_file_path)
    else:
        logger.warning(f"[i] Configuration file '{config_file_path}' does not exist. A new file will be created.")
    if not config.has_section('parameters'):
        config.add_section('parameters')
    if inventory_agent.lower() == 'glpiagent':
        config.set('parameters', 'agent', inventory_agent)
    else:
        config.remove_option('parameters', 'agent')
    try:
        with open(config_file_path, 'w') as configfile:
            config.write(configfile)
        logger.info(f"[v] Inventory agent parameter updated successfully in '{config_file_path}'.")
    except Exception as e:
        logger.error(f"[x] Failed to update inventory agent parameter in '{config_file_path}': {e}")

def update_subsreg_parameters(config_file_path: str = "/etc/pulse-xmpp-agent-substitute/agent_master_substitute_reg.ini.local", db_host: str = None, db_port: int = 3306, db_name: str = 'glpi', db_user: str = 'glpi', db_pass: str = None) -> None:
    """Update ITSM parameters in /etc/pulse-xmpp-agent-substitute/agent_master_substitute_reg.ini.local."""
    logger.info("Updating ITSM parameters in /etc/pulse-xmpp-agent-substitute/agent_master_substitute_reg.ini.local...")
    config = configparser.ConfigParser()
    if os.path.exists(config_file_path):
        config.read(config_file_path)
    else:
        logger.warning(f"[i] Configuration file '{config_file_path}' does not exist. A new file will be created.")
    if not config.has_section('glpidatabase'):
        config.add_section('glpidatabase')
    config.set('glpidatabase', 'glpi_dbhost', db_host)
    config.set('glpidatabase', 'glpi_dbport', str(db_port))
    config.set('glpidatabase', 'glpi_dbuser', db_user)
    config.set('glpidatabase', 'glpi_dbpasswd', db_pass)
    config.set('glpidatabase', 'glpi_dbname', db_name)
    try:
        with open(config_file_path, 'w') as configfile:
            config.write(configfile)
        logger.info(f"[v] ITSM parameters updated successfully in '{config_file_path}'.")
    except Exception as e:
        logger.error(f"[x] Failed to update ITSM parameters in '{config_file_path}': {e}")

def update_regagent_parameters(config_file_path: str = "/etc/pulse-xmpp-agent-substitute/registeryagent.ini.local", readonly: bool = False) -> None:
    """Update registery agent parameters in /etc/pulse-xmpp-agent-substitute/registeryagent.ini.local."""
    logger.info("Updating registery agent parameters in /etc/pulse-xmpp-agent-substitute/registeryagent.ini.local...")
    config = configparser.ConfigParser()
    if os.path.exists(config_file_path):
        config.read(config_file_path)
    else:
        logger.warning(f"[i] Configuration file '{config_file_path}' does not exist. A new file will be created.")
    if not config.has_section('parameters'):
        config.add_section('parameters')
    if readonly:
        config.set('parameters', 'check_uuidinventory', 'True')
    else:
        config.remove_option('parameters', 'check_uuidinventory')
    try:
        with open(config_file_path, 'w') as configfile:
            config.write(configfile)
        logger.info(f"[v] Registery agent parameters updated successfully in '{config_file_path}'.")
    except Exception as e:
        logger.error(f"[x] Failed to update registery agent parameters in '{config_file_path}': {e}")

def update_saas_application_parameters(api_url: str = None, api_usertoken: str = None, api_apptoken: str = None, crypt_key: str = None) -> None:
    """Update saas_application parameters in the database."""
    logger.info("Updating saas_application parameters in the database...")
    dbhost, dbport, dbuser, dbpass = get_medulla_db_settings()
    try:
        conn = mysql.connector.connect(
            host=dbhost,
            port=dbport,
            user=dbuser,
            password=dbpass,
            database='admin'
        )
        cursor = conn.cursor()
        update_query = f"UPDATE admin.saas_application SET setting_value='{api_apptoken}' WHERE setting_name='glpi_mmc_app_token'; UPDATE admin.saas_application SET setting_value='{api_usertoken}' WHERE setting_name='glpi_root_user_token'; UPDATE admin.saas_application SET setting_value='{api_url}' WHERE setting_name='glpi_url_base_api'; UPDATE admin.saas_application SET setting_value='{crypt_key}' WHERE setting_name='glpi_crypt_key';"
        for _ in cursor.execute(update_query, multi=True):
            pass
        conn.commit()
        logger.info("[v] saas_application parameters updated successfully.")
    except mysql.connector.Error as err:
        logger.error(f"[x] Error updating saas_application parameters: {err}")
    finally:
        if cursor:
            cursor.close()
        if conn:
            conn.close()

def restart_required_services():
    """Restart services that are required for the changes to take effect."""
    logger.info("Restarting services to apply changes...")
    try:
        os.system("systemctl restart mmc-agent")
        os.system("systemctl restart pulse-xmpp-master-substitute-inventory")
        os.system("systemctl restart pulse-xmpp-master-substitute-registration")
        logger.info("[v] Services restarted successfully.")
    except Exception as e:
        logger.error(f"[x] Failed to restart services: {e}")

def ask_reconfiguration():
    """Update machines table to mark all machines as needing reconfiguration, so that they will be reconfigured with the new ITSM parameters."""
    logger.info("Marking all machines as needing reconfiguration to apply new ITSM parameters...")
    dbhost, dbport, dbuser, dbpass = get_medulla_db_settings()
    try:
        conn = mysql.connector.connect(
            host=dbhost,
            port=dbport,
            user=dbuser,
            password=dbpass,
            database='xmppmaster'
        )
        cursor = conn.cursor()
        update_query = f"UPDATE xmppmaster.machines SET need_reconf=1 WHERE agenttype='machine'"
        cursor.execute(update_query)
        conn.commit()
        logger.info("[v] All machines marked as needing reconfiguration.")
    except mysql.connector.Error as err:
        logger.error(f"[x] Error marking machines for reconfiguration: {err}")
    finally:
        if cursor:
            cursor.close()
        if conn:
            conn.close()
            
def main():
    parser = argparse.ArgumentParser(description="Update ITSM parameters")
    parser.add_argument("--url", required=True, help="ITSM provider URL")
    parser.add_argument("--db-host", required=True, help="ITSM provider database host")
    parser.add_argument("--db-port", required=False, default=3306, help="ITSM provider database port")
    parser.add_argument("--db-name", required=True, help="ITSM provider database name")
    parser.add_argument("--db-user", required=True, help="ITSM provider database user")
    parser.add_argument("--db-pass", required=True, help="ITSM provider database password")
    parser.add_argument("--api-url", required=True, help="ITSM provider API URL")
    parser.add_argument("--api-user", required=False, help="ITSM provider API user")
    parser.add_argument("--api-pass", required=False, help="ITSM provider API password")
    parser.add_argument("--readonly", required=False, type=str_to_bool, default=False, help="Whether ITSM provider database is read-only for Medulla (optional)")
    parser.add_argument("--crypt-key", required=False, help="Decoded GLPI crypt key file - base64 /etc/glpi/glpicrypt.key (optional)")
    parser.add_argument("--inv-forward", required=False, type=str_to_bool, default=True, help="Whether to forward inventory data to ITSM provider (optional)")
    parser.add_argument("--inv-forward-url", required=False, help="URL to forward inventory data to (optional)")
    parser.add_argument("--inv-plugin", required=False, default="glpiinventory", help="Inventory plugin to use - glpiinventory or fusioninventory(optional)")
    parser.add_argument("--inv-agent", required=False, default="glpiagent", help="Inventory agent to use on client machine - glpiagent or fusioninventory (optional, required if --inv-forward is true)")
    parser.add_argument("--inv-agent-disabled", required=False, type=str_to_bool, default=False, help="Whether to include inventory agent in Medulla agent (optional)")

    args = parser.parse_args()
    # Make sure arguments are provided in the expected format and validate them before proceeding
    if bool(args.readonly) and bool(args.inv_forward):
        logger.error("[x] Invalid argument combination: --readonly cannot be true when --inv-forward is true, as forwarding inventory data to ITSM provider would require write access to the database. Please set --readonly to false if you want to enable inventory forwarding.")
        exit(1) 
    if not check_db_connection(args.db_host, args.db_port, args.db_name, args.db_user, args.db_pass):
        exit(1)
    if not bool(args.readonly):
        if not args.api_user or not args.api_pass:
            logger.error("[x] API user credentials are required when --readonly is false. Please provide --api-user and --api-pass.")
            exit(1)
        if get_tokens_from_db(args.db_host, args.db_port, args.db_user, args.db_pass, args.db_name, args.api_user) == (None, None):
            exit(1)
        api_apptoken, api_usertoken = get_tokens_from_db(args.db_host, args.db_port, args.db_user, args.db_pass, args.db_name, args.api_user)
        if '/' in api_apptoken or '=' in api_apptoken or len(api_apptoken) > 50:
            # App token is encypted with crypt_key
            if not args.crypt_key:
                logger.error(f"[x] API app token {api_apptoken} appears to be encrypted. Please provide the decoded GLPI crypt key file using --crypt-key argument.")
                exit(1)
            else:
                decoded_api_apptoken = decode_token(args.crypt_key, api_apptoken)
                decoded_api_usertoken = decode_token(args.crypt_key, api_usertoken)
        else:
            decoded_api_apptoken = api_apptoken
            decoded_api_usertoken = api_usertoken
        if not check_api_connection(args.api_url, args.api_user, args.api_pass, decoded_api_usertoken, decoded_api_apptoken):
            exit(1)
    if not check_db_database_exists(args.db_host, args.db_port, args.db_name, args.db_user, args.db_pass):
        exit(1)
    if bool(args.inv_forward):
        if not args.inv_forward_url:
            logger.error("[x] Inventory forwarding is enabled but --inv-forward-url is not provided. Please provide the URL to forward inventory data to.")
            exit(1)
        if not args.inv_plugin:
            logger.error("[x] Inventory forwarding is enabled but --inv-plugin is not provided. Please specify the inventory plugin to use (glpiinventory or fusioninventory).")
            exit(1)
        if not args.inv_agent:
            logger.error("[x] Inventory forwarding is enabled but --inv-agent is not provided. Please specify the inventory agent to use on client machine (glpiagent or fusioninventory).")
            exit(1)
        if not check_forward_url(args.inv_forward_url):
            exit(1)
    
    # All checks passed, proceed with backup and update
    backup_config_file(file_path="/etc/mmc/plugins/glpi.ini.local")
    update_mmc_itsm_parameters(url=args.url, db_host=args.db_host, db_port=args.db_port, db_name=args.db_name, db_user=args.db_user, db_pass=args.db_pass, api_url=args.api_url, api_user=args.api_user, api_pass=args.api_pass, readonly=args.readonly)
    create_federated_server(db_host=args.db_host, db_port=args.db_port, db_name=args.db_name, db_user=args.db_user, db_pass=args.db_pass)
    create_federated_views(db_host=args.db_host, db_port=args.db_port, db_name=args.db_name, db_user=args.db_user, db_pass=args.db_pass)
    if not bool(args.readonly):
        update_saas_application_parameters(api_url=args.api_url, api_usertoken=api_usertoken, api_apptoken=api_apptoken, crypt_key=args.crypt_key)
    if bool(args.inv_forward):
        backup_config_file(file_path="/etc/pulse-xmpp-agent-substitute/agent_master_substitute_inv.ini.local")
        update_subsinv_parameters(inv_forward_url=args.inv_forward_url, inventory_plugin=args.inv_plugin, db_host=args.db_host, db_port=args.db_port, db_name=args.db_name, db_user=args.db_user, db_pass=args.db_pass)
        backup_config_file(file_path="/var/lib/pulse2/clients/config/inventory.ini")
        update_client_parameters(config_file_path="/var/lib/pulse2/clients/config/inventory.ini", inventory_agent=args.inv_agent)
    backup_config_file(file_path="/etc/pulse-xmpp-agent-substitute/agent_master_substitute_reg.ini.local")
    update_subsreg_parameters(db_host=args.db_host, db_port=args.db_port, db_name=args.db_name, db_user=args.db_user, db_pass=args.db_pass)
    if bool(args.readonly):
        backup_config_file(file_path="/etc/pulse-xmpp-agent-substitute/registeryagent.ini.local")
        update_regagent_parameters(config_file_path="/etc/pulse-xmpp-agent-substitute/registeryagent.ini.local", readonly=True)
    restart_required_services()
    ask_reconfiguration()
    if bool(args.inv_agent_disabled):
        logger.info("Inventory agent will be disabled in Medulla agent configuration and agent regenerated.")
        backup_config_file(file_path="/var/lib/pulse2/clients/.generation_options")
        # Run command /var/lib/pulse2/clients/generate-pulse-agent.sh --disable-inventory to regenerate agent installer
        try:
            result = os.system("/var/lib/pulse2/clients/generate-pulse-agent.sh --disable-inventory > /tmp/generate_agent.log 2>&1")
            if result == 0:
                logger.info("[v] Medulla agent installer regenerated successfully with inventory agent disabled.")
            else:
                logger.error(f"[x] Failed to regenerate Medulla agent installer. Command exited with status {result}. Please check the command output for details.")
        except Exception as e:
            logger.error(f"[x] Failed to regenerate Medulla agent installer: {e}")

if __name__ == "__main__":
    main()
