import threading
import time
import platform
import ctypes
import ctypes.wintypes
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
import datetime

logger = LogWriter.file('DeviceListener')

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

# Constants for activity tracking
ACTIVITY_ARRAY_MIN_SIZE = 999  # Minimum array size for activity tracking
MAX_EVENTS_LIMIT = 1000  # Limit event list size for memory management

# Health monitoring constants
HEALTH_CHECK_INTERVAL_SECONDS = 120  # Check every 2 minutes when user is active
MAX_INACTIVITY_THRESHOLD_SECONDS = 60  # Maximum inactivity threshold
MIN_RESTART_COOLDOWN_SECONDS = 600  # Avoid tearing down hooks more than once per 10 min

# Mouse polling (Windows) — GetCursorPos / GetAsyncKeyState, no LL hooks
MOUSE_POLL_INTERVAL_SECONDS = 1.0
MOUSE_POLL_IDLE_INTERVAL_SECONDS = 2.0
MOUSE_MOVE_THROTTLE_SECONDS = 0.3
MOUSE_VERIFICATION_THRESHOLD_SQUARED = 1
MOUSE_MIN_MOVE_THRESHOLD_SQUARED = 9   # 3px
MOUSE_ACTIVITY_THRESHOLD_SQUARED = 25  # 5px

# When Windows GetLastInputInfo exceeds this, cursor-poll deltas are not user input
# (optical jitter / SetCursorPos). Real mouse use resets OS idle to ~0 immediately.
OS_IDLE_POLL_SUPPRESS_SECONDS = 5

# Default interval (10 minutes)
DEFAULT_INTERVAL_SECONDS = 600

# Windows virtual-key codes for poll-based click detection (no WH_MOUSE_LL hook)
_VK_LBUTTON = 0x01
_VK_RBUTTON = 0x02
_VK_MBUTTON = 0x04


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("Windows Listener initialized: %s self.size is %s", interval, self.size)
        
        # Activity tracking
        self.keyboard_count = 0
        self.mouse_move_count = 0
        self.mouse_click_count = 0

        # Dynamic activity arrays based on interval size
        self.array_size = max(interval, ACTIVITY_ARRAY_MIN_SIZE)
        self.keyboard_activity_array = [False] * self.array_size
        self.mouse_activity_array = [False] * self.array_size
        self.scroll_count = 0

        self.prev_mouse_x, self.prev_mouse_y = 0, 0
        self._move_log_counter = 0
        self._last_mouse_process_time = 0.0
        self._mouse_poll_interval = MOUSE_POLL_INTERVAL_SECONDS
        self._mouse_poll_idle_interval = MOUSE_POLL_IDLE_INTERVAL_SECONDS
        self._mouse_button_down = {
            _VK_LBUTTON: False,
            _VK_RBUTTON: False,
            _VK_MBUTTON: False,
        }
        
        # Thread management with health monitoring
        self.mouse_thread = None
        self.keyboard_thread = None  # unused — keyboard polled in mouse thread
        self.health_monitor_thread = None
        self.stop_listeners = False
        
        # Health monitoring variables
        self.mouse_listener_active = False
        self.keyboard_listener_active = False
        self.last_mouse_activity = time.time()
        self.last_keyboard_activity = time.time()
        self.health_check_interval = HEALTH_CHECK_INTERVAL_SECONDS
        self.max_inactivity_threshold = MAX_INACTIVITY_THRESHOLD_SECONDS
        
        # Event reception verification
        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.verification_timeout = 20  # Base timeout: 20 seconds (increased from 10)
        self.adaptive_timeout = True  # Enable adaptive timeout
        
        # Exponential backoff tracking for general failures
        self.consecutive_failures = 0
        self.last_restart_time = 0
        self._restart_lock = threading.Lock()
        
        # Keys currently held after a physical press. Activity is credited on the
        # matching release, so a held key never accumulates activity.
        self.pressed_keys = set()

        # Keyboard-only anti-automation/fudging services.
        self.hardware_validator = None
        self.fudging_detector = None
        self.fudging_report_service = None
        self._last_fudging_report_time = 0.0
        try:
            from ws_package.utils.hardware_event_validator import get_hardware_validator
            from ws_package.utils.fudging_detector import get_fudging_detector
            from ws_package.services.fudging_report_service import get_fudging_report_service

            self.hardware_validator = get_hardware_validator()
            self.fudging_detector = get_fudging_detector()
            self.fudging_report_service = get_fudging_report_service()
        except Exception as e:
            logger.error(
                "Keyboard hardware/fudging validation unavailable; "
                "keyboard activity will not use the secure edge path: %s",
                e,
            )
        
        # Windows-specific variables
        self.is_windows = platform.system() == "Windows"
        self.last_system_wake_time = time.time()
        
        # Listener references for state monitoring
        self.mouse_listener = None
        self.keyboard_listener = None
        
        self._cached_os_idle_seconds = 0.0
        self._last_os_idle_check_time = 0.0
        
        # Start mouse polling plus keyboard edges supplied by the validator's
        # keyboard-only WH_KEYBOARD_LL hook.
        self.start_listeners()
        self.start_health_monitor()

    def _get_os_idle_seconds(self) -> float:
        """Seconds since last real keyboard/mouse input (Windows GetLastInputInfo)."""
        now = time.time()
        if now - self._last_os_idle_check_time < 1.0:
            return self._cached_os_idle_seconds
        try:
            self._cached_os_idle_seconds = float(IdleDuration().value())
        except Exception:
            self._cached_os_idle_seconds = 0.0
        self._last_os_idle_check_time = now
        return self._cached_os_idle_seconds

    def _is_os_user_idle_for_polling(self) -> bool:
        """True when OS reports no user input recently — skip cursor-poll counting."""
        return self._get_os_idle_seconds() > OS_IDLE_POLL_SUPPRESS_SECONDS

    def _is_os_user_idle_for_health(self) -> bool:
        """True when the user has not interacted recently — listener restarts are not needed."""
        return self._get_os_idle_seconds() > float(self.max_inactivity_threshold)

    def start_listeners(self):
        """Start mouse polling and keyboard edge consumption."""
        try:
            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()
            # Drop held-key state from a previous run so a stray release cannot be
            # credited to a press this listener never observed.
            self.pressed_keys.clear()

            self.mouse_thread = threading.Thread(
                target=self.start_input_poll_loop,
                daemon=True,
                name="InputPoll",
            )
            self.mouse_thread.start()
            self.keyboard_listener_active = True

            logger.info(
                "Input tracker started (mouse polling + keyboard-only WH_KEYBOARD_LL validation)"
            )

            time.sleep(0.5)

        except Exception as e:
            logger.error("Failed to start input poll tracker: %s", e)
            self.restart_listeners()

    def start_health_monitor(self):
        """Start health monitoring thread"""
        try:
            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)

    def health_monitor_loop(self):
        """Monitor listener health and restart if needed - Optimized to prevent memory leaks"""
        health_check_counter = 0
        last_resource_cleanup = time.time()
        
        while not self.stop_listeners:
            try:
                current_time = time.time()
                health_check_counter += 1
                
                # Check if threads are alive (lightweight check)
                mouse_alive = self.mouse_thread and self.mouse_thread.is_alive()
                keyboard_alive = mouse_alive  # keyboard polled in same thread
                
                # Always check for activity timeout (fixed bug where it was cached)
                mouse_inactive = (current_time - self.last_mouse_activity) > self.max_inactivity_threshold
                keyboard_inactive = (current_time - self.last_keyboard_activity) > self.max_inactivity_threshold
                
                # Check if listeners are verified (actually receiving events)
                mouse_verified = self.mouse_listener_verified
                keyboard_verified = self.keyboard_listener_verified
                
                # Check system wake state (Windows only) - reduced frequency
                if self.is_windows and health_check_counter % 5 == 0:
                    self.check_system_wake_state()
                
                # Log health status (reduced frequency — avoid log I/O in hot paths)
                if health_check_counter % 30 == 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)
                    self.log_listener_state()
                
                user_is_idle = self._is_os_user_idle_for_health()

                # Restart if needed with exponential backoff for general failures
                if not mouse_alive or not keyboard_alive:
                    self.consecutive_failures += 1
                    # Exponential backoff: 30s, 60s, 120s, 300s (max 5 min)
                    backoff_time = min(30 * (2 ** (self.consecutive_failures - 1)), 300)
                    logger.warning("Detected listener thread issues. Using exponential backoff: %ss (failure count: %s)",
                                 backoff_time, self.consecutive_failures)
                    time.sleep(backoff_time)
                    self.restart_listeners(force=True)
                    self.last_restart_time = current_time
                elif user_is_idle:
                    # No user input — missing events / unverified keyboard is expected, not a failure.
                    if self.consecutive_failures > 0:
                        logger.debug(
                            "OS idle for %ss — skipping listener restart (inactive/verify checks suppressed).",
                            int(self._get_os_idle_seconds()),
                        )
                        self.consecutive_failures = 0
                elif (mouse_inactive or keyboard_inactive) and (mouse_verified or keyboard_verified):
                    # Only restart on timeout if listeners were verified (were working before)
                    # This prevents false restarts when system is just idle
                    if current_time - self.last_restart_time > 300:  # 5 minutes
                        self.consecutive_failures += 1
                        backoff_time = min(30 * (2 ** (self.consecutive_failures - 1)), 300)
                        logger.warning("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
                    if time_since_start > self.verification_timeout:
                        self.consecutive_failures += 1
                        backoff_time = min(30 * (2 ** (self.consecutive_failures - 1)), 300)
                        logger.warning("Listeners not verified after %.1fs. Using exponential backoff: %ss (failure count: %s)",
                                     time_since_start, backoff_time, self.consecutive_failures)
                        time.sleep(backoff_time)
                        self.restart_listeners()
                        self.last_restart_time = current_time
                else:
                    # Success case: listeners are working, reset failure counter
                    if self.consecutive_failures > 0:
                        logger.info("Listeners recovered! Resetting failure counter (was: %s)", self.consecutive_failures)
                        self.consecutive_failures = 0
                
                # Perform resource cleanup periodically (avoid frequent gc.collect — it pauses all threads)
                if current_time - last_resource_cleanup > 1800:  # Every 30 minutes
                    self._cleanup_health_monitor_resources()
                    last_resource_cleanup = current_time
                
                # Sleep longer when user is idle — no listener restarts needed
                if user_is_idle:
                    time.sleep(120)
                else:
                    time.sleep(self.health_check_interval)
                
            except Exception as e:
                logger.error("Health monitor error: %s", e)
                # Reset counter on error
                health_check_counter = 0
                time.sleep(self.health_check_interval)

    def _cleanup_health_monitor_resources(self):
        """Clean up health monitor resources to prevent memory leaks"""
        try:
            logger.debug("Health monitor periodic cleanup tick")
        except Exception as e:
            logger.error("Error during health monitor resource cleanup: %s", e)

    def check_system_wake_state(self):
        """Check if system has woken from sleep (Windows only)"""
        try:
            current_time = time.time()
            idle_duration = IdleDuration().value()
            
            # CRITICAL FIX: If stop_listeners is True (sleep was detected) but system is responsive, restart listeners
            if self.stop_listeners:
                # Verify system is actually awake by checking if we can query foreground window
                system_is_responsive = False
                try:
                    import win32gui
                    hwnd = win32gui.GetForegroundWindow()
                    if hwnd:
                        system_is_responsive = True
                except ImportError:
                    pass
                except Exception:
                    pass
                
                # Also check with SystemWakeDetector
                try:
                    from ws_package.utils.system_wake_detector import get_system_wake_detector
                    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
                            
                            time_since_sleep = current_time - sleep_timestamp
                            
                            # If sleep was detected more than 3 minutes ago, assume awake
                            if time_since_sleep > 180:
                                system_is_responsive = True
                            elif 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:
                                    system_is_responsive = True
                except Exception:
                    pass
                
                if system_is_responsive:
                    logger.info("[WAKE] System marked as sleeping but is actually awake - restarting listeners")
                    self.stop_listeners = False
                    self.restart_listeners(force=True)
                    return
            
            # If system was idle for a long time but now has activity, it likely woke up.
            # Only restart when listeners were stopped for sleep — avoid spurious restarts
            # while the user is actively working (GetLastInputInfo idle < 10s is normal).
            if (
                self.stop_listeners
                and idle_duration < 10
                and (current_time - self.last_system_wake_time) > 300
            ):
                logger.info("System appears to have woken from sleep, restarting listeners...")
                self.last_system_wake_time = current_time
                self.stop_listeners = False
                self.restart_listeners(force=True)
                
        except Exception as e:
            logger.error("Error checking system wake state: %s", e)

    def verify_listeners_are_working(self):
        """Verify that listeners are actually receiving events with adaptive timeout"""
        try:
            logger.info("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 > 5:  # If already running for 5+ seconds
                    timeout = 30  # 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
            
            while (time.time() - start_verification) < timeout:
                # Check if we've received any events
                if self.mouse_events_received > initial_mouse_events:
                    self.mouse_listener_verified = True
                    logger.info("Mouse listener verified - received %s events", self.mouse_events_received)
                
                if self.keyboard_events_received > initial_keyboard_events:
                    self.keyboard_listener_verified = True
                    logger.info("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.info("Both listeners verified and receiving events")
                    return True
                
                time.sleep(0.5)
            
            # Timeout reached - check what we got
            if self.mouse_listener_verified:
                logger.warning("Mouse listener verified but keyboard listener not verified after %ss", timeout)
            elif self.keyboard_listener_verified:
                logger.warning("Keyboard listener verified but mouse listener not verified after %ss", timeout)
            else:
                logger.warning("Neither listener verified after %ss - Mouse events: %s, Keyboard events: %s",
                             timeout, self.mouse_events_received, self.keyboard_events_received)
                logger.warning("Listeners may not have proper permissions or may be blocked by antivirus")
            
            return self.mouse_listener_verified and self.keyboard_listener_verified
            
        except Exception as e:
            logger.error("Error verifying listeners: %s", e)
            return False
    
    def restart_listeners(self, force=False):
        """Safely restart all listeners"""
        with self._restart_lock:
            now = time.time()
            if not force and (now - self.last_restart_time) < MIN_RESTART_COOLDOWN_SECONDS:
                logger.debug(
                    "Skipping listener restart — cooldown (%.0fs since last restart)",
                    now - self.last_restart_time,
                )
                return
            try:
                logger.info("Restarting listeners...")
                
                # Stop current listeners
                self.stop_listeners = True
                time.sleep(2)  # Give threads time to stop
                
                # Reset state
                self.stop_listeners = False
                self.mouse_listener_active = False
                self.keyboard_listener_active = False
                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()
                
                # Restart listeners
                self.start_listeners()
                
                self.last_restart_time = time.time()
                logger.info("Listeners restarted successfully")
                
            except Exception as e:
                logger.error("Failed to restart listeners: %s", e)

    def start_input_poll_loop(self):
        """Poll mouse and consume validated keyboard down/up edges."""
        retry_count = 0
        max_retries = 5

        while retry_count < max_retries and not self.stop_listeners:
            try:
                logger.info(
                    "Starting input poll tracker (attempt %s/%s)...",
                    retry_count + 1,
                    max_retries,
                )

                if self.is_windows:
                    self.check_windows_permissions()

                self.mouse_listener = None
                self.keyboard_listener = None
                self.mouse_listener_active = True
                self.keyboard_listener_active = True
                self._seed_mouse_position()
                logger.info(
                    "Input poll active — mouse via GetCursorPos/GetAsyncKeyState; "
                    "keyboard via validated WH_KEYBOARD_LL edges"
                )

                check_counter = 0
                last_event_time = time.time()

                while not self.stop_listeners:
                    # Keyboard edges come from the LL hook, so they are drained on
                    # every tick and never inferred from mouse/OS-idle state.
                    self._poll_keyboard_activity()
                    if self._is_os_user_idle_for_polling():
                        time.sleep(self._mouse_poll_idle_interval)
                    else:
                        self._poll_mouse_position()
                        self._poll_mouse_buttons()
                        time.sleep(self._mouse_poll_interval)
                    check_counter += 1

                    if check_counter % 60 == 0:
                        logger.debug(
                            "[DEBUG] Input poll status - active=%s, mouse_events=%s, "
                            "keyboard_events=%s",
                            self.mouse_listener_active,
                            self.mouse_events_received,
                            self.keyboard_events_received,
                        )

                    if self.mouse_events_received > 0 or self.keyboard_events_received > 0:
                        last_event_time = time.time()

                self.mouse_listener_active = False
                self.keyboard_listener_active = False
                break

            except Exception as e:
                retry_count += 1
                self.mouse_listener_active = False
                self.keyboard_listener_active = False
                logger.error("Input poll tracker failed (attempt %s): %s", retry_count, e)

                if retry_count < max_retries:
                    wait_time = min(retry_count * 5, 30)
                    logger.info("Retrying input poll tracker in %s seconds...", wait_time)
                    time.sleep(wait_time)
                else:
                    logger.error("Input poll tracker failed after all retries")

    def check_windows_permissions(self):
        """Check if the application has necessary Windows permissions"""
        if not self.is_windows:
            return
            
        try:
            # Use GetTokenInformation(TokenElevation) rather than IsUserAnAdmin().
            # IsUserAnAdmin() calls SHCheckAccountType which can be blocked by UIPI
            # on Windows 11 when the process has medium integrity.
            is_admin = self._check_token_elevation()
            logger.info("Running with elevated privileges: %s", is_admin)

            if not is_admin:
                logger.warning("Not running as administrator - may have limited access to input monitoring")
            
            # Check accessibility permissions (Windows 10/11)
            if hasattr(ctypes.windll.user32, 'GetLastInputInfo'):
                logger.info("Accessibility API available")
            else:
                logger.warning("Accessibility API not available - input monitoring may be limited")
            
            # Try to get last input info as a test
            try:
                class LASTINPUTINFO(ctypes.Structure):
                    _fields_ = [("cbSize", ctypes.c_uint),
                              ("dwTime", ctypes.c_uint)]
                
                lastInputInfo = LASTINPUTINFO()
                lastInputInfo.cbSize = ctypes.sizeof(LASTINPUTINFO)
                if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo)):
                    logger.info("Successfully accessed GetLastInputInfo - permissions appear OK")
                else:
                    logger.warning("Failed to access GetLastInputInfo - may indicate permission issues")
            except Exception as perm_error:
                logger.warning("Could not test input API access: %s", perm_error)
                
        except Exception as e:
            logger.error("Error checking Windows permissions: %s", e)

    def _check_token_elevation(self) -> bool:
        """Return True if the current process token is elevated (admin).

        Uses GetTokenInformation(TokenElevation) which is UIPI-safe and works
        correctly on Windows 11 where IsUserAnAdmin() can be blocked by UAC
        integrity level restrictions.
        """
        try:
            TOKEN_QUERY = 0x0008
            TokenElevation = 20

            advapi32 = ctypes.WinDLL('advapi32', use_last_error=True)
            kernel32  = ctypes.WinDLL('kernel32',  use_last_error=True)

            token = ctypes.wintypes.HANDLE()
            if not advapi32.OpenProcessToken(
                kernel32.GetCurrentProcess(), TOKEN_QUERY, ctypes.byref(token)
            ):
                return False

            elevated = ctypes.wintypes.DWORD(0)
            ret_len  = ctypes.wintypes.DWORD(0)
            advapi32.GetTokenInformation(
                token, TokenElevation,
                ctypes.byref(elevated), ctypes.sizeof(elevated), ctypes.byref(ret_len)
            )
            kernel32.CloseHandle(token)
            return bool(elevated.value)
        except Exception as e:
            logger.debug("_check_token_elevation failed, falling back to IsUserAnAdmin: %s", e)
            try:
                return bool(ctypes.windll.shell32.IsUserAnAdmin())
            except Exception:
                return False

    def on_click(self, x, y, button, pressed):
        """Record a mouse click from poll-based detection."""
        try:
            if not pressed:
                return
            self.last_mouse_activity = time.time()
            self.mouse_events_received += 1
            if not self.mouse_listener_verified:
                self.mouse_listener_verified = True
                logger.debug("Mouse activity verified via poll")

            current_time = time.time()
            second_index = int(current_time % self.size) % self.array_size
            self.mouse_click_count += 1
            self.mouse_activity_array[second_index] = True
        except Exception as e:
            logger.error("Error in on_click: %s", e)

    def _get_cursor_position(self):
        """Return (x, y) from GetCursorPos or None on failure."""
        class POINT(ctypes.Structure):
            _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]

        pt = POINT()
        if not ctypes.windll.user32.GetCursorPos(ctypes.byref(pt)):
            return None
        return pt.x, pt.y

    def _seed_mouse_position(self):
        pos = self._get_cursor_position()
        if pos:
            self.prev_mouse_x, self.prev_mouse_y = pos

    def _poll_mouse_position(self):
        """Return True if meaningful mouse movement was detected this tick."""
        if self._is_os_user_idle_for_polling():
            return False
        pos = self._get_cursor_position()
        if pos:
            return self._process_mouse_move(pos[0], pos[1])
        return False

    def _poll_mouse_buttons(self):
        """Detect click down edges via GetAsyncKeyState. Returns True if a click was counted."""
        if self._is_os_user_idle_for_polling():
            return False
        pos = self._get_cursor_position()
        x, y = pos if pos else (0, 0)
        user32 = ctypes.windll.user32
        clicked = False
        for vk, button_name in (
            (_VK_LBUTTON, "left"),
            (_VK_RBUTTON, "right"),
            (_VK_MBUTTON, "middle"),
        ):
            pressed = bool(user32.GetAsyncKeyState(vk) & 0x8000)
            was_pressed = self._mouse_button_down.get(vk, False)
            if pressed and not was_pressed:
                self.on_click(x, y, button_name, True)
                clicked = True
            self._mouse_button_down[vk] = pressed
        return clicked

    def _poll_keyboard_activity(self):
        """
        Credit a keystroke only when a physically pressed key is released.

        Injected keys (SendInput, AutoHotkey, pyautogui, etc.) are excluded. A key
        that stays down contributes nothing — its repeats only feed the
        keyboard-only fudging detector — so holding keys accumulates no activity.
        """
        validator = self.hardware_validator
        if validator is None or not validator.is_enabled():
            return

        for event in validator.drain_windows_keyboard_events():
            vk_code = int(event.get("vk_code", 0))
            key_name = f"vk_{vk_code}"
            event_time = event.get("timestamp") or time.time()

            if event.get("injected"):
                validator.should_count_activity(False, "keyboard")
                continue

            if event.get("is_down"):
                if self.fudging_detector:
                    self.fudging_detector.record_keyboard_event(key_name, event_time)
                self.pressed_keys.add(vk_code)
                continue

            if not event.get("is_up"):
                continue

            if self.fudging_detector:
                self.fudging_detector.record_key_release(key_name)

            # A release with no matching physical press (hook started mid-hold, or
            # the press was injected) is not a keystroke we can credit.
            if vk_code not in self.pressed_keys:
                continue
            self.pressed_keys.discard(vk_code)

            self.keyboard_events_received += 1
            if not self.keyboard_listener_verified:
                self.keyboard_listener_verified = True
                logger.debug("Keyboard activity verified via physical key release")

            second_index = int(event_time % self.size) % self.array_size
            self.keyboard_count += 1
            self.keyboard_events.append(key_name)
            if len(self.keyboard_events) > MAX_EVENTS_LIMIT:
                self.keyboard_events = self.keyboard_events[-MAX_EVENTS_LIMIT:]
            self.keyboard_activity_array[second_index] = True
            self.last_keyboard_activity = event_time

    def _process_mouse_move(self, x, y):
        """Lightweight mouse-move handler driven by cursor polling."""
        try:
            current_time = time.time()
            dx = x - self.prev_mouse_x
            dy = y - self.prev_mouse_y
            move_distance_squared = dx * dx + dy * dy

            # Always resync the reference point, otherwise a stale origin turns
            # the next genuine move into an inflated jump.
            self.prev_mouse_x, self.prev_mouse_y = x, y

            if self._is_os_user_idle_for_polling():
                return False

            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.debug("Mouse activity verified via cursor poll")

            # The cursor did not really move. Typing keeps the OS non-idle, so
            # counting this tick would report keyboard-only work as mouse activity.
            if move_distance_squared < MOUSE_MIN_MOVE_THRESHOLD_SQUARED:
                return False

            if current_time - self._last_mouse_process_time < MOUSE_MOVE_THROTTLE_SECONDS:
                return True

            self._last_mouse_process_time = current_time
            self.last_mouse_activity = current_time
            second_index = int(current_time % self.size) % self.array_size
            self.mouse_activity_array[second_index] = True

            if move_distance_squared < MOUSE_ACTIVITY_THRESHOLD_SQUARED:
                self.mouse_move_count += 0.5
            else:
                self.mouse_move_count += 1

            return True

        except Exception as e:
            logger.error("Error in _process_mouse_move: %s", e)
            return False

    def on_mousemove(self, x, y):
        """Legacy entry point — delegates to polled move handler."""
        self._process_mouse_move(x, y)

    def stop_listening(self):
        """Stop all listeners and cleanup"""
        try:
            logger.info("Stopping all listeners...")
            self.stop_listeners = True
            
            # Wait for threads to finish
            if self.mouse_thread and self.mouse_thread.is_alive():
                self.mouse_thread.join(timeout=5)
            if self.keyboard_thread and self.keyboard_thread.is_alive():
                self.keyboard_thread.join(timeout=5)
            if self.health_monitor_thread and self.health_monitor_thread.is_alive():
                self.health_monitor_thread.join(timeout=5)
                
            logger.info("All listeners stopped")
            
        except Exception as e:
            logger.error("Error stopping listeners: %s", e)
    
    def handle_system_sleep(self):
        """
        Handle system sleep event - stop listeners and free resources.
        This is called when Windows system goes to sleep (lid down or sleep button).
        """
        try:
            logger.info("[SLEEP] System sleep detected - stopping listeners and freeing resources")
            
            # Stop all listeners immediately to free resources
            self.stop_listeners = True
            
            # Wait for threads to finish
            if self.mouse_thread and self.mouse_thread.is_alive():
                self.mouse_thread.join(timeout=2)
            if self.keyboard_thread and self.keyboard_thread.is_alive():
                self.keyboard_thread.join(timeout=2)
            
            # Stop listener objects
            if hasattr(self, 'mouse_listener') and self.mouse_listener:
                try:
                    self.mouse_listener.stop()
                except Exception:
                    pass
                self.mouse_listener = None
                self.mouse_listener_active = False
            
            if hasattr(self, 'keyboard_listener') and self.keyboard_listener:
                try:
                    self.keyboard_listener.stop()
                except Exception:
                    pass
                self.keyboard_listener = None
                self.keyboard_listener_active = False
            
            logger.info("[SLEEP] Listeners stopped and resources 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.
        """
        try:
            logger.info("[WAKE] System wake detected - restarting listeners")
            
            # 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()
            
            # Wait a moment for system to fully wake
            time.sleep(1.0)
            
            # Restart listeners
            logger.info("[WAKE] Restarting listeners...")
            self.start_listeners()
            
            logger.info("[WAKE] Listeners restarted successfully")
            
        except Exception as e:
            logger.error("[WAKE] Error handling system wake: %s", e, exc_info=True)

    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.debug("[DEBUG] Listener State - "
                       f"Mouse: poll_active={self.mouse_listener_active}, "
                       f"verified={self.mouse_listener_verified}, "
                       f"events={self.mouse_events_received}, "
                       f"thread_alive={self.mouse_thread.is_alive() if self.mouse_thread else False}, "
                       f"last_activity={time.time() - self.last_mouse_activity:.1f}s ago | "
                       f"Keyboard: running={keyboard_running}, active={self.keyboard_listener_active}, "
                       f"verified={self.keyboard_listener_verified}, "
                       f"events={self.keyboard_events_received}, "
                       f"thread_alive={self.keyboard_thread.is_alive() if self.keyboard_thread else False}, "
                       f"last_activity={time.time() - self.last_keyboard_activity:.1f}s ago")
        except Exception as e:
            logger.error("[DEBUG] Error logging listener state: %s", e, exc_info=True)
    
    def get_listener_status(self):
        """Get current status of all listeners"""
        return {
            "mouse_listener_active": self.mouse_listener_active,
            "keyboard_listener_active": self.keyboard_listener_active,
            "mouse_listener_verified": self.mouse_listener_verified,
            "keyboard_listener_verified": self.keyboard_listener_verified,
            "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() - self.last_mouse_activity,
            "last_keyboard_activity": time.time() - self.last_keyboard_activity,
            "mouse_events_received": self.mouse_events_received,
            "keyboard_events_received": self.keyboard_events_received
        }

    async def get_activity_level(self, interval=600):
        """Get activity level with improved error handling"""
        try:
            # CRITICAL FIX: Don't change self.size when interval parameter changes!
            # The arrays are circular buffers that use self.size for modulo calculation.
            # Changing self.size causes data to be written to different positions, corrupting the buffer.
            # Instead, keep self.size fixed at the maximum interval needed (typically 600 seconds),
            # and calculate activity from the most recent 'interval' seconds of the circular buffer.
            
            # Ensure arrays are large enough (expand if needed, but preserve existing data)
            if interval > self.array_size:
                old_size = self.array_size
                self.array_size = max(interval, 999)
                # Extend arrays with False values (preserve existing data)
                self.keyboard_activity_array.extend([False] * (self.array_size - old_size))
                self.mouse_activity_array.extend([False] * (self.array_size - old_size))
                logger.info("Extended activity arrays from %s to %s", old_size, self.array_size)
            
            # Keep self.size fixed - don't change it based on interval parameter
            # self.size is used for circular buffer modulo in event handlers
            # If it needs to be larger, update it, but don't shrink it
            if interval > self.size:
                self.size = interval
                logger.info("Updated self.size to %s to accommodate larger interval", self.size)
            
            # Calculate activity counts from the most recent 'interval' seconds of the circular buffer
            # The circular buffer uses current_time % self.size, so we need to read the last 'interval' indices
            # that would have been written based on the current time
            current_time = int(time.time())
            # Get the indices for the last 'interval' seconds
            indices_to_check = []
            for i in range(interval):
                # Calculate which array index corresponds to (current_time - i) seconds ago
                time_ago = current_time - i
                idx = int(time_ago % self.size) % self.array_size
                indices_to_check.append(idx)
            
            keyboard_activity_count = sum(1 for idx in indices_to_check 
                                       if self.keyboard_activity_array[idx])
            mouse_activity_count = sum(1 for idx in indices_to_check 
                                     if self.mouse_activity_array[idx])

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

            # Calculate activity percentage (sum of keyboard and mouse, capped at 100%)
            # Note: "average" is just the field name - it actually contains the sum, not an average
            # This ensures consistency: average = keyboard + mouse (max 100%)
            activity_percentage = min(keyboard_activity + mouse_activity, 100.0)

            system_idle_seconds = self._get_os_idle_seconds()
            system_idle_over_threshold = (
                system_idle_seconds > int(self.idle_threshold_seconds)
            )
            if system_idle_over_threshold:
                if keyboard_activity > 0 or mouse_activity > 0:
                    logger.info(
                        "System idle for %ss (> %ss threshold) — reporting zero activity "
                        "(was keyboard=%s%%, mouse=%s%%; cursor poll ignored).",
                        int(system_idle_seconds),
                        self.idle_threshold_seconds,
                        keyboard_activity,
                        mouse_activity,
                    )
                else:
                    logger.info(
                        "System has been idle for more than the threshold duration "
                        "(%ss > %ss). Potential sleep or user inactivity detected.",
                        int(system_idle_seconds),
                        self.idle_threshold_seconds,
                    )
                keyboard_activity = 0.0
                mouse_activity = 0.0
                activity_percentage = 0.0
            
            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)
            fudging_detected = False
            fudging_type = ""
            fudging_score = 0.0
            
            if getattr(self, "fudging_detector", None) and not system_idle_over_threshold:
                try:
                    # Check for fudging patterns
                    fudging_detected, fudging_type, fudging_score = self.fudging_detector.check_fudging()
                    
                    if fudging_detected:
                        # Keyboard-only fudging: never reduce mouse activity and never
                        # use mouse movement as evidence of keyboard fudging.
                        reduction_factor = self.fudging_detector.get_reduction_factor()
                        
                        keyboard_activity = keyboard_activity * reduction_factor
                        
                        # Preserve genuine mouse activity in the combined percentage.
                        activity_percentage = min(keyboard_activity + mouse_activity, 100.0)
                        
                        # 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 Unchanged: %s%%", 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 getattr(self, "fudging_report_service", None) and (
                            time.time() - getattr(self, "_last_fudging_report_time", 0)
                        ) > 86400:
                            try:
                                # Use thread pool for non-blocking API call (doesn't block activity calculation)
                                import threading
                                def _report_async():
                                    try:
                                        # Note: report_fudging is synchronous but runs in background thread
                                        # This prevents blocking the main activity calculation
                                        # Get fudging pattern information for API
                                        fudging_info = self.fudging_detector.get_fudging_info()
                                        fudged_duration = fudging_info.get('fudged_duration_seconds', 0.0)
                                        
                                        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 / 60.0,
                                                "interval_seconds": interval
                                            }
                                        )
                                    except Exception as e:
                                        logger.debug("Fudging report error in thread (ignored): %s", e)
                                
                                # Run in background thread (daemon, won't block shutdown)
                                report_thread = threading.Thread(target=_report_async, daemon=True, name="FudgingReport")
                                report_thread.start()
                                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
            
            # Log activity summary - DISABLED to reduce logging overhead
            # logger.info(f"Activity Summary - Mouse: {mouse_activity:.1f}%, "
            #            f"Keyboard: {keyboard_activity:.1f}%, Total: {activity_percentage:.1f}%")  # DISABLED to reduce logging overhead
            # logger.info(f"Counts - Mouse moves: {self.mouse_move_count}, "
            #            f"Clicks: {self.mouse_click_count}, Keys: {self.keyboard_count}")  # DISABLED to reduce logging overhead
            
            activityLevel = {
                "mouse": str(mouse_activity), 
                "average": str(activity_percentage), 
                "keyboard": str(keyboard_activity)
            }

            # Create activity log
            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,
                "listener_status": self.get_listener_status()
            }

            # Store current values before reset
            keyboard_count = self.keyboard_count
            keyboard_events = self.keyboard_events.copy()
            mouse_click_count = self.mouse_click_count
            mouse_events = self.mouse_events.copy()

            # Reset activity arrays and counters
            self.keyboard_activity_array = [False] * self.array_size
            self.mouse_activity_array = [False] * self.array_size
            self.scroll_count = 0
            self.keyboard_count = 0
            self.mouse_move_count = 0
            self.mouse_click_count = 0
            self.keyboard_events = []
            self.mouse_events = []

            # Create activity entity with proper memory management
            activity_entity = self._create_activity_entity(
                keyboard_count, 
                keyboard_events,
                mouse_click_count,
                mouse_events
            )
            
            return activityLevel, activity_entity
            
        except Exception as e:
            logger.error("Error in get_activity_level: %s", e)
            # Return default values on error
            return {"mouse": "0", "average": "0", "keyboard": "0"}, None

    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()
            )