"""
This is the logging module 2.0, personalized to meet the logging needs of the preprocessing project
"""
import datetime
import os
import inspect
import threading
import traceback

class Logger:
    def __init__(self,module,log_path=None,switch = True, type='use'):
        self.module = module
        self.type = type
        self.switch = switch
        # If log_path is not provided, use default path in user's home directory
        if log_path is None:
            # Create .myffe directory in user's home directory
            home_dir = os.path.expanduser("~")
            logs_dir = os.path.join(home_dir, ".myffe", "logs")
            if not os.path.exists(logs_dir):
                os.makedirs(logs_dir, exist_ok=True)
            log_path = os.path.join(logs_dir, f"{module}.log")
        else:
            # Ensure parent directory exists
            parent_dir = os.path.dirname(log_path)
            if parent_dir and not os.path.exists(parent_dir):
                os.makedirs(parent_dir, exist_ok=True)
        self.log_path = log_path
        self.lock = threading.Lock()# Create a lock to prevent multiple threads from writing to the log file simultaneously, which could corrupt the log file
        if not hasattr(Logger,'_initialized'):# Check if the class attribute exists, if not create it
            self.write_start_separator() # Write start separator
            import atexit
            atexit.register(self.write_end_separator)# Register exit function to write end separator when the program exits
            Logger._initialized = True# Create class attribute, which applies to all instances of the class, ensuring only one separator per file, and resetting each time the file is executed
    def write_start_separator(self):# Method to write separator in log file
        if self.type is None:
            return
        file_path = os.path.join(self.log_path)
        with open(file_path, 'a', encoding='utf-8') as f:
            if self.type == 'use':
                pass
            elif self.type == 'test':
                f.write( '\n'+'-'*10+' Test Start '+ '-'*10 + '\n')
            else:
                print('Your log type is set incorrectly. For future extensibility, please set it to use, test, or None\n')
                pass
            call_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            f.write(' ='*50 + '\n')
            f.write(f'                                         Log Start, Time: {call_time}\n')

    def write_end_separator(self):# Method to write end separator in log file
        if self.type is None:
            return
        file_path = os.path.join(self.log_path)
        with open(file_path, 'a', encoding='utf-8') as f:
            call_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            f.write(f'                                         Log End, Time: {call_time}\n')
            f.write(' =' * 50 + '\n')
            if self.type == 'use':
                pass
            elif self.type == 'test':
                f.write( '\n'+'-'*10+' Test End '+ '-'*10 + '\n')
            else:
                print('Your log type is set incorrectly. For future extensibility, please set it to use, test, or None\n')
                pass
    def write_log(self, log_content):
        # Regardless of switch status, as long as type is not None, write to log file
        if self.type is not None:
            with self.lock:
                file_path = os.path.join(self.log_path)
                with open(file_path, 'a', encoding='utf-8') as f:
                    f.write(log_content + '\n\n')
        else:
            pass
    def log_method_call_test(self,func):
        def wrapper(*args, **kwargs): # *args receives any positional arguments, **kwargs receives any keyword arguments
            if self.type is None:
                return func(*args, **kwargs)
            
            method_name = func.__name__# Get function name
            call_time_obj = datetime.datetime.now()# Get current time
            call_time = call_time_obj.strftime('%Y-%m-%d %H:%M:%S')# Get current time
            # The strftime method formats time, with the format string '%Y-%m-%d %H:%M:%S' meaning year-month-day hour:minute:second
            self.start_time = f'{call_time}[INFO]Method [{method_name}] Start Call | Positional Args:{args} | Keyword Args:{kwargs}'
            # Write log (regardless of switch status)
            self.write_log(self.start_time)# Call write log method
            try:
                result = func(*args,**kwargs)# Return function call result
                cost_time = round((datetime.datetime.now() - call_time_obj).total_seconds(), 4)
                # The round method rounds to 4 decimal places
                success = f'{call_time}[INFO]Method [{method_name}] Call Success | Duration:{cost_time}s | Return Value:{result}'
                # Write log (regardless of switch status)
                self.write_log(success)# Call write log method
                return result# Return function call result
            except Exception as e:
                cost_time = round((datetime.datetime.now() - call_time_obj).total_seconds(), 2)
                error_type = type(e).__name__
                error_msg =str(e)
                error_stack = traceback.format_exc()[:500]# Get error stack information and truncate to first 500 characters to prevent log file from getting too large
                fail_log = f'{call_time}[ERROR]Method [{method_name}] Call Failed | Duration:{cost_time}s | Error Type:{error_type} | Error Message:{error_msg}\n{error_stack}'
                # Write log (regardless of switch status)
                self.write_log(fail_log)# Call write log method
                raise e
                # Raise exception
        return wrapper
    def log_method_call_use(self,func):
        def wrapper(*args, **kwargs): # *args receives any positional arguments, **kwargs receives any keyword arguments
            if self.type is None:
                return func(*args, **kwargs)
            
            method_name = func.__name__# Get function name
            call_time_obj = datetime.datetime.now()# Get current time
            call_time = call_time_obj.strftime('%Y-%m-%d %H:%M:%S')# Get current time
            
            # Extract preprocessing tool name
            tool_name = args[0].__class__.__name__ if args else "UnknownTool"
            
            # Cool start message (only displayed when switch is True)
            if self.switch:
                start_msg = f"\n{'='*70}"
                start_msg += f"\n[{call_time}] 🚀 Start Executing: {tool_name}.{method_name}"
                start_msg += f"\n{'='*70}"
                start_msg += f"\n🎯 Tool: {tool_name}"
                start_msg += f"\n🔧 Method: {method_name}"
                start_msg += f"\n📅 Time: {call_time}"
                if kwargs:
                    start_msg += f"\n⚙️  Parameters: {kwargs}"
                start_msg += f"\n{'='*70}"
                print(start_msg)
            
            # Write log (regardless of switch status)
            self.write_log(f"{call_time}[INFO]Start Executing {tool_name}.{method_name}")
            
            try:
                # Execute function
                result = func(*args,**kwargs)
                
                # Calculate execution time
                cost_time = round((datetime.datetime.now() - call_time_obj).total_seconds(), 4)
                
                # Cool completion message (only displayed when switch is True)
                if self.switch:
                    finish_msg = f"\n{'='*70}"
                    finish_msg += f"\n[{call_time}] ✅ Execution Complete: {tool_name}.{method_name}"
                    finish_msg += f"\n{'='*70}"
                    finish_msg += f"\n🎉 Status: Successfully Completed"
                    finish_msg += f"\n⏱️  Duration: {cost_time}s"
                    finish_msg += f"\n📊 Performance: {'Excellent' if cost_time < 0.1 else 'Good' if cost_time < 1 else 'Average'}"
                    finish_msg += f"\n{'='*70}"
                    print(finish_msg)
                
                # Write log (regardless of switch status)
                self.write_log(f"{call_time}[INFO]Execution Complete {tool_name}.{method_name} | Duration: {cost_time}s | Status: Success")
                
                return result# Return function call result
            except Exception as e:
                cost_time = round((datetime.datetime.now() - call_time_obj).total_seconds(), 2)
                error_type = type(e).__name__
                error_msg =str(e)
                error_stack = traceback.format_exc()[:500]# Get error stack information and truncate to first 500 characters to prevent log file from getting too large
                
                # Cool error message (only displayed when switch is True)
                if self.switch:
                    error_msg_display = f"\n{'='*70}"
                    error_msg_display += f"\n[{call_time}] ❌ Execution Failed: {tool_name}.{method_name}"
                    error_msg_display += f"\n{'='*70}"
                    error_msg_display += f"\n💥 Status: Execution Failed"
                    error_msg_display += f"\n⏱️  Duration: {cost_time}s"
                    error_msg_display += f"\n🔍 Error Type: {error_type}"
                    error_msg_display += f"\n💬 Error Message: {error_msg}"
                    error_msg_display += f"\n🔧 Suggestion: Check parameter configuration and data format"
                    error_msg_display += f"\n{'='*70}"
                    print(error_msg_display)
                
                # Write log (regardless of switch status)
                fail_log = f'{call_time}[ERROR]Execution Failed {tool_name}.{method_name} | Duration:{cost_time}s | Error Type:{error_type} | Error Message:{error_msg}\n{error_stack}'
                self.write_log(fail_log)# Call write log method
                raise e
                # Raise exception
        return wrapper

def log_class(type = 'use'):
    def decorator(cls):
        for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
            # Get class method names and methods through inspect.isfunction method, then pass the method as a parameter to log_method_call method
            # This method is a function object with the same code as the method, but without the self parameter bound to the class        
            if not name.startswith('__'):
                # Check if the method name starts with __, if so it's a special class method that doesn't need logging
                if type == 'use':
                    setattr(cls, name, logger.log_method_call_use(method))# The setattr method is used to set class attributes, here replacing class methods with logging methods
                elif type == 'test':
                    setattr(cls, name, logger.log_method_call_test(method))# The setattr method is used to set class attributes, here replacing class methods with logging methods
                # If type is None, do not add logging decorator
        return cls
    return decorator
# This section configures all methods of the decorated class as logging methods, where name is the class method name and method is the class method


# Create logger instance
logger = Logger(module="preprocessing", switch=True, type=None)

# Global logging control functions
def set_logging_enabled(enabled):
    """
    Set global console output switch status
    
    Args:
        enabled (bool): Whether to display logs in the console
    """
    global logger
    logger.switch = enabled
    print(f"\033[34mConsole log output has been set to: {'Enabled' if enabled else 'Disabled'}\033[0m")

def set_logging_mode(mode):
    """
    Set global logging mode
    
    Args:
        mode (str): Logging mode, optional values: 'None', 'use', 'test'
    """
    global logger      
    # The purpose of setting the global keyword is to allow the local scope inside a function 
    # to modify the instance defined in the outer scope.
    valid_modes = ['None', 'use', 'test']
    if mode not in valid_modes:
        print(f"\033[31mInvalid logging mode: {mode}, please use one of the following modes: {valid_modes}\033[0m")
        return
    
    logger.type = None if mode == 'None' else mode
    print(f"\033[34mLogging mode has been set to: {mode}\033[0m")

def get_logging_status():
    """
    Get current logging status
    
    Returns:
        dict: Dictionary containing logging mode and console output status
    """
    global logger
    return {
        'mode': logger.type,
        'console_enabled': logger.switch,
        'log_path': logger.log_path
    }

def set_logging_path(log_path):
    """
    Set custom logging path
    
    Args:
        log_path (str): Custom log file path
    """
    global logger
    # Ensure parent directory exists
    parent_dir = os.path.dirname(log_path)
    if parent_dir and not os.path.exists(parent_dir):
        os.makedirs(parent_dir, exist_ok=True)
    logger.log_path = log_path
    print(f"\033[34mLog path has been set to: {log_path}\033[0m")


# Main program example
if __name__ == "__main__":
    """
    Logging system usage example
    Demonstrates how to pass parameters to the logging system and control its behavior
    """
    print("=" * 80)
    print("FFE 3.0 Logging System Usage Example")
    print("=" * 80)
    
    # Example 1: Using default settings (None mode, no logging)
    print("\nExample 1: Using default settings (None mode, no logging)")
    print("-" * 80)
    print(f"Current log type: {logger.type}")
    print(f"Current console output: {logger.switch}")
    
    # Define a test function
    def test_function(param1, param2):
        """Test function"""
        print(f"Executing test function, parameters: {param1}, {param2}")
        return f"Result: {param1 + param2}"
    
    # Apply logging decorator
    @logger.log_method_call_use
    def decorated_test_function(param1, param2):
        """Test function with logging decorator"""
        print(f"Executing decorated test function, parameters: {param1}, {param2}")
        return f"Result: {param1 + param2}"
    
    # Call function (default None mode, no log output)
    result = decorated_test_function(10, 20)
    print(f"Function return value: {result}")
    
    # Example 2: Switch to use mode and enable console output
    print("\nExample 2: Switch to use mode and enable console output")
    print("-" * 80)
    logger.type = 'use'
    logger.switch = True
    print(f"Current log type: {logger.type}")
    print(f"Current console output: {logger.switch}")
    
    # Call function (use mode, with console output)
    result = decorated_test_function(30, 40)
    print(f"Function return value: {result}")
    
    # Example 3: Disable console output but keep log writing
    print("\nExample 3: Disable console output but keep log writing")
    print("-" * 80)
    logger.switch = False
    print(f"Current log type: {logger.type}")
    print(f"Current console output: {logger.switch}")
    
    # Call function (no console output, but still writes to log file)
    result = decorated_test_function(50, 60)
    print(f"Function return value: {result}")
    
    # Example 4: Switch to test mode
    print("\nExample 4: Switch to test mode")
    print("-" * 80)
    logger.type = 'test'
    logger.switch = True
    print(f"Current log type: {logger.type}")
    print(f"Current console output: {logger.switch}")
    
    # Apply test mode decorator
    @logger.log_method_call_test
    def test_mode_function(param1, param2):
        """Test function in test mode"""
        print(f"Executing test mode test function, parameters: {param1}, {param2}")
        return f"Result: {param1 * param2}"
    
    # Call function (test mode, detailed log output)
    result = test_mode_function(7, 8)
    print(f"Function return value: {result}")
    
    # Example 5: Use set_logging_enabled function to control console output
    print("\nExample 5: Use set_logging_enabled function to control console output")
    print("-" * 80)
    set_logging_enabled(False)
    print(f"Current console output: {logger.switch}")
    
    # Call function (no console output)
    result = decorated_test_function(90, 100)
    print(f"Function return value: {result}")
    
    # Restore console output
    set_logging_enabled(True)
    
    # Example 6: Set custom logging path
    print("\nExample 6: Set custom logging path")
    print("-" * 80)
    custom_path = os.path.join(os.getcwd(), "logs", "custom_preprocessing.log")
    set_logging_path(custom_path)
    status = get_logging_status()
    print(f"Current log path: {status['log_path']}")
    
    # Call function with custom log path
    logger.type = 'use'
    result = decorated_test_function(110, 120)
    print(f"Function return value: {result}")
    
    print("\n" + "=" * 80)
    print("Logging system usage example completed")
    print("=" * 80)
    print("\nTips:")
    print("1. Use logger.type to set logging mode (None/use/test)")
    print("2. Use logger.switch to control console output (True/False)")
    print("3. Use set_logging_enabled() function to quickly control console output")
    print("4. Use set_logging_path() function to set custom log file path")
    print("5. Use get_logging_status() function to check current logging status")
    print("6. By default, logs are saved in '~/.myffe/logs/{module}.log' in user's home directory")
    print("7. Regardless of whether console output is enabled, logs will be written to the file (when type is not None)")
