import threading
try:
    from pynput import keyboard
    from pynput import mouse
except Exception:
    keyboard = None
    mouse = None
import time,math
import json
import platform
import ctypes
from ws_package.configs import config
from ws_package.utils.system import IdleDuration
from ws_package.log.writer import LogWriter
from ws_package.entities.activity_entity import ActivityEntity
from ws_package.services.activity_percentage_service import activity_percentage_service
import datetime
from ws_package.monitoring.cgevent_listener import MacCGEventListener
logger = LogWriter.file('DeviceListener')

TIME_FORMAT = '%Y-%m-%d %H:%M:%S'

# Constants for activity tracking
ACTIVITY_ARRAY_SIZE = 999  # Tracks every second of activity (max interval size)
MAX_EVENTS_LIMIT = 1000  # Limit event list size for memory management
KEY_REPEAT_THRESHOLD_SECONDS = 0.1  # 100ms - ignore key repeats faster than this

# Default interval (10 minutes)
DEFAULT_INTERVAL_SECONDS = 600

# Timing constants
LISTENER_INITIALIZATION_WAIT_SECONDS = 0.5  # Wait time for listeners to initialize
BRIEF_WAIT_SECONDS = 0.1  # Brief wait time for listener checks
HEALTH_CHECK_INTERVAL_SECONDS = 30  # Health check interval
THREAD_STOP_WAIT_SECONDS = 2  # Wait time for threads to stop
VERIFICATION_TIMEOUT_SECONDS = 20  # Base verification timeout
VERIFICATION_TIMEOUT_MULTIPLIER = 2  # Multiplier for extended timeout
VERIFICATION_START_THRESHOLD_SECONDS = 5  # Threshold for extending verification timeout
CHECK_INTERVAL_SECONDS = 5  # Check interval for listener status

# Exponential backoff constants
BACKOFF_BASE_SECONDS = 30  # Base backoff time
BACKOFF_MULTIPLIER = 2  # Exponential multiplier
BACKOFF_MAX_SECONDS = 300  # Maximum backoff time (5 minutes)
RESTART_COOLDOWN_SECONDS = 300  # Cooldown between restarts (5 minutes)
LISTENER_STOPPED_COOLDOWN_SECONDS = 60  # Cooldown when listener.running is False but thread is alive (1 minute)

# Retry constants
MAX_RETRIES = 5  # Maximum retry attempts
RETRY_WAIT_MULTIPLIER = 5  # Wait time multiplier for retries
MAX_RETRY_WAIT_SECONDS = 30  # Maximum wait time between retries

# Health check constants
HEALTH_CHECK_LOG_INTERVAL = 10  # Log health status every N checks
PERIODIC_CLEANUP_INTERVAL_SECONDS = 1800  # 30 minutes - periodic cleanup check interval
PERIODIC_CLEANUP_INACTIVITY_THRESHOLD = 1800  # 30 minutes - no events threshold for safety cleanup

# macOS permission handling
PERMISSION_CACHE_TTL_SECONDS = 300  # Cache permission result for 5 minutes
APPLICATION_SERVICES_PATH = "/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices"

# Mouse movement thresholds
MOUSE_VERIFICATION_THRESHOLD_PIXELS = 1  # Minimum pixels for verification (any movement)
MOUSE_ACTIVITY_THRESHOLD_PIXELS = 5  # Minimum pixels for activity counting
MOUSE_VERIFICATION_THRESHOLD_SQUARED = MOUSE_VERIFICATION_THRESHOLD_PIXELS * MOUSE_VERIFICATION_THRESHOLD_PIXELS  # 1² = 1
MOUSE_ACTIVITY_THRESHOLD_SQUARED = MOUSE_ACTIVITY_THRESHOLD_PIXELS * MOUSE_ACTIVITY_THRESHOLD_PIXELS  # 5² = 25
# CRITICAL: Balanced throttle to reduce WindowServer load while maintaining responsiveness
# CGEventTap intercepts ALL events at system level, so reducing handler frequency reduces WindowServer processing
# Increased to 0.3s to reduce WindowServer load (max 3.3 events/second instead of 5)
MOUSE_MOVE_THROTTLE_SECONDS = 0.3  # Throttle mouse events to max 3.3 per second (300ms) to reduce WindowServer load
# Minimum movement threshold to ignore micro-movements and reduce WindowServer processing
# Increased to 3px to filter out more noise and reduce WindowServer processing significantly
MOUSE_MIN_MOVE_PIXELS = 3  # Ignore movements smaller than 3 pixels (reduces noise and WindowServer load)
MOUSE_MIN_MOVE_THRESHOLD_SQUARED = MOUSE_MIN_MOVE_PIXELS * MOUSE_MIN_MOVE_PIXELS  # 3² = 9

# Activity calculation constants
PERCENTAGE_MULTIPLIER = 100  # Multiplier for percentage calculations
SECONDS_PER_DAY = 86400  # Seconds in a day (for daily fudging report)
MINUTES_PER_HOUR = 60  # Minutes per hour

class DeviceListener: 

    keyboard_weight: float = 0.6  # Default value, will be set in __init__
    mouse_weight: float = 0.4  # Default value, will be set in __init__
    idle_threshold_seconds: int = 180  # Default value, will be set in __init__

    def __init__(self, interval=DEFAULT_INTERVAL_SECONDS):
        # Ensure keyboard_weight and mouse_weight are floats (config may return string or None)
        try:
            keyboard_weight_config = config('keyboard_weight', 'ActivityMonitor', dtype='float')
            self.keyboard_weight = float(keyboard_weight_config) if keyboard_weight_config is not None else 0.6
        except (ValueError, TypeError):
            self.keyboard_weight = 0.6
        
        try:
            mouse_weight_config = config('mouse_weight', 'ActivityMonitor', dtype='float')
            self.mouse_weight = float(mouse_weight_config) if mouse_weight_config is not None else 0.4
        except (ValueError, TypeError):
            self.mouse_weight = 0.4
        
        try:
            idle_threshold_config = config('idle_threshold_seconds', 'ActivityMonitor', dtype='int')
            self.idle_threshold_seconds = int(idle_threshold_config) if idle_threshold_config is not None else 180
        except (ValueError, TypeError):
            self.idle_threshold_seconds = 180
        self.keyboard_events = []
        self.mouse_events = []
        self.size = interval
        logger.info("Linux/MAC Listener initialized : %s self.size is %s", interval, self.size)
        # Keeping previous variables for backward compatibility
        self.keyboard_count = 0
        self.mouse_move_count = 0
        self.mouse_click_count = 0

        # New arrays for activity tracking
        self.keyboard_activity_array = [False] * ACTIVITY_ARRAY_SIZE
        self.mouse_activity_array = [False] * ACTIVITY_ARRAY_SIZE
        self.scroll_count = 0

        self.prev_mouse_x, self.prev_mouse_y = 0, 0  # Initial mouse positions
        
        # Mouse movement throttling (to prevent system lag on macOS)
        self._last_mouse_process_time = 0
        
        # Memory management
        self.max_events = MAX_EVENTS_LIMIT
        self._cleanup_counter = 0
        
        # Key repeat detection variables
        self.last_key_press_time = 0
        self.last_key = None
        self.key_repeat_threshold = KEY_REPEAT_THRESHOLD_SECONDS
        self.pressed_keys = set()  # Track currently pressed keys
        
        # Health monitoring variables
        self.mouse_listener_active = False
        self.keyboard_listener_active = False
        self.mouse_listener_verified = False
        self.keyboard_listener_verified = False
        self.last_mouse_activity = time.time()
        self.last_keyboard_activity = time.time()
        self.mouse_events_received = 0
        self.keyboard_events_received = 0
        self.listener_start_time = time.time()
        self.verification_timeout = VERIFICATION_TIMEOUT_SECONDS
        self.adaptive_timeout = True  # Enable adaptive timeout
        self.stop_listeners = False
        self.health_monitor_thread = None
        self.mouse_listener = None
        self.keyboard_listener = None
        
        # Permission and backoff tracking (macOS-specific)
        self.permissions_issue_detected = False
        self.listeners_paused_due_to_permissions = False
        self._cached_permission_result = (True, True)
        self._last_permission_check = 0.0
        self.consecutive_failures = 0
        self.last_restart_time = 0
        self.mouse_listener_stopped_time = 0  # Track when mouse listener stopped (listener.running=False)
        self.keyboard_listener_stopped_time = 0  # Track when keyboard listener stopped (listener.running=False)
        self._sleeping = False  # Track if system is sleeping (to prevent health checks and free event taps)
        self._last_periodic_cleanup_time = time.time()  # Track last periodic cleanup check
        
        # Hardware event validation (for filtering synthetic events)
        self.hardware_validator = None
        self.synthetic_events_filtered = 0
        try:
            from ws_package.utils.hardware_event_validator import get_hardware_validator
            self.hardware_validator = get_hardware_validator()
        except Exception as e:
            logger.debug("Hardware event validation not available: %s", e)
        
        # Fudging detection (for detecting data fudging patterns)
        self.fudging_detector = None
        self.fudging_report_service = None
        self._last_fudging_report_time = 0
        self._report_thread = None  # Track report thread to prevent accumulation
        try:
            from ws_package.utils.fudging_detector import get_fudging_detector
            from ws_package.services.fudging_report_service import get_fudging_report_service
            self.fudging_detector = get_fudging_detector()
            self.fudging_report_service = get_fudging_report_service()
        except Exception as e:
            logger.debug("Fudging detection not available: %s", e)

        # Start listeners during initialization
        self.start_listeners()
        # self.start_health_monitor()# for make it fast 

    def _should_count_hardware_activity(self, event_checker, input_kind: str) -> bool:
        """Return True when a pynput event should count as real user activity."""
        if not self.hardware_validator or not self.hardware_validator.is_enabled():
            return True
        try:
            return self.hardware_validator.should_count_activity(
                event_checker(),
                input_kind=input_kind,
            )
        except Exception as e:
            logger.debug("Hardware validation error (accepting event): %s", e)
            return True

    def start_listeners(self):
        """Start keyboard and mouse listeners with error handling and permission checking"""
        try:
            # Check permissions first (macOS only - best practice)
            # NOTE: We check permissions but don't block listener startup if check fails
            # This allows pynput to attempt starting and trigger permission dialogs if needed
            if platform.system() == "Darwin":
                mouse_perm, keyboard_perm = self.check_macos_permissions()
                
                if not mouse_perm or not keyboard_perm:
                    logger.warning("[PERMISSIONS] macOS permissions check indicates permissions may not be granted")
                    logger.warning("[PERMISSIONS] Mouse: %s, Keyboard: %s",
                               '✓' if mouse_perm else '✗',
                               '✓' if keyboard_perm else '✗')
                    logger.warning("[PERMISSIONS] Attempting to start listeners anyway - pynput will trigger permission dialogs if needed")
                    logger.warning("[PERMISSIONS] If listeners fail, please grant permissions in:")
                    logger.warning("[PERMISSIONS] System Settings > Privacy & Security > Input Monitoring")
                    
                    # Set flag but don't block startup - let pynput try to start
                    # This is important because permission checks can be unreliable, and pynput
                    # will show the permission dialog if needed
                    self.permissions_issue_detected = True
                    self.listeners_paused_due_to_permissions = False  # Don't pause - let it try
                else:
                    self.permissions_issue_detected = False
                    self.listeners_paused_due_to_permissions = False
                    self.consecutive_failures = 0  # Reset on success
            
            # CRITICAL: Stop existing threads and listeners before creating new ones to prevent thread accumulation
            # This prevents CPU rise from multiple threads running simultaneously
            if hasattr(self, 'mouse_thread') and self.mouse_thread and self.mouse_thread.is_alive():
                logger.debug("[DEBUG] Stopping existing mouse thread and listener before starting new one")
                try:
                    # Stop the pynput listener first
                    if hasattr(self, 'mouse_listener') and self.mouse_listener:
                        try:
                            self.mouse_listener.stop()
                        except Exception:
                            pass
                        self.mouse_listener = None
                    # Set stop flag to signal thread to exit
                    self.stop_listeners = True
                    # Wait for thread to finish (with timeout to prevent blocking)
                    self.mouse_thread.join(timeout=2)
                    if self.mouse_thread.is_alive():
                        logger.warning("[DEBUG] Mouse thread did not stop within timeout, continuing anyway")
                except Exception as e:
                    logger.debug("[DEBUG] Error stopping existing mouse thread: %s", e)
                finally:
                    self.stop_listeners = False
            
            if hasattr(self, 'keyboard_thread') and self.keyboard_thread and self.keyboard_thread.is_alive():
                logger.debug("[DEBUG] Stopping existing keyboard thread and listener before starting new one")
                try:
                    # Stop the pynput listener first
                    if hasattr(self, 'keyboard_listener') and self.keyboard_listener:
                        try:
                            self.keyboard_listener.stop()
                        except Exception:
                            pass
                        self.keyboard_listener = None
                    # Set stop flag to signal thread to exit
                    self.stop_listeners = True
                    # Wait for thread to finish (with timeout to prevent blocking)
                    self.keyboard_thread.join(timeout=2)
                    if self.keyboard_thread.is_alive():
                        logger.warning("[DEBUG] Keyboard thread did not stop within timeout, continuing anyway")
                except Exception as e:
                    logger.debug("[DEBUG] Error stopping existing keyboard thread: %s", e)
                finally:
                    self.stop_listeners = False
            
            # Reset verification flags
            self.mouse_listener_verified = False
            self.keyboard_listener_verified = False
            self.mouse_events_received = 0
            self.keyboard_events_received = 0
            self.listener_start_time = time.time()
            
            self.mouse_thread = threading.Thread(target=self.start_mouse_listener, daemon=True, name="MouseListener")
            self.keyboard_thread = threading.Thread(target=self.start_keyboard_listener, daemon=True, name="KeyboardListener")
            
            self.mouse_thread.start()
            self.keyboard_thread.start()
            
            logger.info("Mouse and keyboard listener threads started")
            
            # Wait a moment for listeners to initialize
            time.sleep(LISTENER_INITIALIZATION_WAIT_SECONDS)
            
            # Verify listeners are actually receiving events
            # self.verify_listeners_are_working()
            
        except Exception as e:
            logger.error("Failed to start listeners: %s", e, exc_info=True)
            self.restart_listeners()
    
    def start_health_monitor(self):
        """Start health monitoring thread"""
        try:
            # CRITICAL: Stop existing health monitor thread before creating new one
            # This prevents thread accumulation and CPU rise
            if hasattr(self, 'health_monitor_thread') and self.health_monitor_thread and self.health_monitor_thread.is_alive():
                logger.debug("[DEBUG] Stopping existing health monitor thread before starting new one")
                try:
                    # Health monitor checks self.stop_listeners, so set it to signal exit
                    self.stop_listeners = True
                    # Wait for thread to finish (with timeout to prevent blocking)
                    self.health_monitor_thread.join(timeout=2)
                    if self.health_monitor_thread.is_alive():
                        logger.warning("[DEBUG] Health monitor thread did not stop within timeout, continuing anyway")
                except Exception as e:
                    logger.debug("[DEBUG] Error stopping existing health monitor thread: %s", e)
                finally:
                    self.stop_listeners = False
            
            self.health_monitor_thread = threading.Thread(target=self.health_monitor_loop, daemon=True, name="HealthMonitor")
            self.health_monitor_thread.start()
            logger.info("Health monitor started")
        except Exception as e:
            logger.error("Failed to start health monitor: %s", e, exc_info=True)
    
    def health_monitor_loop(self):
        """Monitor listener health with exponential backoff for permission issues"""
        health_check_counter = 0
        max_inactivity_threshold = 60  # Restart if no activity for 60 seconds
        
        while not self.stop_listeners:
            try:
                # Check if system is actually sleeping (not just flagged as sleeping)
                if getattr(self, '_sleeping', False):
                    # CRITICAL FIX: Verify system is actually sleeping before skipping health checks
                    # If system is responsive (can query frontmost app), it's awake and listeners should restart
                    system_actually_sleeping = self._verify_system_is_sleeping()
                    if not system_actually_sleeping:
                        logger.info("[HEALTH] System marked as sleeping but is actually awake - resetting sleep flag and restarting listeners")
                        self._sleeping = False
                        # Restart listeners since system is awake
                        try:
                            self.stop_listeners = False
                            self.start_listeners()
                            logger.info("[HEALTH] Listeners restarted after detecting system is awake")
                        except Exception as restart_error:
                            logger.error("[HEALTH] Error restarting listeners after wake detection: %s", restart_error)
                    else:
                        # System is actually sleeping, skip health checks
                        time.sleep(HEALTH_CHECK_INTERVAL_SECONDS)
                        continue
                
                current_time = time.time()
                health_check_counter += 1
                
                # Exponential backoff for permission issues (macOS best practice)
                if getattr(self, 'permissions_issue_detected', False):
                    self.consecutive_failures += 1
                    # Exponential backoff: 30s, 60s, 120s, 300s (max 5 min)
                    backoff_time = min(BACKOFF_BASE_SECONDS * (BACKOFF_MULTIPLIER ** (self.consecutive_failures - 1)), BACKOFF_MAX_SECONDS)
                    logger.warning("[PERMISSIONS] Using exponential backoff: checking again in %ss (failure count: %s)",
                                 backoff_time, self.consecutive_failures)
                    time.sleep(backoff_time)
                    # Re-check permissions after backoff
                    if platform.system() == "Darwin":
                        mouse_perm, keyboard_perm = self.check_macos_permissions()
                        if mouse_perm and keyboard_perm:
                            logger.info("[PERMISSIONS] Permissions now granted! Resetting backoff.")
                            self.permissions_issue_detected = False
                            self.listeners_paused_due_to_permissions = False
                            self.consecutive_failures = 0
                            self.restart_listeners()
                    continue
                
                # Check if threads are alive
                mouse_alive = self.mouse_thread and self.mouse_thread.is_alive()
                keyboard_alive = self.keyboard_thread and self.keyboard_thread.is_alive()
                
                # Check if listeners stopped but threads are still alive (listener.running=False)
                mouse_stopped_but_alive = (mouse_alive and not self.mouse_listener_active and 
                                          self.mouse_listener_stopped_time > 0)
                keyboard_stopped_but_alive = (keyboard_alive and not self.keyboard_listener_active and 
                                            self.keyboard_listener_stopped_time > 0)
                
                # Check if cooldown period has passed for stopped listeners
                mouse_cooldown_passed = (mouse_stopped_but_alive and 
                                        (current_time - self.mouse_listener_stopped_time) >= LISTENER_STOPPED_COOLDOWN_SECONDS)
                keyboard_cooldown_passed = (keyboard_stopped_but_alive and 
                                           (current_time - self.keyboard_listener_stopped_time) >= LISTENER_STOPPED_COOLDOWN_SECONDS)
                
                # Check for activity timeout
                mouse_inactive = (current_time - self.last_mouse_activity) > max_inactivity_threshold
                keyboard_inactive = (current_time - self.last_keyboard_activity) > max_inactivity_threshold
                
                # Check if listeners are verified
                mouse_verified = self.mouse_listener_verified
                keyboard_verified = self.keyboard_listener_verified
                
                # Log health status (reduced frequency) - use debug level to reduce I/O overhead
                if health_check_counter % HEALTH_CHECK_LOG_INTERVAL == 0:
                    logger.debug(
                        "[DEBUG] Health Check - Mouse: %s (%s), Keyboard: %s (%s), "
                        "Mouse Inactive: %s, Keyboard Inactive: %s, "
                        "Events: Mouse=%s, Keyboard=%s",
                        'Active' if mouse_alive else 'Dead',
                        'Verified' if mouse_verified else 'Not Verified',
                        'Active' if keyboard_alive else 'Dead',
                        'Verified' if keyboard_verified else 'Not Verified',
                        mouse_inactive, keyboard_inactive,
                        self.mouse_events_received, self.keyboard_events_received)
                
                # Restart if needed with exponential backoff for general failures
                # Priority 1: Thread is dead (immediate restart)
                if not mouse_alive or not keyboard_alive:
                    self.consecutive_failures += 1
                    # Exponential backoff: 30s, 60s, 120s, 300s (max 5 min)
                    backoff_time = min(BACKOFF_BASE_SECONDS * (BACKOFF_MULTIPLIER ** (self.consecutive_failures - 1)), BACKOFF_MAX_SECONDS)
                    logger.warning("[DEBUG] Detected listener thread issues. Using exponential backoff: %ss (failure count: %s)",
                                 backoff_time, self.consecutive_failures)
                    time.sleep(backoff_time)
                    self.restart_listeners()
                    self.last_restart_time = current_time
                # Priority 2: Listener stopped but thread is alive (restart only after cooldown)
                elif (mouse_stopped_but_alive and mouse_cooldown_passed) or (keyboard_stopped_but_alive and keyboard_cooldown_passed):
                    # Reset stopped time to prevent immediate restart
                    if mouse_stopped_but_alive:
                        self.mouse_listener_stopped_time = 0
                    if keyboard_stopped_but_alive:
                        self.keyboard_listener_stopped_time = 0
                    
                    self.consecutive_failures += 1
                    backoff_time = min(BACKOFF_BASE_SECONDS * (BACKOFF_MULTIPLIER ** (self.consecutive_failures - 1)), BACKOFF_MAX_SECONDS)
                    logger.warning("[DEBUG] Listener stopped but thread alive - cooldown passed. Restarting with backoff: %ss (failure count: %s)",
                                 backoff_time, self.consecutive_failures)
                    time.sleep(backoff_time)
                    self.restart_listeners()
                    self.last_restart_time = current_time
                # Priority 3: Listener stopped but cooldown hasn't passed yet (wait)
                elif mouse_stopped_but_alive or keyboard_stopped_but_alive:
                    remaining_cooldown = 0
                    if mouse_stopped_but_alive:
                        remaining = LISTENER_STOPPED_COOLDOWN_SECONDS - (current_time - self.mouse_listener_stopped_time)
                        remaining_cooldown = max(remaining_cooldown, remaining)
                    if keyboard_stopped_but_alive:
                        remaining = LISTENER_STOPPED_COOLDOWN_SECONDS - (current_time - self.keyboard_listener_stopped_time)
                        remaining_cooldown = max(remaining_cooldown, remaining)
                    logger.debug("[DEBUG] Listener stopped but thread alive - waiting for cooldown (%.1fs remaining). Mouse: %s, Keyboard: %s",
                               remaining_cooldown, mouse_stopped_but_alive, keyboard_stopped_but_alive)
                elif (mouse_inactive or keyboard_inactive) and (mouse_verified or keyboard_verified):
                    # Only restart on timeout if listeners were verified (were working before)
                    if current_time - self.last_restart_time > RESTART_COOLDOWN_SECONDS:
                        self.consecutive_failures += 1
                        backoff_time = min(30 * (2 ** (self.consecutive_failures - 1)), 300)
                        logger.warning("[DEBUG] Detected listener timeout (no events received). Using exponential backoff: %ss (failure count: %s)",
                                     backoff_time, self.consecutive_failures)
                        time.sleep(backoff_time)
                        self.restart_listeners()
                        self.last_restart_time = current_time
                elif not mouse_verified or not keyboard_verified:
                    # If listeners haven't been verified yet, check if they should have been by now
                    time_since_start = current_time - self.listener_start_time
                    extended_timeout = self.verification_timeout
                    if mouse_verified or keyboard_verified:
                        extended_timeout *= VERIFICATION_TIMEOUT_MULTIPLIER

                    if time_since_start > extended_timeout:
                        missing_mouse = (not mouse_verified and not self.mouse_listener_active)
                        missing_keyboard = (not keyboard_verified and not self.keyboard_listener_active)

                        if missing_mouse or missing_keyboard:
                            self.consecutive_failures += 1
                            backoff_time = min(30 * (2 ** (self.consecutive_failures - 1)), 300)
                            logger.warning(
                                "[DEBUG] Listener verification failed (mouse_verified=%s, keyboard_verified=%s). "
                                "Using exponential backoff: %ss (failure count: %s)",
                                mouse_verified, keyboard_verified, backoff_time, self.consecutive_failures)
                            time.sleep(backoff_time)
                            self.restart_listeners()
                            self.last_restart_time = current_time
                        else:
                            logger.debug("[DEBUG] Listeners pending verification but threads are active; waiting for user activity.")
                else:
                    # Success case: listeners are working, reset failure counter and stopped times
                    if self.consecutive_failures > 0:
                        logger.debug("[DEBUG] Listeners recovered! Resetting failure counter (was: %s)", self.consecutive_failures)
                        self.consecutive_failures = 0
                    # Reset stopped times if listeners are active again
                    if self.mouse_listener_active and self.mouse_listener_stopped_time > 0:
                        self.mouse_listener_stopped_time = 0
                    if self.keyboard_listener_active and self.keyboard_listener_stopped_time > 0:
                        self.keyboard_listener_stopped_time = 0
                
                # Periodic cleanup check (safety net to prevent event tap accumulation)
                # This runs every health check but only performs cleanup when interval has passed
                self._periodic_cleanup_check()
                
                # Sleep for health check interval
                time.sleep(HEALTH_CHECK_INTERVAL_SECONDS)
                
            except Exception as e:
                logger.error("[DEBUG] Health monitor error: %s", e, exc_info=True)
                time.sleep(30)
    
    def handle_system_sleep(self):
        """
        Handle system sleep event - stop listeners and free event taps to prevent WindowServer accumulation.
        This is critical for macOS users who close the lid at night.
        """
        try:
            # Record lifecycle event for health monitoring
            try:
                from ws_package.utils.health_monitor import record_lifecycle_event
                record_lifecycle_event('sleep')
            except Exception:
                pass  # Fail silently if health monitor not available
            
            logger.info("[SLEEP] System sleep detected - stopping listeners and freeing event taps")
            
            # Mark as sleeping first to prevent health monitor from interfering
            self._sleeping = True
            
            # Stop all listeners immediately to free event taps
            self._stop_listener_threads(stop_health_monitor=False, preserve_stop_flag=True)
            
            # CRITICAL: Additional wait on macOS to ensure WindowServer fully releases event taps
            # This prevents event tap accumulation when lid is closed
            if platform.system() == "Darwin":
                time.sleep(1.0)  # Additional wait for WindowServer to fully process cleanup
            
            logger.info("[SLEEP] Listeners stopped and event taps freed - ready for sleep")
            
        except Exception as e:
            logger.error("[SLEEP] Error handling system sleep: %s", e, exc_info=True)
    
    def handle_system_wake(self):
        """
        Handle system wake event - restart listeners after system wakes from sleep.
        CRITICAL: Explicitly clean up any lingering event taps and flush WindowServer backlog
        before restarting to prevent event accumulation.
        """
        try:
            # Record lifecycle event for health monitoring
            try:
                from ws_package.utils.health_monitor import record_lifecycle_event
                record_lifecycle_event('wake')
            except Exception:
                pass  # Fail silently if health monitor not available
            
            logger.info("[WAKE] System wake detected - cleaning up event taps and restarting listeners")
            
            # Reset sleep flag first
            self._sleeping = False
            
            # CRITICAL: Explicitly clean up any existing event taps before restarting
            # This ensures WindowServer backlog events are flushed and prevents accumulation
            # Use preserve_stop_flag=True to allow restart after cleanup
            try:
                logger.info("[WAKE] Cleaning up existing event taps to flush WindowServer backlog...")
                self._stop_listener_threads(stop_health_monitor=False, preserve_stop_flag=True)
                logger.info("[WAKE] Event taps cleaned up successfully")
            except Exception as cleanup_error:
                logger.warning("[WAKE] Error during event tap cleanup (will continue anyway): %s", cleanup_error)
            
            # Reset stop flag to allow restart
            self.stop_listeners = False
            
            # Reset verification flags - listeners need to be verified again after wake
            self.mouse_listener_verified = False
            self.keyboard_listener_verified = False
            
            # Reset counters
            self.mouse_events_received = 0
            self.keyboard_events_received = 0
            self.listener_start_time = time.time()
            
            # Reset stopped times
            self.mouse_listener_stopped_time = 0
            self.keyboard_listener_stopped_time = 0
            
            # Wait a moment for system to fully wake and WindowServer to flush any remaining events
            # This ensures WindowServer has time to process cleanup and initialize properly
            if platform.system() == "Darwin":
                time.sleep(2.0)  # Give WindowServer time to fully wake and flush backlog
            
            # Restart listeners with clean state
            logger.info("[WAKE] Restarting listeners with clean event taps...")
            self.start_listeners()
            
            logger.info("[WAKE] Listeners restarted after system wake - tracking resumed")
            
        except Exception as e:
            logger.error("[WAKE] Error handling system wake: %s", e, exc_info=True)
            # Try to restart anyway using restart_listeners which includes cleanup
            try:
                logger.warning("[WAKE] Attempting fallback restart with cleanup...")
                self.restart_listeners()
            except Exception as e2:
                logger.error("[WAKE] Failed to restart listeners after wake: %s", e2, exc_info=True)
    
    def _verify_system_is_sleeping(self) -> bool:
        """
        Verify if system is actually sleeping by checking if we can query system state.
        Returns True if system is actually sleeping, False if system is awake.
        """
        try:
            import platform
            import subprocess
            
            if platform.system() == "Darwin":
                # macOS: Try to get frontmost app - if successful, system is awake
                try:
                    cmd = """osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'"""
                    result = subprocess.check_output(
                        cmd, 
                        shell=True, 
                        timeout=1.0,
                        stderr=subprocess.DEVNULL
                    ).decode('utf-8').strip()
                    if result and result.strip():
                        # System is responsive - it's awake
                        return False
                except Exception:
                    # If we can't query the system, it might be sleeping
                    pass
            elif platform.system() == "Windows":
                # Windows: Try to get foreground window - if successful, system is awake
                try:
                    import win32gui
                    hwnd = win32gui.GetForegroundWindow()
                    if hwnd:
                        # System is responsive - it's awake
                        return False
                except ImportError:
                    pass
                except Exception:
                    pass
            
            # If we can't verify, check with SystemWakeDetector
            try:
                from ws_package.utils.system_wake_detector import get_system_wake_detector
                import time
                wake_detector = get_system_wake_detector()
                if wake_detector:
                    stats = wake_detector.get_stats()
                    last_sleep_time = stats.get('last_sleep_time')
                    last_wake_time = stats.get('last_wake_time')
                    
                    if last_sleep_time:
                        if isinstance(last_sleep_time, datetime.datetime):
                            sleep_timestamp = last_sleep_time.timestamp()
                        else:
                            sleep_timestamp = last_sleep_time
                        
                        current_timestamp = time.time()
                        time_since_sleep = current_timestamp - sleep_timestamp
                        
                        # If sleep was detected more than 3 minutes ago, assume awake
                        if time_since_sleep > 180:
                            return False
                        
                        # Check if wake time is after sleep time
                        if last_wake_time:
                            if isinstance(last_wake_time, datetime.datetime):
                                wake_timestamp = last_wake_time.timestamp()
                            else:
                                wake_timestamp = last_wake_time
                            
                            if wake_timestamp >= sleep_timestamp:
                                return False
            except Exception:
                pass
            
            # Default: assume sleeping if we can't verify otherwise
            return True
            
        except Exception as e:
            logger.debug("Error verifying system sleep state: %s", e)
            # If we can't verify, assume sleeping to be safe
            return True
    
    def _periodic_cleanup_check(self):
        """
        Periodic check to ensure event taps are cleaned up (safety net).
        This prevents event tap accumulation even if sleep/wake detection fails.
        """
        try:
            # Skip if system is sleeping (but verify it's actually sleeping)
            if self._sleeping:
                # Verify system is actually sleeping
                if not self._verify_system_is_sleeping():
                    logger.info("[CLEANUP] System marked as sleeping but is actually awake - resetting sleep flag")
                    self._sleeping = False
                else:
                    return
            
            current_time = time.time()
            
            # Check if enough time has passed since last cleanup check
            time_since_last_cleanup = current_time - self._last_periodic_cleanup_time
            if time_since_last_cleanup < PERIODIC_CLEANUP_INTERVAL_SECONDS:
                return  # Not time for cleanup check yet
            
            # Check if listeners are running but haven't received events in a while
            # This could indicate event taps are stuck or accumulating
            time_since_mouse = current_time - self.last_mouse_activity
            time_since_keyboard = current_time - self.last_keyboard_activity
            
            # If no events for threshold period, perform safety cleanup
            if (time_since_mouse > PERIODIC_CLEANUP_INACTIVITY_THRESHOLD and 
                time_since_keyboard > PERIODIC_CLEANUP_INACTIVITY_THRESHOLD and
                (self.mouse_listener_active or self.keyboard_listener_active)):
                
                logger.warning(
                    "[CLEANUP] No events received for %ss (mouse: %.0fs, keyboard: %.0fs) - "
                    "performing safety cleanup to prevent event tap accumulation",
                    PERIODIC_CLEANUP_INACTIVITY_THRESHOLD, time_since_mouse, time_since_keyboard)
                
                # Perform cleanup and restart
                try:
                    self._stop_listener_threads(stop_health_monitor=False, preserve_stop_flag=True)
                    
                    # Wait for WindowServer to flush on macOS
                    if platform.system() == "Darwin":
                        time.sleep(1.0)  # Wait for WindowServer cleanup
                    
                    # Reset stop flag and restart
                    self.stop_listeners = False
                    self.start_listeners()
                    
                    logger.info("[CLEANUP] Safety cleanup completed - listeners restarted")
                    
                except Exception as cleanup_error:
                    logger.error("[CLEANUP] Error during safety cleanup: %s", cleanup_error, exc_info=True)
            
            # Update last cleanup check time
            self._last_periodic_cleanup_time = current_time
            
        except Exception as e:
            logger.error("[CLEANUP] Error in periodic cleanup check: %s", e, exc_info=True)
    
    def verify_listeners_are_working(self):
        """Verify that listeners are actually receiving events with adaptive timeout"""
        try:
            logger.debug("[DEBUG] Verifying listeners are receiving events...")
            
            # Adaptive timeout: Longer if system appears idle or if already running
            timeout = self.verification_timeout
            if self.adaptive_timeout:
                time_since_start = time.time() - self.listener_start_time
                if time_since_start > VERIFICATION_START_THRESHOLD_SECONDS:
                    timeout = HEALTH_CHECK_INTERVAL_SECONDS  # Extend to 30 seconds for better reliability
            
            # Wait up to timeout seconds for first events
            start_verification = time.time()
            initial_mouse_events = self.mouse_events_received
            initial_keyboard_events = self.keyboard_events_received
            
            # FIXED: Use counter-based logging to reduce overhead in tight loop
            # Only log every 10 iterations (every 5 seconds) to reduce I/O overhead
            check_count = 0
            LOG_INTERVAL = 10  # Log every 10 iterations (every 5 seconds with 0.5s sleep)
            
            while (time.time() - start_verification) < timeout:
                check_count += 1
                
                # Check if we've received any events
                mouse_verified_this_iteration = False
                keyboard_verified_this_iteration = False
                
                if self.mouse_events_received > initial_mouse_events:
                    if not self.mouse_listener_verified:
                        self.mouse_listener_verified = True
                        mouse_verified_this_iteration = True
                
                if self.keyboard_events_received > initial_keyboard_events:
                    if not self.keyboard_listener_verified:
                        self.keyboard_listener_verified = True
                        keyboard_verified_this_iteration = True
                
                # Log verification events immediately (important state changes)
                if mouse_verified_this_iteration:
                    logger.debug("[DEBUG] Mouse listener verified - received %s events", self.mouse_events_received)
                if keyboard_verified_this_iteration:
                    logger.debug("[DEBUG] Keyboard listener verified - received %s events", self.keyboard_events_received)
                
                # If both verified, we're done
                if self.mouse_listener_verified and self.keyboard_listener_verified:
                    logger.debug("[DEBUG] Both listeners verified and receiving events")
                    return True
                
                # Only log periodic status every N iterations to reduce I/O overhead
                if check_count % LOG_INTERVAL == 0:
                    elapsed = time.time() - start_verification
                    logger.debug("[DEBUG] Still verifying listeners... (%.1fs/%ss elapsed, mouse_verified=%s, keyboard_verified=%s)",
                               elapsed, timeout, self.mouse_listener_verified, self.keyboard_listener_verified)
                
                time.sleep(LISTENER_INITIALIZATION_WAIT_SECONDS)
            
            # Timeout reached
            if self.mouse_listener_verified:
                logger.debug("[DEBUG] Mouse listener verified but keyboard listener not verified after %ss (keyboard events may be infrequent)", timeout)
            elif self.keyboard_listener_verified:
                logger.debug("[DEBUG] Keyboard listener verified but mouse listener not verified after %ss", timeout)
            else:
                logger.warning("[DEBUG] Neither listener verified after %ss - Mouse events: %s, Keyboard events: %s",
                             timeout, self.mouse_events_received, self.keyboard_events_received)
                logger.warning("[DEBUG] Listeners may not have proper permissions or may be blocked by system")
            
            # Return True only if both verified (preserve original contract)
            # Health monitor handles partial verification separately with extended timeout
            return self.mouse_listener_verified and self.keyboard_listener_verified
            
        except Exception as e:
            logger.error("[DEBUG] Error verifying listeners: %s", e, exc_info=True)
            return False
    
    def restart_listeners(self):
        """Safely restart all listeners"""
        try:
            logger.debug("[DEBUG] Restarting listeners...")
            self._stop_listener_threads()

            if self.listeners_paused_due_to_permissions:
                logger.warning("[DEBUG] Skipping listener restart - waiting for macOS permissions to be granted")
                return

            # Reset counters before restarting
            self.mouse_events_received = 0
            self.keyboard_events_received = 0
            self.listener_start_time = time.time()
            # Reset stopped times when restarting
            self.mouse_listener_stopped_time = 0
            self.keyboard_listener_stopped_time = 0

            # Restart listeners
            self.start_listeners()
            
            logger.debug("[DEBUG] Listeners restarted successfully")
            
        except Exception as e:
            logger.error("[DEBUG] Failed to restart listeners: %s", e, exc_info=True)

    def _stop_listener_threads(self, stop_health_monitor: bool = False, preserve_stop_flag: bool = False):
        """Gracefully stop listener threads and optionally the health monitor."""
        try:
            if not preserve_stop_flag:
                self.stop_listeners = True
            
            # CRITICAL: Stop HardwareEventValidator FIRST (before pynput listeners)
            # Linux: reinitialize monitors so evdev/xinput survive listener restarts.
            # macOS/Windows: full cleanup so pynput can reclaim event taps.
            if hasattr(self, 'hardware_validator') and self.hardware_validator:
                try:
                    if platform.system() == "Linux":
                        self.hardware_validator.reinitialize_linux()
                        logger.debug("[DEBUG] HardwareEventValidator reinitialized (Linux)")
                    else:
                        self.hardware_validator.cleanup()
                        logger.debug("[DEBUG] HardwareEventValidator cleaned up")
                except Exception as e:
                    logger.debug("[DEBUG] Error resetting hardware validator: %s", e)
            
            # Wait for HardwareEventValidator cleanup to complete
            if platform.system() == "Darwin":
                time.sleep(0.5)  # Give HardwareEventValidator time to clean up
            
            # Stop pynput listeners
            for listener_attr in ("mouse_listener", "keyboard_listener"):
                listener = getattr(self, listener_attr, None)
                if listener:
                    try:
                        listener.stop()
                    except Exception as e:
                        logger.debug("[DEBUG] Error stopping %s: %s", listener_attr, e)
                    setattr(self, listener_attr, None)
            
            # CRITICAL: Extended wait for CFRunLoop to process stop commands
            # WindowServer needs time to fully release event taps
            if platform.system() == "Darwin":
                time.sleep(2.0)  # Increased from 0.8s to 2.0s for better cleanup
            else:
                time.sleep(THREAD_STOP_WAIT_SECONDS)

            # Wait for threads to finish
            for thread_attr in ("mouse_thread", "keyboard_thread"):
                thread = getattr(self, thread_attr, None)
                if thread and thread.is_alive():
                    try:
                        thread.join(timeout=5)
                    except RuntimeError:
                        pass
                    setattr(self, thread_attr, None)

            if stop_health_monitor and self.health_monitor_thread and self.health_monitor_thread.is_alive():
                try:
                    self.health_monitor_thread.join(timeout=5)
                except RuntimeError:
                    pass
                self.health_monitor_thread = None
        finally:
            if not preserve_stop_flag:
                self.stop_listeners = False
            self.mouse_listener_active = False
            self.keyboard_listener_active = False
            if not preserve_stop_flag:
                self.mouse_listener_verified = False
                self.keyboard_listener_verified = False

    def start_mouse_listener(self):
        """Start mouse listener with comprehensive error handling and debugging"""
        retry_count = 0
        max_retries = 5
        
        while retry_count < max_retries and not self.stop_listeners:
            try:
                logger.debug("[DEBUG] Starting mouse listener (attempt %s/%s)...", retry_count + 1, max_retries)

                if platform.system() == "Darwin":
                    def _mac_mouse_callback(event_type, event):
                        if (
                            self.hardware_validator
                            and not self.hardware_validator.should_count_macos_cg_event(
                                event, input_kind='mouse'
                            )
                        ):
                            return

                        location = Quartz.CGEventGetLocation(event)
                        x, y = location.x, location.y

                        if event_type == Quartz.kCGEventMouseMoved:
                            self.on_mousemove(x, y)
                        elif event_type in (Quartz.kCGEventLeftMouseDown, Quartz.kCGEventRightMouseDown, Quartz.kCGEventOtherMouseDown):
                            self.on_click(x, y, str(event_type), True)
                        elif event_type in (Quartz.kCGEventLeftMouseUp, Quartz.kCGEventRightMouseUp, Quartz.kCGEventOtherMouseUp):
                            self.on_click(x, y, str(event_type), False)
                        elif event_type == Quartz.kCGEventScrollWheel:
                            dy = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGScrollWheelEventDeltaAxis1)
                            dx = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGScrollWheelEventDeltaAxis2)
                            self.on_scroll(x, y, dx, dy)

                    import Quartz

                    listener = MacCGEventListener(
                        event_types=[
                            Quartz.kCGEventMouseMoved,
                            Quartz.kCGEventLeftMouseDown,
                            Quartz.kCGEventLeftMouseUp,
                            Quartz.kCGEventRightMouseDown,
                            Quartz.kCGEventRightMouseUp,
                            Quartz.kCGEventOtherMouseDown,
                            Quartz.kCGEventOtherMouseUp,
                            Quartz.kCGEventScrollWheel,
                        ],
                        callback=_mac_mouse_callback,
                        name="MacCGMouseListener",
                    )
                    listener.start()
                    self.mouse_listener = listener
                    self.mouse_listener_active = True
                    logger.info("[DEBUG] macOS CGevent mouse listener started")
                else:
                    if mouse is None:
                        raise RuntimeError("pynput mouse module is unavailable")
                    with mouse.Listener(
                        on_click=self.on_click,
                        on_scroll=self.on_scroll,
                        on_move=self.on_mousemove
                    ) as listener:
                        self.mouse_listener = listener
                        self.mouse_listener_active = True
                        logger.debug("[DEBUG] Mouse listener created, running=%s", listener.running)
                        if not listener.running:
                            raise Exception("Mouse listener failed to start - listener.running is False")
                        while listener.running and not self.stop_listeners:
                            time.sleep(1)
                        if listener.running:
                            listener.stop()

                while not self.stop_listeners and getattr(self.mouse_listener, "running", False):
                    time.sleep(1)
                self.mouse_listener_active = False
                break
                    
            except Exception as e:
                retry_count += 1
                self.mouse_listener_active = False
                logger.error("[DEBUG] Mouse listener exception (attempt %s): %s: %s", retry_count, type(e).__name__, e)
                logger.error("[DEBUG] Exception traceback:", exc_info=True)
                
                if retry_count < max_retries:
                    wait_time = min(retry_count * RETRY_WAIT_MULTIPLIER, MAX_RETRY_WAIT_SECONDS)
                    logger.debug("[DEBUG] Retrying mouse listener in %s seconds...", wait_time)
                    time.sleep(wait_time)
                else:
                    logger.error("[DEBUG] Mouse listener failed after all retries")
                    break

    def start_keyboard_listener(self):
        """Start keyboard listener with comprehensive error handling and debugging"""
        retry_count = 0
        max_retries = 5
        
        while retry_count < max_retries and not self.stop_listeners:
            try:
                logger.debug("[DEBUG] Starting keyboard listener (attempt %s/%s)...", retry_count + 1, max_retries)

                if platform.system() == "Darwin":
                    def _mac_keyboard_callback(event_type, event):
                        if (
                            self.hardware_validator
                            and not self.hardware_validator.should_count_macos_cg_event(
                                event, input_kind='keyboard'
                            )
                        ):
                            return

                        keycode = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode)
                        key_repr = f"cgkeycode:{int(keycode)}"
                        if event_type == Quartz.kCGEventKeyDown:
                            self.on_keypress(key_repr)
                        elif event_type == Quartz.kCGEventKeyUp:
                            self.on_keyrelease(key_repr)

                    import Quartz

                    listener = MacCGEventListener(
                        event_types=[Quartz.kCGEventKeyDown, Quartz.kCGEventKeyUp],
                        callback=_mac_keyboard_callback,
                        name="MacCGKeyboardListener",
                    )
                    listener.start()
                    self.keyboard_listener = listener
                    self.keyboard_listener_active = True
                    logger.info("[DEBUG] macOS CGevent keyboard listener started")
                else:
                    if keyboard is None:
                        raise RuntimeError("pynput keyboard module is unavailable")
                    with keyboard.Listener(
                        on_press=self.on_keypress,
                        on_release=self.on_keyrelease
                    ) as listener:
                        self.keyboard_listener = listener
                        self.keyboard_listener_active = True
                        logger.debug("[DEBUG] Keyboard listener created, running=%s", listener.running)
                        if not listener.running:
                            raise Exception("Keyboard listener failed to start - listener.running is False")
                        while listener.running and not self.stop_listeners:
                            time.sleep(1)
                        if listener.running:
                            listener.stop()

                while not self.stop_listeners and getattr(self.keyboard_listener, "running", False):
                    time.sleep(1)
                self.keyboard_listener_active = False
                break
                    
            except Exception as e:
                retry_count += 1
                self.keyboard_listener_active = False
                logger.error("[DEBUG] Keyboard listener exception (attempt %s): %s: %s", retry_count, type(e).__name__, e)
                logger.error("[DEBUG] Exception traceback:", exc_info=True)
                
                if retry_count < max_retries:
                    wait_time = min(retry_count * RETRY_WAIT_MULTIPLIER, MAX_RETRY_WAIT_SECONDS)
                    logger.debug("[DEBUG] Retrying keyboard listener in %s seconds...", wait_time)
                    time.sleep(wait_time)
                else:
                    logger.error("[DEBUG] Keyboard listener failed after all retries")
                    break

    def on_click(self, x, y, button, pressed):
        """Handle mouse click events with error handling and debugging"""
        try:
            if pressed:
                self.last_mouse_activity = time.time()
                self.mouse_events_received += 1
                
                # Verify listener on first event
                if not self.mouse_listener_verified:
                    self.mouse_listener_verified = True
                    logger.debug("[DEBUG] Mouse listener verified - first click event received")
                
                should_count = self._should_count_hardware_activity(
                    self.hardware_validator.is_hardware_mouse_event
                    if self.hardware_validator
                    else lambda: None,
                    'mouse',
                )
                
                # Removed frequent "Mouse Clicked" log - too verbose for production
                current_time = time.time()
                second_index = int(current_time % self.size)
                
                # Only count clicks and mark activity if validation passed
                if should_count:
                    self.mouse_click_count += 1
                    logger.info("Mouse clicked")
                    self.mouse_activity_array[second_index] = True
                    
                    # Limit event list size
                    if len(self.mouse_events) >= self.max_events:
                        self.mouse_events = self.mouse_events[-500:]
                    
                    # Removed mouse event logging for Linux (similar to macOS)
                    # self.mouse_events.append({str(time.strftime('%Y-%m-%d %H:%M:%S')): f'Mouse clicked at ({str(x)}, {str(y)}) with {str(button)}'})
                    
                    # Record for fudging detection (fail-safe)
                    if self.fudging_detector:
                        try:
                            self.fudging_detector.record_mouse_event(x, y, 0, time.time())
                        except Exception as e:
                            logger.debug("Fudging detection error (ignored): %s", e)
        except Exception as e:
            logger.error("[DEBUG] Exception in on_click handler: %s: %s", type(e).__name__, e)
            logger.error("[DEBUG] Exception details:", exc_info=True)
            # Don't re-raise - continue listening even if handler fails

    def on_scroll(self, x, y, dx, dy):
        """Handle mouse scroll events with error handling"""
        try:
            self.last_mouse_activity = time.time()
            self.mouse_events_received += 1
            
            # Verify listener on first event
            if not self.mouse_listener_verified:
                self.mouse_listener_verified = True
                # Removed frequent verification log - already verified on click/move
            
            current_time = time.time()
            second_index = int(current_time % self.size)

            should_count = self._should_count_hardware_activity(
                self.hardware_validator.is_hardware_mouse_event
                if self.hardware_validator
                else lambda: None,
                'mouse',
            )

            if should_count:
                self.mouse_move_count += 1
                self.mouse_activity_array[second_index] = True
        except Exception as e:
            logger.error("[DEBUG] Exception in on_scroll handler: %s: %s", type(e).__name__, e)
            logger.error("[DEBUG] Exception details:", exc_info=True)
            # Don't re-raise - continue listening even if handler fails

    def on_keypress(self, key):
        """Handle keyboard press events with key repeat detection and error handling"""
        try:
            self.keyboard_events_received += 1
            
            # Verify listener on first event
            if not self.keyboard_listener_verified:
                self.keyboard_listener_verified = True
                logger.info("[DEBUG] Keyboard listener verified - first keypress event received")
            
            current_time = time.time()
            key_str = str(key)
            
            should_count = self._should_count_hardware_activity(
                self.hardware_validator.is_hardware_keyboard_event
                if self.hardware_validator
                else lambda: None,
                'keyboard',
            )
            
            # Key repeat detection - track press state; activity marked on release only
            if key_str not in self.pressed_keys:
                if should_count:
                    self.pressed_keys.add(key_str)
                    self.keyboard_count += 1
                    logger.info("Keyboard pressed")
                    
                    # Removed keyboard event logging for Linux (similar to macOS)
                    # self.keyboard_events.append(f'Key pressed')
            
            # Record ALL keypress events for fudging detection (including repeats for held key detection)
            # This must be outside the if/else to capture repeat events
            if self.fudging_detector and should_count:
                try:
                    self.fudging_detector.record_keyboard_event(key_str, current_time)
                except Exception as e:
                    logger.debug("Fudging detection error (ignored): %s", e)
            
            self.last_key_press_time = current_time
            self.last_key = key_str
        except Exception as e:
            logger.error("[DEBUG] Exception in on_keypress handler: %s: %s", type(e).__name__, e)
            logger.error("[DEBUG] Exception details:", exc_info=True)
            # Don't re-raise - continue listening even if handler fails

    def on_keyrelease(self, key):
        """Handle keyboard release events — activity is counted on release only."""
        try:
            key_str = str(key)
            if key_str in self.pressed_keys:
                self.pressed_keys.remove(key_str)

                current_time = time.time()
                second_index = int(current_time % self.size)

                should_count = self._should_count_hardware_activity(
                    self.hardware_validator.is_hardware_keyboard_event
                    if self.hardware_validator
                    else lambda: None,
                    'keyboard',
                )

                if should_count:
                    self.keyboard_activity_array[second_index] = True
                    self.last_keyboard_activity = current_time

                # Reset fudging pattern tracking when key is released
                # This ensures accurate duration calculation (pattern ends when key is released)
                if self.fudging_detector:
                    try:
                        self.fudging_detector.record_key_release(key_str)
                        fudging_info = self.fudging_detector.get_fudging_info()
                        pattern_key = fudging_info.get('fudging_pattern_key')
                        pattern_type = fudging_info.get('fudging_pattern_type')
                        if (pattern_key == key_str and 
                            pattern_type in ('regular_intervals', 'held_key')):
                            # Key that was being fudged is now released - reset pattern tracking
                            self.fudging_detector.reset_pattern_tracking()
                            logger.debug("Fudging pattern tracking reset - key '%s' released", key_str)
                    except Exception as e:
                        logger.debug("Error resetting fudging pattern on key release (ignored): %s", e)
        except Exception as e:
            logger.error("Error in keyboard release handler: %s", e)

    def on_mousemove(self, x, y):
        """Handle mouse movement events with throttling and distance filtering to reduce WindowServer load"""
        try:
            current_time = time.time()
            
            # CRITICAL: Calculate distance FIRST (cheap operation, no array access)
            # This allows us to exit early before any expensive operations
            dx = x - self.prev_mouse_x
            dy = y - self.prev_mouse_y
            move_distance_squared = dx * dx + dy * dy
            
            # CRITICAL: Verification must happen BEFORE early exits
            # This ensures the listener gets verified even with small initial movements
            # VERIFICATION: Lower threshold (1px) to detect ANY movement
            # Compare squared values (1px² = 1) - avoids sqrt calculation
            if move_distance_squared > MOUSE_VERIFICATION_THRESHOLD_SQUARED:
                self.mouse_events_received += 1
                if not self.mouse_listener_verified:
                    self.mouse_listener_verified = True
                    logger.info("[DEBUG] Mouse listener verified - first move event received (distance: %spx²)", move_distance_squared)
            
            # EARLY EXIT 1: Throttle check FIRST (before any array operations)
            # This reduces WindowServer processing by limiting handler execution frequency
            # No throttle for unverified listeners to ensure faster verification
            throttle_time = MOUSE_MOVE_THROTTLE_SECONDS if self.mouse_listener_verified else 0.0
            if throttle_time > 0 and current_time - self._last_mouse_process_time < throttle_time:
                # Update position for next calculation, but skip all processing
                self.prev_mouse_x, self.prev_mouse_y = x, y
                return  # Exit early - reduces WindowServer processing significantly
            
            # EARLY EXIT 2: Ignore micro-movements to reduce WindowServer load
            # Only filter AFTER verification to ensure listener gets verified
            # This filters out tiny movements (< 3px) that don't represent meaningful activity
            if move_distance_squared < MOUSE_MIN_MOVE_THRESHOLD_SQUARED and self.mouse_listener_verified:
                # Update position for next calculation, but skip all processing
                self.prev_mouse_x, self.prev_mouse_y = x, y
                return  # Exit early - reduces WindowServer processing significantly
            
            second_index = int(current_time % self.size)

            should_count = self._should_count_hardware_activity(
                self.hardware_validator.is_hardware_mouse_event
                if self.hardware_validator
                else lambda: None,
                'mouse',
            )

            self._last_mouse_process_time = current_time
            self.last_mouse_activity = current_time

            # Only mark activity array for validated hardware events
            if should_count and (
                not self.mouse_listener_verified
                or move_distance_squared >= MOUSE_MIN_MOVE_THRESHOLD_SQUARED
            ):
                self.mouse_activity_array[second_index] = True

            # ACTIVITY COUNTING: Higher threshold (5px) to avoid noise
            if should_count:
                if move_distance_squared < MOUSE_ACTIVITY_THRESHOLD_SQUARED:
                    self.mouse_move_count += 0.5
                else:
                    self.mouse_move_count += 1
                
                # Record for fudging detection (fail-safe, throttled internally)
                # Only calculate sqrt when needed (fudging detector requires actual distance)
                if self.fudging_detector:
                    try:
                        move_distance = math.sqrt(move_distance_squared) if move_distance_squared > 0 else 0
                        self.fudging_detector.record_mouse_event(x, y, move_distance, current_time)
                    except Exception as e:
                        logger.debug("Fudging detection error (ignored): %s", e)
            
            # Update position
            self.prev_mouse_x, self.prev_mouse_y = x, y
        except Exception as e:
            logger.error("[DEBUG] Exception in on_mousemove handler: %s: %s", type(e).__name__, e)
            logger.error("[DEBUG] Exception details:", exc_info=True)
            # Don't re-raise - continue listening even if handler fails

    async def get_activity_level(self, interval=600):
        # OPTIMIZED: Single loop instead of three separate loops (66% fewer iterations)
        keyboard_activity_count = 0
        mouse_activity_count = 0
        active_seconds = 0
        
        for i in range(interval):
            kb_active = self.keyboard_activity_array[i]
            mouse_active = self.mouse_activity_array[i]
            
            if kb_active:
                keyboard_activity_count += 1
            if mouse_active:
                mouse_activity_count += 1
            if kb_active or mouse_active:
                active_seconds += 1

        keyboard_activity = min(keyboard_activity_count * PERCENTAGE_MULTIPLIER / interval, PERCENTAGE_MULTIPLIER)
        mouse_activity = min(mouse_activity_count * PERCENTAGE_MULTIPLIER / interval, PERCENTAGE_MULTIPLIER)

        # Calculate activity percentage using active_seconds (matches urls_cpu branch behavior)
        # This represents the percentage of seconds where there was ANY activity (keyboard OR mouse)
        # This is different from sum: it counts seconds with activity, not the sum of percentages
        activity_percentage = (active_seconds / interval) * PERCENTAGE_MULTIPLIER
        
        # Aggregate mouse clicks into total activity
        total_activity = self.keyboard_count + self.mouse_move_count + self.mouse_click_count
        
        # Store original values before reduction for logging
        keyboard_activity_before = keyboard_activity
        mouse_activity_before = mouse_activity
        activity_before_reduction = activity_percentage
        
        # Apply fudging detection and reduction (fail-safe, non-blocking)
        if self.fudging_detector:
            try:
                # Check for fudging patterns (throttled internally)
                fudging_detected, fudging_type, fudging_score = self.fudging_detector.check_fudging()
                
                if fudging_detected:
                    # Get reduction factor and apply to activity percentage (matches urls_cpu behavior)
                    # The reduction factor is applied to the final activity_percentage which is based on active_seconds
                    reduction_factor = self.fudging_detector.get_reduction_factor()
                    
                    # Apply reduction to keyboard and mouse activity percentages
                    keyboard_activity = keyboard_activity * reduction_factor
                    mouse_activity = mouse_activity * reduction_factor
                    
                    # Apply reduction to activity_percentage (which is based on active_seconds)
                    # This matches urls_cpu behavior: reduce the final percentage, not recalculate from arrays
                    activity_percentage = activity_percentage * reduction_factor
                    
                    # Get fudging pattern information (duration, key, etc.)
                    fudging_info = self.fudging_detector.get_fudging_info()
                    fudged_duration = fudging_info.get('fudged_duration_seconds', 0.0)
                    fudging_pattern_key = fudging_info.get('fudging_pattern_key', 'Unknown')
                    fudging_pattern_type = fudging_info.get('fudging_pattern_type', 'Unknown')
                    
                    # Enhanced logging for fudging detection (bold and clear formatting)
                    logger.info("")
                    logger.info("=" * 80)
                    logger.info("*** DATA FUDGING DETECTED ***")
                    logger.info("=" * 80)
                    logger.info("DETECTION PATTERN: %s", fudging_type)
                    logger.info("CONFIDENCE SCORE: %s (0.0 to 1.0)", fudging_score)
                    logger.info("")
                    logger.info("FUDGING DETAILS:")
                    logger.info("  - Pattern Type: %s", fudging_pattern_type)
                    logger.info("  - Key/Device: %s", fudging_pattern_key)
                    logger.info("  - Fudged Duration: %s seconds (%s minutes)", fudged_duration, fudged_duration/60)
                    logger.info("")
                    logger.info("ACTIVITY REDUCTION:")
                    logger.info("  - Reduction Factor: %s (%s%% of activity counted)", reduction_factor, reduction_factor*100)
                    logger.info("  - Keyboard Before: %s%%, After: %s%%", keyboard_activity_before, keyboard_activity)
                    logger.info("  - Mouse Before: %s%%, After: %s%%", mouse_activity_before, mouse_activity)
                    logger.info("  - Average Before: %s%%, After: %s%%", activity_before_reduction, activity_percentage)
                    logger.info("  - Reduction Applied: %s%%", activity_before_reduction - activity_percentage)
                    logger.info("")
                    logger.info("EVENT COUNTS (in current interval):")
                    # logger.info(f"  - Keyboard Events: {self.keyboard_count}")  # DISABLED to reduce logging overhead
                    # logger.info(f"  - Mouse Moves: {self.mouse_move_count}")  # DISABLED to reduce logging overhead
                    # logger.info(f"  - Mouse Clicks: {self.mouse_click_count}")  # DISABLED to reduce logging overhead
                    # logger.info(f"  - Total Mouse Events: {self.mouse_move_count + self.mouse_click_count}")  # DISABLED to reduce logging overhead
                    logger.info("")
                    logger.info("=" * 80)
                    logger.info("")
                    
                    # Report fudging to API (with daily limit, fail-safe, non-blocking)
                    if self.fudging_report_service and (time.time() - self._last_fudging_report_time) > SECONDS_PER_DAY:
                        try:
                            import threading
                            def _report_async():
                                try:
                                    # Get fudging pattern information for API
                                    fudging_info = self.fudging_detector.get_fudging_info()
                                    fudged_duration = fudging_info.get('fudged_duration_seconds', 0.0)
                                    fudging_pattern_key = fudging_info.get('fudging_pattern_key', 'Unknown')
                                    fudging_pattern_type = fudging_info.get('fudging_pattern_type', 'Unknown')
                                    
                                    self.fudging_report_service.report_fudging(
                                        fudging_type=fudging_type,
                                        fudging_score=fudging_score,
                                        reduction_factor=reduction_factor,
                                        activity_before=activity_before_reduction,
                                        activity_after=activity_percentage,
                                        keyboard_before=keyboard_activity_before,
                                        keyboard_after=keyboard_activity,
                                        mouse_before=mouse_activity_before,
                                        mouse_after=mouse_activity,
                                        keyboard_count=self.keyboard_count,
                                        mouse_count=self.mouse_move_count + self.mouse_click_count,
                                        fudged_duration_seconds=fudged_duration,
                                        details={
                                            "pattern": fudging_type,
                                            "pattern_type": fudging_pattern_type,
                                            "pattern_key": fudging_pattern_key,
                                            "fudged_duration_seconds": fudged_duration,
                                            "fudged_duration_minutes": fudged_duration / MINUTES_PER_HOUR,
                                            "interval_seconds": interval
                                        }
                                    )
                                except Exception as e:
                                    logger.debug("Fudging report error in thread (ignored): %s", e)
                            
                            # CRITICAL: Stop existing report thread before creating new one to prevent accumulation
                            if hasattr(self, '_report_thread') and self._report_thread and self._report_thread.is_alive():
                                logger.debug("[DEBUG] Stopping existing fudging report thread before starting new one")
                                try:
                                    self._report_thread.join(timeout=1)
                                    if self._report_thread.is_alive():
                                        logger.warning("[DEBUG] Report thread did not stop within timeout, continuing anyway")
                                except Exception as e:
                                    logger.debug("[DEBUG] Error stopping existing report thread: %s", e)
                            
                            self._report_thread = threading.Thread(target=_report_async, daemon=True, name="FudgingReport")
                            self._report_thread.start()
                            # Register worker thread for tracking
                            from ws_package.thread_manager import register_worker
                            register_worker("FudgingReport", self._report_thread)
                            self._last_fudging_report_time = time.time()
                        except Exception as e:
                            logger.debug("Fudging report thread error (ignored): %s", e)
            except Exception as e:
                logger.debug("Fudging detection error (activity unchanged): %s", e)
                # Fail-safe: activity_percentage remains unchanged
        
        activityLevel = {"mouse": str(mouse_activity), "average": str(activity_percentage), "keyboard": str(keyboard_activity)}
        
        # Get system idle duration
        system_idle_duration = IdleDuration()

        # Log if system has been idle beyond the threshold
        if system_idle_duration.value() > int(self.idle_threshold_seconds):
            logger.info(
                "System has been idle for more than the threshold duration. \
                Potential sleep or user inactivity detected."
            )


        activity_log = {
            "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
            "module": "ActivityMonitor",
            "keyboard_activity": self.keyboard_count,
            "key_pressed": self.keyboard_events,
            "mouse_activity": self.mouse_move_count,
            "mouse_clicks": self.mouse_click_count,
            "mouse_clicked": self.mouse_events,
            "total_activity": total_activity,
            "activity_level_percent": activity_percentage
        }

        # Reset activity arrays after logging to prevent memory buildup
        self.keyboard_activity_array = [False] * 999
        self.mouse_activity_array = [False] * 999
        self.scroll_count = 0

        keyboard_count = self.keyboard_count
        keyboard_events = self.keyboard_events
        mouse_click_count = self.mouse_click_count
        mouse_events = self.mouse_events

        # Reset activity counters after logging
        self.keyboard_count = 0
        self.mouse_move_count = 0
        self.mouse_click_count = 0
        
        # Clear event lists periodically to prevent memory leaks
        self._cleanup_counter += 1
        if self._cleanup_counter % 10 == 0:  # Every 10 intervals
            self.keyboard_events.clear()
            self.mouse_events.clear()
            self._cleanup_counter = 0
        else:
            # Keep lists but limit size
            if len(self.keyboard_events) > 100:
                self.keyboard_events = self.keyboard_events[-50:]
            if len(self.mouse_events) > 100:
                self.mouse_events = self.mouse_events[-50:]

        # Create ActivityEntity with proper memory management
        activity_entity = self._create_activity_entity(
            keyboard_count, 
            keyboard_events,
            mouse_click_count,
            mouse_events
        )
        
        # logger.info(f"Activity Level: {activityLevel}")  # DISABLED to reduce logging overhead - keyboard/mouse events
        return activityLevel, activity_entity
    
    def log_listener_state(self):
        """Log detailed listener state for debugging"""
        try:
            mouse_running = False
            keyboard_running = False
            
            # Try to check listener state (may not always be accessible)
            if hasattr(self, 'mouse_listener') and self.mouse_listener:
                mouse_running = getattr(self.mouse_listener, 'running', False)
            if hasattr(self, 'keyboard_listener') and self.keyboard_listener:
                keyboard_running = getattr(self.keyboard_listener, 'running', False)
            
            logger.info("[DEBUG] Listener State - "
                       f"Mouse: running={mouse_running}, active={getattr(self, 'mouse_listener_active', False)}, "
                       f"verified={getattr(self, 'mouse_listener_verified', False)}, "
                       f"events={getattr(self, 'mouse_events_received', 0)}, "
                       f"thread_alive={self.mouse_thread.is_alive() if self.mouse_thread else False}, "
                       f"last_activity={time.time() - getattr(self, 'last_mouse_activity', 0):.1f}s ago | "
                       f"Keyboard: running={keyboard_running}, active={getattr(self, 'keyboard_listener_active', False)}, "
                       f"verified={getattr(self, 'keyboard_listener_verified', False)}, "
                       f"events={getattr(self, 'keyboard_events_received', 0)}, "
                       f"thread_alive={self.keyboard_thread.is_alive() if self.keyboard_thread else False}, "
                       f"last_activity={time.time() - getattr(self, 'last_keyboard_activity', 0):.1f}s ago")
        except Exception as e:
            logger.error("[DEBUG] Error logging listener state: %s", e, exc_info=True)
    
    def stop_listening(self):
        """Stop all listeners and cleanup"""
        try:
            logger.info("[DEBUG] Stopping all listeners...")
            self._stop_listener_threads(stop_health_monitor=True, preserve_stop_flag=True)
            
            # Additional wait on macOS to let WindowServer fully flush queued events
            # This prevents mouse cursor movement after app termination
            if platform.system() == "Darwin":
                time.sleep(1.0)  # Give WindowServer time to flush any remaining queued events
            
            logger.info("[DEBUG] All listeners stopped and event taps released")
            
        except Exception as e:
            logger.error("[DEBUG] Error stopping listeners: %s", e, exc_info=True)
    
    def get_listener_status(self):
        """Get current status of all listeners"""
        return {
            "mouse_listener_active": getattr(self, 'mouse_listener_active', False),
            "keyboard_listener_active": getattr(self, 'keyboard_listener_active', False),
            "mouse_listener_verified": getattr(self, 'mouse_listener_verified', False),
            "keyboard_listener_verified": getattr(self, 'keyboard_listener_verified', False),
            "mouse_thread_alive": self.mouse_thread.is_alive() if self.mouse_thread else False,
            "keyboard_thread_alive": self.keyboard_thread.is_alive() if self.keyboard_thread else False,
            "health_monitor_alive": self.health_monitor_thread.is_alive() if self.health_monitor_thread else False,
            "last_mouse_activity": time.time() - getattr(self, 'last_mouse_activity', 0),
            "last_keyboard_activity": time.time() - getattr(self, 'last_keyboard_activity', 0),
            "mouse_events_received": getattr(self, 'mouse_events_received', 0),
            "keyboard_events_received": getattr(self, 'keyboard_events_received', 0)
        }

    def check_macos_permissions(self):
        """
        Check macOS permissions without creating additional event taps.
        Returns tuple: (mouse_permission_granted, keyboard_permission_granted)
        This method is macOS-specific and safe to call on other platforms.
        """
        if platform.system() != "Darwin":
            # Not macOS, assume permissions OK (Windows/Linux don't need these)
            return (True, True)
        
        current_time = time.time()
        if (current_time - self._last_permission_check) < PERMISSION_CACHE_TTL_SECONDS:
            return self._cached_permission_result

        permission_result = self._check_macos_permissions_via_ctypes()
        if permission_result is None:
            # Fallback to previous known state to avoid blocking the app
            permission_result = self._cached_permission_result
        else:
            self._cached_permission_result = permission_result
        self._last_permission_check = current_time
        
        return permission_result

    def _check_macos_permissions_via_ctypes(self):
        """Use ApplicationServices APIs to determine Input Monitoring status."""
        try:
            app_services = ctypes.CDLL(APPLICATION_SERVICES_PATH)
        except OSError as e:
            logger.warning("[PERMISSIONS] Unable to load ApplicationServices: %s", e)
            return None

        listen_ok = True
        accessibility_ok = True

        try:
            app_services.CGPreflightListenEventAccess.restype = ctypes.c_bool
            listen_ok = bool(app_services.CGPreflightListenEventAccess())
        except AttributeError:
            logger.debug("[PERMISSIONS] CGPreflightListenEventAccess not available - assuming granted")
        except Exception as e:
            logger.warning("[PERMISSIONS] CGPreflightListenEventAccess check failed: %s", e)

        try:
            if hasattr(app_services, "AXIsProcessTrusted"):
                app_services.AXIsProcessTrusted.restype = ctypes.c_bool
                accessibility_ok = bool(app_services.AXIsProcessTrusted())
        except AttributeError:
            logger.debug("[PERMISSIONS] AXIsProcessTrusted not available - assuming granted")
        except Exception as e:
            logger.warning("[PERMISSIONS] AXIsProcessTrusted check failed: %s", e)

        # CRITICAL: Only require Input Monitoring permission (listen_ok) for keyboard/mouse listeners
        # Accessibility permission (accessibility_ok) is optional and not required for Input Monitoring
        # This allows listeners to start even if Accessibility permission is not granted
        # Input Monitoring is the primary requirement for keyboard/mouse event monitoring
        permission_granted = listen_ok  # Only check Input Monitoring, not Accessibility
        return (permission_granted, permission_granted)
    
    def _create_activity_entity(self, keyboard_count: int, keyboard_events: list, 
                               mouse_click_count: int, mouse_events: list) -> ActivityEntity:
        """Create ActivityEntity with proper memory management and error handling."""
        try:
            # Create copies of event lists to prevent reference issues
            keyboard_events_copy = keyboard_events.copy() if keyboard_events else []
            mouse_events_copy = mouse_events.copy() if mouse_events else []
            
            return ActivityEntity(
                keyboard_count=keyboard_count, 
                keyboard_activity=keyboard_events_copy,
                mouse_click_count=mouse_click_count,
                mouse_activity=mouse_events_copy,
                timestamp=datetime.datetime.now()
            )
        except Exception as e:
            logger.error("Error creating ActivityEntity: %s", e)
            # Return minimal entity to prevent crashes
            return ActivityEntity(
                keyboard_count=0, 
                keyboard_activity=[],
                mouse_click_count=0,
                mouse_activity=[],
                timestamp=datetime.datetime.now()
            )