#! /usr/bin/python3
#-------------------------------------------------------------------------------
#
# Copyright (C) 2025 NVIDIA Corporation. ALL RIGHTS RESERVED.
#
#-------------------------------------------------------------------------------

# Imports
import pyinotify
import os
import time
import sys
import subprocess
import json
import threading
import syslog
import warnings
import re
import signal
import traceback
import argparse
import configparser
import cumulus.sdnotify

# global variables

ReloadDir = '/cumulus/tcvrcntl/'
ReloadFile = ReloadDir + 'reload'
tcvr_user_config_file = '/etc/cumulus/tcvr_temperature_setpoint.conf'
tcvr_config_change_monitor_file = "/etc/cumulus/tcvr_temp_monitor"
tcvr_config_lock = threading.Lock()  # Combined lock for config file and processing
tcvr__thread_process_req_cnt = threading.Lock()
tc_max_temp_control_limit = 30
tc_def_temp_control_offset = 0
tcvr_temp_cntrl_info = {}
tcvr_thread_cfg_process_cnt = 0
tcvr_thread_name_user_cfg_change = "user_config_change"
tcvr_thread_name_present_file_change = "present_file_change"
tc_service_restart_retry_max = 5
tcvr_halt = 0


# Classes
class ArgParseError(RuntimeError):
    pass


class TCVRCNTLRuntimeError(RuntimeError):
    pass


class TcvrTempCntrlInfo:
    def __init__(self):
        self.interface = None      # Interface name from config (e.g., swp1)
        self.transceiver = None      # Actual transceiver name (e.g., swp1 or swp1s0)
        self.module = None         # Module number (e.g., module0)
        self.user_config_temp = None # User-defined temperature from config
        self.high_warning_threshold = None # High warning threshold from ethtool
        self.val_max_offset = None   # Difference between high warning threshold and user temp
        self.alarm_raised = False    # Track if alarm is currently raised
        self.sysfs_present_monitor = "" # path for present file sx_core/asic0/{file_name}/present
        self.temp_setpint_low_warn = "" # setpoint lower than allowed range
        self.temp_setpint_high_warn = "" # setpoint above module high warn temperature
    
    def tcvr_unset_tc_max_offset(self):
        self.val_max_offset = None
        syslog.syslog(syslog.LOG_INFO, f"tcvrcntl {self.transceiver} unset tc_max_offset to None")
    
    def tcvr_set_tc_max_offset(self, offset):
        self.val_max_offset = offset
        syslog.syslog(syslog.LOG_INFO, f"tcvrcntl {self.transceiver} set tc_max_offset to {self.val_max_offset}")


class CommonEventHandler(pyinotify.ProcessEvent):
    def __init__(self):
        super().__init__()
        self.last_states = {}
        self.notifier = None

    def set_notifier(self, notifier):
        self.notifier = notifier
        syslog.syslog(syslog.LOG_DEBUG, "tcvrcntl notifier set successfully")

    def process_default(self, event):
        global tcvr_thread_cfg_process_cnt
        try:
            syslog.syslog(syslog.LOG_DEBUG, f"tcvrcntl received event: {event.maskname} for {event.pathname}")
                    
            if event.mask & pyinotify.IN_IGNORED:
                syslog.syslog(syslog.LOG_WARNING, f"tcvrcntl watch removed for {tcvr_config_change_monitor_file}")
                return
            
            syslog.syslog(syslog.LOG_INFO, f"tcvrcntl configuration file {tcvr_config_change_monitor_file} changed")
            is_valid_file = tcvr_is_modified_file_valid(event.pathname)
                    
            if is_valid_file:
                tcvr_thread_name = tcvr_thread_name_user_cfg_change
                if tcvr_config_change_monitor_file not in event.pathname:
                    tcvr_thread_name = tcvr_thread_name_present_file_change 
                    with open(event.pathname, 'r') as f:
                        present_value = f.read().strip()
                        if present_value != '1':  # Skip if not present
                            syslog.syslog(syslog.LOG_INFO,
                                f"tcvrcntl skip process event {event.pathname} "
                                f"present {present_value}"
                            )
                            return
                
                is_set = True
                tcvr_set_unset_active_thread_count(is_set)
                config_thread = threading.Thread(
                    target=tcvr_process_configuration_change,
                    daemon=True,
                    name=tcvr_thread_name
                )
                config_thread.start()
            else:
                syslog.syslog(syslog.LOG_INFO, f"tcvrcntl modifiled file is not valid {event.pathname}")
        except Exception as e:
            syslog.syslog(syslog.LOG_ERR, f"tcvrcntl error processing event: {e}")


# Functions
def tcvrcntlwarn(message, category, filename, lineno, line=None):
    """
    Custom warning formatter for tcvrcntl.
    Formats warnings with filename, line number, category, and message.
    """
    return '%s:%s : %s : %s\n' % (
        filename, lineno, category.__name__, message
    )


def setHaltTCVRControl(h):
    """
    Set the global halt flag for TCVR control.
    Args:
        h: Boolean value to set the halt flag
    """
    global tcvr_halt
    tcvr_halt = h


def getHaltTCVRControl():
    """
    Get the current value of the TCVR control halt flag.
    Returns:
        bool: Current value of tcvr_halt
    """
    global tcvr_halt
    return tcvr_halt


# check to see if an instance is already running
def already_running(pidfile):
    """
    Check if another instance of tcvrcntl is already running.
    Args:
        pidfile: Path to the PID file
    Returns:
        bool: True if another instance is running, False otherwise
    Raises:
        TCVRCNTLRuntimeError: If unable to validate pidfile
    """
    myname = os.path.basename(sys.argv[0])
    try:
        if not os.path.isfile(pidfile):
            return False
        with open(pidfile, 'r') as f:
            oldpid = re.findall(
                '\D*(\d+).*',
                f.readline()
            )[0]
        if not os.path.exists('/proc/%s' % oldpid):
            return False
        with open('/proc/%s/cmdline' % oldpid, 'r') as f:
            if myname not in f.readline():
                return False
        sys.stderr.write(
            "%s already running as process %s\n" % (myname, oldpid)
        )
        return True
    except Exception as inst:
        raise TCVRCNTLRuntimeError(
            "unable to validate pidfile %s: %s" % (pidfile, str(inst))
        )


# normal exit
def exit_normally(signum=0, frame=None):
    """
    Clean up resources and exit the program normally.
    Args:
        signum: Signal number (default 0)
        frame: Current stack frame (default None)
    """
    setHaltTCVRControl(1)
    syslog.syslog(syslog.LOG_INFO, "exiting normally")
    sys.stderr.write("%s : exiting normally\n" % str(sys.argv[0]))
    exit(0)


def tcvr_reset_tc_restart_fail_count():
    """
    Reset the failed count for hw-management-tc.service.
    Returns:
        bool: True if successful, False otherwise
    """
    try:
        result = subprocess.run(
            ['systemctl', 'reset-failed', 'hw-management-tc.service'],
            capture_output=True,
            text=True,
            check=True
        )
        syslog.syslog(syslog.LOG_INFO, "hw-management-tc service reset success")
        return True
    except subprocess.CalledProcessError as e:
        syslog.syslog(syslog.LOG_ERR, f"Error hw-management-tc reset failed: {e}")
        if e.stdout:
            syslog.syslog(syslog.LOG_ERR, f"stdout: {e.stdout}")
        if e.stderr:
            syslog.syslog(syslog.LOG_ERR, f"stderr: {e.stderr}")
        return False


def tcvr_restart_hw_management_service():
    """
    Restart the hw-management-tc.service.
    Returns:
        bool: True if successful, False otherwise
    """
    try:
        result = subprocess.run(
            ['systemctl', 'restart', 'hw-management-tc.service'],
            capture_output=True,
            text=True,
            check=True
        )
        syslog.syslog(syslog.LOG_INFO, f"hw-management-tc service restarted successfully")
        return True
    except subprocess.CalledProcessError as e:
        syslog.syslog(syslog.LOG_ERR, f"Error restarting hw-management-tc service: {e}")
        if e.stdout:
            syslog.syslog(syslog.LOG_ERR, f"stdout: {e.stdout}")
        if e.stderr:
            syslog.syslog(syslog.LOG_ERR, f"stderr: {e.stderr}")
        return False


def tcvr_check_and_restart_hw_management_service():
    """
    Attempt to restart hw-management-tc.service with retries.
    Uses global tc_service_restart_retry_max for maximum retry attempts.
    Returns:
        bool: True if restart was successful, False if all retries failed
    """
    global tc_service_restart_retry_max
    retry = tc_service_restart_retry_max
    
    while retry > 0:
        is_success = tcvr_restart_hw_management_service()
        tcvr_reset_tc_restart_fail_count()
        if is_success:
            return True
        retry -= 1

    if not retry:
        syslog.syslog(syslog.LOG_ERR, f"service restart failed after {tc_service_restart_retry_max} retries")
    return False


def tcvr_check_config_diff_and_restart_tc():
    """
    Check for configuration differences and restart the thermal control service if needed.
    Writes temperature configuration to JSON files and manages service restarts.
    """
    if not tcvr_temp_cntrl_info:
        syslog.syslog(syslog.LOG_WARNING, "No temperature information to write to JSON")
        return

    config = {"sensors_config": {}}
    for key, tcvr_info in tcvr_temp_cntrl_info.items():
        if tcvr_info.module:
            offset = 0
            if tcvr_info.val_max_offset:
                offset = int(tcvr_info.val_max_offset * 1000)
            config["sensors_config"][tcvr_info.module] = {
                "val_max_offset": offset
            }

    if not config["sensors_config"]:
        syslog.syslog(syslog.LOG_WARNING, "No valid temperature configurations to write")
        return

    temp_config_path_file = '/etc/hw-management-thermal/tc_config_user_temp.json'
    original_config_path_file = '/etc/hw-management-thermal/tc_config_user.json'
    tc_config_path_file = '/var/run/hw-management/config/tc_config_user.json'

    # Backup existing temp config if it exists
    if os.path.exists(temp_config_path_file):
        backup_path = f"{temp_config_path_file}.bak"
        os.rename(temp_config_path_file, backup_path)
    try:
        tc_restart = False
        # Write to JSON file
        with open(temp_config_path_file, 'w') as f:
            json.dump(config, f, indent=4)
            syslog.syslog(syslog.LOG_INFO,
                f"Updated temperature configuration in {temp_config_path_file}"
            )
        
        if os.path.exists(original_config_path_file):
            with open(original_config_path_file, 'r') as f:
                temp_config = json.load(f)
                if temp_config != config:
                    tc_restart = True    
        else:
            tc_restart = True
        
        if tc_restart:
            with open(original_config_path_file, 'w') as f:
                json.dump(config, f, indent=4)
                syslog.syslog(syslog.LOG_INFO,
                    f"Updated temperature configuration in "
                    f"{original_config_path_file}"
                )
            
            with open(tc_config_path_file, 'w') as f:
                json.dump(config, f, indent=4)
                syslog.syslog(syslog.LOG_INFO,
                    f"Updated temperature configuration in {tc_config_path_file}"
                )
            
            if tcvr_check_and_restart_hw_management_service():
                syslog.syslog(syslog.LOG_INFO, "Processed restart of hw-management-tc.service")
            else:
                syslog.syslog(syslog.LOG_ERR, "Failed to restart hw-management-tc.service")
        else:
            syslog.syslog(syslog.LOG_INFO,
                f"Skip TC service restart existing {original_config_path_file} and new {temp_config_path_file} "
                f"configuration same"
            )
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Error processing pending restart: {e}")
        traceback.print_exc()


def tcvr_init_temp_cntrl_info():
    """
    Initialize temperature control information for all modules.
    Scans system for valid modules and creates corresponding control info objects.
    """
    valid_module_list = []
    
    # Check if required base directories exist before accessing
    base_dirs = [
        '/sys',
        '/sys/module',
        '/sys/module/sx_core'
    ]
    
    for dir_path in base_dirs:
        if not os.path.exists(dir_path):
            syslog.syslog(syslog.LOG_WARNING, f"Required directory {dir_path} does not exist, skipping module initialization")
            return
    
    # Discover all ASIC instances
    asic_instances = []
    try:
        for item in os.listdir('/sys/module/sx_core'):
            if item.startswith('asic') and os.path.isdir(f'/sys/module/sx_core/{item}'):
                asic_instances.append(item)
    except OSError as e:
        syslog.syslog(syslog.LOG_ERR, f"Error accessing directory /sys/module/sx_core: {e}")
        return
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Unexpected error while listing directory /sys/module/sx_core: {e}")
        return
    
    if not asic_instances:
        syslog.syslog(syslog.LOG_INFO, "No ASIC instances found in /sys/module/sx_core, skipping module initialization")
        return
    
    syslog.syslog(syslog.LOG_INFO, f"Found ASIC instances: {asic_instances}")
    
    # Scan all ASIC instances for modules
    for asic_instance in asic_instances:
        asic_path = f'/sys/module/sx_core/{asic_instance}'
        try:
            for file_name in os.listdir(asic_path):
                module_name = re.search(r'module(\d+)$', file_name)
                if module_name:
                    num = file_name.split("module")
                    # sysfs is 0 based and ethtool interface is 1 based
                    port_num = int(num[1]) + 1
                    valid_module_list.append(port_num)
                    syslog.syslog(syslog.LOG_DEBUG, f"Found module{port_num} in {asic_instance}")
        except OSError as e:
            syslog.syslog(syslog.LOG_ERR, f"Error accessing directory {asic_path}: {e}")
            continue
        except Exception as e:
            syslog.syslog(syslog.LOG_ERR, f"Unexpected error while listing directory {asic_path}: {e}")
            continue
    
    if not valid_module_list:
        syslog.syslog(syslog.LOG_WARNING, "No valid modules found in any ASIC instance")
        return
    
    valid_module_list.sort()
    syslog.syslog(syslog.LOG_INFO, f"Total modules found: {valid_module_list}")
    
    # Create module info objects - need to determine which ASIC each module belongs to
    for num in valid_module_list:
        module_name = f"module{num}"
        # Find which ASIC instance contains this module
        module_found = False
        for asic_instance in asic_instances:
            asic_path = f'/sys/module/sx_core/{asic_instance}'
            module_path = f'{asic_path}/module{num-1}/present'
            if os.path.exists(module_path):
                tcvr_info = TcvrTempCntrlInfo()
                tcvr_info.module = module_name 
                tcvr_info.sysfs_present_monitor = module_path
                tcvr_temp_cntrl_info[module_name] = tcvr_info
                module_found = True
                syslog.syslog(syslog.LOG_DEBUG, f"Assigned {module_name} to {asic_instance}")
                break
        
        if not module_found:
            syslog.syslog(syslog.LOG_INFO, f"Could not find present file for {module_name} in any ASIC instance")


def tcvr_update_temp_cntrl_info():
    """
    Update transceiver information for all modules.
    Sets interface and transceiver names based on system configuration.
    """
    for key, value in tcvr_temp_cntrl_info.items():
        module_num = int(key.split('module')[1])
        name = f"swp{module_num}s0"
        path = f"/sys/class/net/"
        value.interface = f"swp{module_num}"
        if os.path.exists(f"{path}{name}"):
            value.transceiver = name
        else:
            value.transceiver = f"swp{module_num}"


def tcvr_print_temp_cntrl_info():
    """
    Print current temperature control information for all modules.
    Outputs module details including interface, transceiver, and monitoring paths.
    """
    for key, tcvr_info in tcvr_temp_cntrl_info.items():
        syslog.syslog(syslog.LOG_DEBUG, f"{key} {tcvr_info.transceiver} {tcvr_info.module} "
                      f"{tcvr_info.val_max_offset} {tcvr_info.sysfs_present_monitor}")


def tcvr_create_monitor_file():
    """
    Create the monitor file for TCVR configuration changes.
    Creates the file if it doesn't exist.
    """
    try:
        if not os.path.exists(tcvr_config_change_monitor_file):
            fl = open(tcvr_config_change_monitor_file, "w")
            fl.write("tcvr temperature monitoring")
            fl.close()
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Error creating file {tcvr_config_change_monitor_file}: {e}")


def tcvr_get_instance_from_name(cntrl_info, module_name):
    """
    Get a TCVR control info instance by module name.
    Args:
        cntrl_info: Dictionary of control info objects
        module_name: Name of the module to find
    Returns:
        TcvrTempCntrlInfo: The found instance or None if not found
    """
    for key, tcvr_info in cntrl_info.items():
        if key == module_name:
            return tcvr_info
    return None


def tcvr_is_intf_name_valid(interface):
    """
    Validate if an interface name is in the correct format.
    Args:
        interface: Interface name to validate
    Returns:
        bool: True if valid, False otherwise
    """
    match = re.search(r'swp\d+$',interface)
    if match:
        return True
    else:
        return False


def tcvr_get_hardware_temperature(transceiver):
    """
    Get high warning threshold from ethtool output for a transceiver.
    Args:
        transceiver: Transceiver name (e.g., swp1 or swp1s0)
    Returns:
        float: High warning threshold in Celsius, None if not found
    """
    if not transceiver:
        return None
        
    try:
        # Run ethtool command
        cmd = ['/usr/sbin/ethtool', '-m', transceiver]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        syslog.syslog(syslog.LOG_DEBUG, f"ethtool output {result}")

        # Parse output for high warning threshold
        for line in result.stdout.split('\n'):
            if 'Module temperature high warning threshold' in line and 'degrees C' in line:
                # Extract temperature value (e.g., "70.00 degrees C")
                temp_str = line.split(':')[1].strip().split()[0]
                temp_value = float(temp_str)
                syslog.syslog(syslog.LOG_DEBUG, f"Parsed high warning threshold for {transceiver}: {temp_value}°C")
                return temp_value
        syslog.syslog(syslog.LOG_DEBUG, f"No high warning threshold found in ethtool output for {transceiver}")
        return None
    except subprocess.CalledProcessError as e:
        syslog.syslog(syslog.LOG_WARNING, f"ethtool: {e}")
        return None
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Error parsing ethtool output: {e}")
        return None


def tcvr_get_eeprom_high_warn(module_num):
    """
    Get high warning threshold from EEPROM for a specific module.
    Args:
        module_num: Module number to check
    Returns:
        float: High warning threshold in Celsius, None if not found
    """
    tcvr_name = f"swp{module_num}"
    name = f"/sys/class/net/swp{module_num}s0"
    if os.path.exists(name):
        tcvr_name = f"swp{module_num}s0"
    #get ethtool output and parse
    return tcvr_get_hardware_temperature(tcvr_name)


def tcvr_parse_and_update_temp_cntrl_info(new_tcvr_temp_cntrl_info):
    """
    Parse configuration file and update temperature control information.
    Args:
        new_tcvr_temp_cntrl_info: Dictionary to store updated control info
    Returns:
        bool: True if processing should be skipped, False otherwise
    """
    try:
        with open(tcvr_user_config_file, 'r') as f:
            for line in f:
                cnt = tcvr_get_active_thread_count()
                if cnt > 1:
                    return True
                line = line.strip()
                # Skip comments and empty lines
                if not line or line.startswith('#'):
                    continue
                
                # Parse lines like "swp1=35"
                if '=' in line:
                    interface, temp = line.split('=')
                    interface = interface.strip()
                    temp_value = float(temp.strip())
                    is_intf_valid = tcvr_is_intf_name_valid(interface)
                    if not is_intf_valid:
                        continue
                    module_num = int(interface.split("swp")[1])
                    module_name = f"module{module_num}"
                    
                    tcvr_info = tcvr_get_instance_from_name(
                        new_tcvr_temp_cntrl_info, module_name
                    )
                    if not tcvr_info:
                        continue
                   
                    try: 
                        tcvr_eeprom_high_warn = tcvr_get_eeprom_high_warn(
                            module_num
                        )
                        syslog.syslog(syslog.LOG_INFO, f"{tcvr_info.interface} ethtool eeprom high warning {tcvr_eeprom_high_warn}")
                    except Exception as e:
                        tcvr_eeprom_high_warn = None
                        syslog.syslog(syslog.LOG_ERR, f"{tcvr_info.interface} error reading ethtool eeprom high warning: {e}")
                    
                    if tcvr_eeprom_high_warn:
                        offset = tcvr_eeprom_high_warn - temp_value
                        if offset < 0:
                            offset = tc_def_temp_control_offset
                            msg = (
                                f"{tcvr_info.interface} user requested setpoint "
                                f"{temp_value}°C is above optics advertised "
                                f"{tcvr_eeprom_high_warn}°C, using "
                                f"{tcvr_eeprom_high_warn}°C as setpoint "
                            )
                            if msg != tcvr_info.temp_setpint_high_warn:
                                syslog.syslog(syslog.LOG_WARNING, msg)
                                tcvr_info.temp_setpint_high_warn = msg

                        if offset > tc_max_temp_control_limit:
                            offset = tc_max_temp_control_limit
                            setpoint = tcvr_eeprom_high_warn - offset
                            msg = (
                                f"{tcvr_info.interface} user requested setpoint "
                                f"{temp_value}°C is too low to optics advertised: "
                                f"{tcvr_eeprom_high_warn}°C, using :"
                                f"{setpoint}°C as setpoint"
                            )
                            if msg != tcvr_info.temp_setpint_low_warn:
                                syslog.syslog(syslog.LOG_WARNING, msg)
                                tcvr_info.temp_setpint_low_warn = msg
                    
                        if offset:
                            offset = -offset
                        syslog.syslog(syslog.LOG_INFO, f"{tcvr_info.interface} calculated offset {offset}")
                        tcvr_info.tcvr_set_tc_max_offset(offset)
                        
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"error while processing configurtion file {tcvr_user_config_file}: {e}")
        return True
    return False


def tcvr_set_unset_active_thread_count(is_set=True):
    """
    Update the count of active configuration processing threads.
    Args:
        is_set: If True, increment count; if False, decrement count
    """
    global tcvr_thread_cfg_process_cnt
    tcvr__thread_process_req_cnt.acquire()

    if is_set:
        tcvr_thread_cfg_process_cnt = tcvr_thread_cfg_process_cnt + 1
    else:
        tcvr_thread_cfg_process_cnt = tcvr_thread_cfg_process_cnt - 1
        if tcvr_thread_cfg_process_cnt < 0:
            tcvr_thread_cfg_process_cnt = 0
            print("Bug if negative")
    cmd = f"Set active tcvr_process_configuration_change thread {tcvr_thread_cfg_process_cnt}"
    syslog.syslog(syslog.LOG_INFO, cmd)
    tcvr__thread_process_req_cnt.release()


def tcvr_get_active_thread_count():
    """
    Get the current count of active configuration processing threads.
    Returns:
        int: Current thread count
    """
    global tcvr_thread_cfg_process_cnt
    cnt = tcvr_thread_cfg_process_cnt 
    cmd = f"Get active tcvr_process_configuration_change thread {tcvr_thread_cfg_process_cnt}"
    syslog.syslog(syslog.LOG_INFO, cmd)
    return cnt


def tcvr_process_configuration_change():
    """
    Process configuration changes and update thermal control settings.
    Handles reading configuration, updating control info, and restarting services.
    """
    global tcvr_temp_cntrl_info
    tcvr_config_lock.acquire()
    try:
        if not os.path.exists(tcvr_user_config_file):
            syslog.syslog(syslog.LOG_INFO, f"Configuration file {tcvr_user_config_file} not found, skipping initial processing")
            return
        
        new_tcvr_temp_cntrl_info = tcvr_temp_cntrl_info.copy()
        
        for key, tcvr_info in new_tcvr_temp_cntrl_info.items():
            tcvr_info.tcvr_unset_tc_max_offset()

        syslog.syslog(syslog.LOG_INFO, f"Found {tcvr_user_config_file}, processing initial configuration...")
        do_skip = tcvr_parse_and_update_temp_cntrl_info(new_tcvr_temp_cntrl_info) 
        
        if do_skip:
            syslog.syslog(syslog.LOG_INFO, f"Skip processing as new thread is active")
            return
        
        syslog.syslog(syslog.LOG_INFO, f"Found {tcvr_user_config_file}, processing initial configuration success ...")
       
        # update global database
        tcvr_temp_cntrl_info = new_tcvr_temp_cntrl_info.copy()
        
        tcvr_check_config_diff_and_restart_tc()
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Error in process_initial_configuration: {e}")
    finally:
        is_set = False
        tcvr_set_unset_active_thread_count(is_set)
        tcvr_config_lock.release()


def tcvr_is_modified_file_valid(pathname):
    """
    Check if a modified file is relevant for TCVR monitoring.
    Args:
        pathname: Path of the modified file
    Returns:
        bool: True if file is valid for monitoring, False otherwise
    """
    if tcvr_config_change_monitor_file in pathname:
        return True

    for key, tcvr_info in tcvr_temp_cntrl_info.items():
        if pathname in tcvr_info.sysfs_present_monitor:
            return True
    return False


def tcvr_monitor_present_and_conf_change_and_restart_tc():
    """
    Monitor configured interfaces for changes and restart thermal control as needed.
    Sets up file system monitoring and handles configuration changes.
    """
    try:
        wm = pyinotify.WatchManager()
        present_file_mask = (
            pyinotify.IN_MODIFY | pyinotify.IN_CREATE | 
            pyinotify.IN_DELETE | pyinotify.IN_MOVED_TO
        )
        config_file_mask = (
            pyinotify.IN_MODIFY | pyinotify.IN_CREATE | 
            pyinotify.IN_DELETE | pyinotify.IN_MOVED_TO | 
            pyinotify.IN_DELETE_SELF | pyinotify.IN_IGNORED
        )

        for key, tcvr_info in tcvr_temp_cntrl_info.items():
            wm.add_watch(tcvr_info.sysfs_present_monitor, present_file_mask)
            syslog.syslog(syslog.LOG_INFO,
                f"Added watch for {tcvr_info.sysfs_present_monitor}"
            )

        wm.add_watch(tcvr_config_change_monitor_file, config_file_mask)
        syslog.syslog(syslog.LOG_INFO,
            f"Added watch for config file: {tcvr_config_change_monitor_file}"
        )
        
        common_handler = CommonEventHandler()
        notifier = pyinotify.Notifier(wm, common_handler)
        common_handler.set_notifier(notifier)
        
        notifier.loop()
        syslog.syslog(syslog.LOG_INFO, "notifier loop terminated")
        time.sleep(1)
    except KeyboardInterrupt:
        syslog.syslog(syslog.LOG_WARNING, "\nStopping monitoring...")
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Error in notifier loop: {e}")


# Main function
def main():
    """
    Main function to initialize and run the TCVR control service.
    Sets up logging, initializes configuration, and starts monitoring.
    """
    global tcvr_thread_cfg_process_cnt
    parser = argparse.ArgumentParser(description="Monitor and control TCVR")
    args = parser.parse_args()

    pidfile = "/var/run/tcvrcntl.pid"
    if already_running(pidfile):
        sys.exit()
    else:
        with open(pidfile, 'w') as f:
            f.write(str(os.getpid()))
            f.close()

    # Create the tcvrcntl reload file
    if not os.path.exists(ReloadDir):
        os.makedirs(ReloadDir)
    reload_file = open(ReloadFile, "w")
    reload_file.close()        

    syslog.syslog(syslog.LOG_INFO, "Monitoring and Controlling TCVR")
    # get dictionary details
    tcvr_init_temp_cntrl_info()
    tcvr_update_temp_cntrl_info()
    tcvr_create_monitor_file()
    
    tcvr_print_temp_cntrl_info()
   
    # Notify systemd that the service is ready
    try:
        cumulus.sdnotify.sd_notify(0, bytes('READY=1', 'ASCII'))
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Failed to notify systemd: {e}")
    
    # Start a separate thread to process initial configuration
    syslog.syslog(syslog.LOG_INFO, "Starting initial configuration processing in separate thread")
    is_set = True
    tcvr_set_unset_active_thread_count(is_set)
    
    config_thread = threading.Thread(
        target=tcvr_process_configuration_change,
        daemon=True,
        name=tcvr_thread_name_user_cfg_change
    )
    config_thread.start()

    while True:
        try:
            tcvr_monitor_present_and_conf_change_and_restart_tc()
        except Exception as e:
            syslog.syslog(syslog.LOG_ERR,
                f"Error in entering monitor_configured_interfaces loop: {e}"
            )
            time.sleep(1)  # Sleep before retrying


# execution check
if __name__ == "__main__":
    try:
        signal.signal(signal.SIGTERM, exit_normally)
        # Cause all warnings to always be triggered.
        warnings.simplefilter("always")
        warnings.formatwarning = tcvrcntlwarn
        syslog.openlog(": %s : " % sys.argv[0])
        syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO))
        exit(main())
    except TCVRCNTLRuntimeError as errstr:
        syslog.syslog(syslog.LOG_ERR, "ERROR : %s" % str(errstr))
        sys.stderr.write("%s : ERROR : %s\n" % (sys.argv[0], str(errstr)))
        exit(1)
    except KeyboardInterrupt:
        exit_normally()
    except Exception:
        (exc_type, exc_value, exc_traceback) = sys.exc_info()
        err = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
        log = 'Unhandled Exception : %s' % err
        syslog.syslog(syslog.LOG_ERR, log)
        sys.stderr.write(log)
