"""
Fudging Detector - Detects suspicious activity patterns that indicate data fudging

This module detects patterns like:
- Regular intervals (same key at exact intervals)
- Key repetition (same key pressed many times)
- Held keys (weight on keyboard)

Fail-safe design: If detection fails, returns no reduction (activity unchanged)
"""

import time
from typing import Optional, Dict, List, Tuple
from collections import deque
from ws_package.log.writer import LogWriter
from ws_package.configs import config

logger = LogWriter.file(__name__)

# Constants for fudging detection
MAX_KEYBOARD_EVENTS_HISTORY = 100  # Last N keyboard events to track
MAX_MOUSE_EVENTS_HISTORY = 100  # Last N mouse events to track

# Throttling constants
FUDGING_CHECK_INTERVAL_SECONDS = 5.0  # Only check every N seconds

# Detection thresholds
REGULAR_INTERVAL_VARIANCE_THRESHOLD = 0.2  # Variance threshold for regular intervals (for longer durations)
REGULAR_INTERVAL_VARIANCE_THRESHOLD_STRICT = 0.1  # Stricter variance threshold for short durations
REGULAR_INTERVAL_MIN_EVENTS = 15  # Minimum events for regular interval detection (increased from 5)
REGULAR_INTERVAL_MIN_DURATION_SECONDS = 10.0  # Minimum duration for regular interval detection
REGULAR_INTERVAL_SHORT_DURATION_SECONDS = 10.0  # Threshold for "short" duration (use stricter criteria)
KEY_REPETITION_THRESHOLD = 18  # Same key pressed more than N times in detection window (increased to reduce false positives)
KEY_REPETITION_RATE_THRESHOLD = 6.0  # Minimum presses per second to flag as fudging (rate check)
HELD_KEY_THRESHOLD_SECONDS = 5.0  # Key held for more than N seconds
UNIFORM_MOUSE_VARIANCE_THRESHOLD = 0.05  # Mouse movement variance threshold (stricter)
UNIFORM_MOUSE_MIN_SAMPLES = 15  # Minimum samples for uniform mouse detection
UNIFORM_MOUSE_MIN_MEAN_PIXELS = 10.0  # Minimum mean distance in pixels for uniform detection

# Recovery and reduction settings
RECOVERY_RATE = 0.05  # Increase by 5% per check when fudging stops
REDUCTION_RATE = 0.1  # Decrease by 10% per detected fudging event
MIN_REDUCTION_FACTOR = 0.1  # Minimum 10% activity (90% reduction max)
NO_REDUCTION_FACTOR = 1.0  # 100% activity (no reduction)

# Event recording throttling
HELD_KEY_BUFFER_UPDATE_INTERVAL = 10  # Update buffer every Nth repeat for held keys
KEYBOARD_RECORD_THROTTLE_SECONDS = 0.1  # Max 10 records per second (100ms)
MOUSE_MOVEMENT_MIN_PIXELS = 5  # Minimum mouse movement distance for fudging detection
MOUSE_RECORD_THROTTLE_SECONDS = 0.2  # Throttle mouse event recording (5 records/sec max)
HELD_KEY_STALE_RESET_SECONDS = 1.5  # Consider key released if no repeats for this long

# Pattern detection time windows
PATTERN_CONTINUATION_WINDOW_SECONDS = 2.0  # Events within last N seconds indicate pattern continuation
PATTERN_DETECTION_WINDOW_SECONDS = 3.0  # Look for patterns within last N seconds
PATTERN_RESET_WINDOW_SECONDS = 3.0  # Reset pattern if no events in last N seconds

# Key repeat detection thresholds
KEY_REPEAT_TYPICAL_INTERVAL_SECONDS = 0.05  # Typical key repeat interval (50-100ms)
KEY_REPEAT_INITIAL_DELAY_SECONDS = 0.5  # Initial repeat delay can be up to 0.5-0.7s
KEY_REPEAT_AVG_THRESHOLD_SECONDS = 0.2  # Average interval threshold for held key detection
KEY_REPEAT_MAX_THRESHOLD_SECONDS = 0.8  # Max interval threshold (allowing for initial delay)
HELD_KEY_SHORT_DURATION_SECONDS = 3.0  # Short duration threshold for held key
HELD_KEY_SHORT_INTERVAL_SECONDS = 0.5  # Short interval threshold for held key detection

# Mouse movement analysis thresholds
UNIFORM_MOUSE_MIN_INTERVAL_SECONDS = 0.1  # Minimum interval for uniform mouse detection
COEFFICIENT_OF_VARIATION_THRESHOLD = 0.1  # Low coefficient of variation for uniform detection


class FudgingDetector:
    """
    Detects keyboard-only fudging patterns.
    
    Fail-safe: All errors result in no reduction (activity unchanged)
    """
    
    def __init__(self):
        # Use dtype='bool' so configparser correctly converts "false"→False, "true"→True.
        # Falls back to False when the key is absent (config() returns None).
        self._enabled = config('fudging_detection_enabled', 'ActivityMonitor', dtype='bool') or False
        self._reduction_enabled = config('fudging_reduction_enabled', 'ActivityMonitor', dtype='bool') or False
        
        # Event history tracking (circular buffers - limited size for memory efficiency)
        self._keyboard_events: deque = deque(maxlen=MAX_KEYBOARD_EVENTS_HISTORY)
        self._mouse_events: deque = deque(maxlen=MAX_MOUSE_EVENTS_HISTORY)
        
        # Long-duration held key tracking (for detecting keys held for hours)
        self._current_held_key = None
        self._current_held_key_start = 0
        self._current_held_key_count = 0
        self._current_held_key_last_update = 0
        
        # Fudging state
        self._reduction_factor = NO_REDUCTION_FACTOR  # Start at 100% (no reduction)
        self._fudging_detected = False
        self._last_fudging_check = time.time()
        self._fudging_start_time = None
        self._fudging_pattern_start_time = None  # When the current pattern started
        self._fudging_pattern_key = None  # Which key is being fudged
        self._fudging_pattern_type = None  # Type of pattern (regular_intervals, held_key, etc.)
        
        # Throttling to reduce CPU load
        self._check_interval = FUDGING_CHECK_INTERVAL_SECONDS
        self._last_check_time = 0   # Timestamp of last full check
        self._last_record_time = 0  # Timestamp of last event recording (for throttling)
        
        # Detection thresholds (configurable)
        self._regular_interval_threshold = REGULAR_INTERVAL_VARIANCE_THRESHOLD
        self._key_repetition_threshold = KEY_REPETITION_THRESHOLD
        self._key_repetition_rate_threshold = KEY_REPETITION_RATE_THRESHOLD
        self._held_key_threshold = HELD_KEY_THRESHOLD_SECONDS
        self._uniform_mouse_threshold = UNIFORM_MOUSE_VARIANCE_THRESHOLD
        self._uniform_mouse_min_samples = UNIFORM_MOUSE_MIN_SAMPLES
        self._uniform_mouse_min_mean = UNIFORM_MOUSE_MIN_MEAN_PIXELS
        
        # Recovery settings
        self._recovery_rate = RECOVERY_RATE
        self._reduction_rate = REDUCTION_RATE
        self._min_reduction_factor = MIN_REDUCTION_FACTOR
        
        # Thread safety (lightweight lock)
        self._lock = None
        try:
            import threading
            self._lock = threading.Lock()
        except Exception:
            pass  # If threading not available, continue without lock
    
    def _safe_operation(self, func, default_return):
        """Execute function with error handling - fail-safe"""
        try:
            if self._lock:
                with self._lock:
                    return func()
            else:
                return func()
        except Exception as e:
            logger.debug("Fudging detection error (fail-safe): %s", e)
            return default_return
    
    def _reset_current_held_key(self):
        """Reset long-duration held key tracking state."""
        self._current_held_key = None
        self._current_held_key_start = 0
        self._current_held_key_count = 0
        self._current_held_key_last_update = 0
    
    def record_keyboard_event(self, key: str, timestamp: Optional[float] = None):
        """Record a keyboard event for pattern analysis (lightweight, non-blocking)"""
        if not self._enabled:
            return
        
        current_time = timestamp or time.time()
        key_str = str(key)
        
        def _record():
            # 1. Efficient Long-Duration Tracking (O(1))
            # This handles hours-long weights without filling memory
            if self._current_held_key == key_str:
                # Same key continuing - just update stats
                self._current_held_key_count += 1
                self._current_held_key_last_update = current_time
                
                # Optimization: Don't flood buffer with same key repeats
                # Only append to buffer occasionally to maintain "aliveness" in buffer
                # without consuming all slots with one key
                if self._current_held_key_count % HELD_KEY_BUFFER_UPDATE_INTERVAL != 0:
                    return
            else:
                # New key or key changed
                self._current_held_key = key_str
                self._current_held_key_start = current_time
                self._current_held_key_count = 1
                self._current_held_key_last_update = current_time
            
            # 2. Buffer Recording (Throttled)
            # Throttle buffer appends to reduce CPU/memory churn
            if current_time - self._last_record_time >= KEYBOARD_RECORD_THROTTLE_SECONDS:
                self._keyboard_events.append((key_str, current_time))
                self._last_record_time = current_time
        
        self._safe_operation(_record, None)

    def record_key_release(self, key: str):
        """Handle key release to reset held-key tracking."""
        if not self._enabled:
            return
        
        key_str = str(key)

        def _release():
            if self._current_held_key == key_str:
                self._reset_current_held_key()
        
        self._safe_operation(_release, None)
    
    def record_mouse_event(self, x: int, y: int, distance: float, timestamp: Optional[float] = None):
        """Record a mouse event for pattern analysis (lightweight, non-blocking)"""
        if not self._enabled:
            return
        
        # Only record movements >= threshold for fudging detection
        # This filters out legitimate micro-adjustments and reduces noise
        # Fudging tools typically use larger movements (10-50px)
        if distance < MOUSE_MOVEMENT_MIN_PIXELS:
            return
        
        # Throttle: Only record if enough time has passed (reduces CPU load)
        current_time = time.time()
        if current_time - self._last_check_time < MOUSE_RECORD_THROTTLE_SECONDS:
            return
        
        def _record():
            event_time = timestamp or time.time()
            # Use simple tuple instead of dict for memory efficiency (only store distance)
            self._mouse_events.append((distance, event_time))
            self._last_check_time = event_time  # Update throttle timestamp
        
        self._safe_operation(_record, None)
    
    def _detect_regular_intervals(self) -> Tuple[bool, str]:
        """Detect if keyboard events occur at very regular intervals (optimized with stricter thresholds and minimal CPU/memory usage)"""
        try:
            # Early exit: not enough events (avoids unnecessary processing)
            if len(self._keyboard_events) < REGULAR_INTERVAL_MIN_EVENTS:
                return False, ""
            
            current_time = time.time()
            
            # Group by key to detect which specific key has regular intervals
            # Limit to most recent keys to reduce processing (max 5 keys checked per iteration)
            key_groups = {}
            key_count = 0
            MAX_KEYS_TO_CHECK = 5  # Limit CPU usage by checking only most frequent keys
            
            # Process events in reverse (most recent first) and limit key groups
            for key, timestamp in reversed(self._keyboard_events):
                if key not in key_groups:
                    key_groups[key] = []
                    key_count += 1
                    if key_count > MAX_KEYS_TO_CHECK:
                        break  # Only check most recent/frequent keys
                key_groups[key].append(timestamp)
            
            # Check each key for regular intervals (early exits for performance)
            for key, timestamps in key_groups.items():
                # Early exit: require minimum number of events (increased threshold)
                if len(timestamps) < REGULAR_INTERVAL_MIN_EVENTS:
                    continue
                
                # Sort timestamps to get chronological order (only if we have enough)
                sorted_timestamps = sorted(timestamps)
                
                # Early exit: check duration first (cheapest check)
                first_event_time = sorted_timestamps[0]
                last_event_time = sorted_timestamps[-1]
                actual_duration = last_event_time - first_event_time
                
                # Early exit: minimum duration requirement (avoids expensive variance calculation)
                if actual_duration < REGULAR_INTERVAL_MIN_DURATION_SECONDS:
                    continue  # Pattern too short, likely normal usage
                
                total_presses = len(sorted_timestamps)
                
                # Apply stricter criteria based on duration (early exit for short durations)
                is_short_duration = actual_duration < REGULAR_INTERVAL_SHORT_DURATION_SECONDS
                if is_short_duration:
                    # Short duration: require more events (early exit before variance calculation)
                    min_events_for_short = REGULAR_INTERVAL_MIN_EVENTS + 5  # Require 20+ events for short durations
                    if total_presses < min_events_for_short:
                        continue  # Not enough events for short duration pattern
                
                # Calculate intervals for this specific key
                # Use last 10 events for variance calculation (performance optimization)
                recent_timestamps = sorted_timestamps[-10:] if len(sorted_timestamps) > 10 else sorted_timestamps
                intervals = [recent_timestamps[i+1] - recent_timestamps[i] for i in range(len(recent_timestamps)-1)]
                
                # Early exit: need minimum intervals for variance calculation
                if len(intervals) < 3:
                    continue
                
                # Check if intervals are too regular (low variance) - optimized calculation
                if len(intervals) > 1:
                    # Use simple variance calculation (faster than statistics.variance, O(n))
                    mean = sum(intervals) / len(intervals)
                    variance = sum((x - mean) ** 2 for x in intervals) / len(intervals)
                    
                    # Determine variance threshold based on duration
                    if is_short_duration:
                        variance_threshold = REGULAR_INTERVAL_VARIANCE_THRESHOLD_STRICT
                    else:
                        variance_threshold = self._regular_interval_threshold
                    
                    # Low variance + consistent mean + sufficient duration = mechanical pattern
                    if variance < variance_threshold and mean > UNIFORM_MOUSE_MIN_INTERVAL_SECONDS:
                        # Check if this is continuation of same pattern (same key, recent events)
                        time_since_last_event = current_time - last_event_time
                        is_continuation = (
                            self._fudging_pattern_key == key and 
                            self._fudging_pattern_type == "regular_intervals" and
                            self._fudging_pattern_start_time is not None and
                            time_since_last_event < PATTERN_CONTINUATION_WINDOW_SECONDS
                        )
                        
                        if is_continuation:
                            # Continue tracking - use actual event duration, not time since pattern start
                            # This prevents inflating duration when pattern was detected earlier
                            pattern_duration = actual_duration
                            # Update start time to first event in current buffer if it's earlier
                            if first_event_time < self._fudging_pattern_start_time:
                                self._fudging_pattern_start_time = first_event_time
                        else:
                            # New pattern detected - start tracking from first event
                            self._fudging_pattern_start_time = first_event_time
                            self._fudging_pattern_key = key
                            self._fudging_pattern_type = "regular_intervals"
                            pattern_duration = actual_duration
                        
                        return True, f"Regular intervals detected: Key '{key}' pressed {total_presses} times at {mean:.2f}s intervals (variance: {variance:.4f}, pattern duration: {pattern_duration:.1f}s, sample duration: {actual_duration:.1f}s)"
            
            # Reset pattern tracking if no regular intervals found for current key
            if self._fudging_pattern_type == "regular_intervals":
                # Check if pattern stopped (no recent events for this key)
                if self._fudging_pattern_key:
                    recent_events_for_key = [e for e in self._keyboard_events 
                                            if e[0] == self._fudging_pattern_key and 
                                            current_time - e[1] < PATTERN_DETECTION_WINDOW_SECONDS]
                    if len(recent_events_for_key) == 0:
                        # Pattern stopped - reset tracking
                        self._fudging_pattern_start_time = None
                        self._fudging_pattern_key = None
                        self._fudging_pattern_type = None
            
            return False, ""
        except Exception as e:
            logger.debug("Regular intervals detection error: %s", e)
            return False, ""
    
    def _detect_key_repetition(self) -> Tuple[bool, str]:
        """Detect if same key is pressed many times (optimized with early exits and limited processing)"""
        try:
            # Early exit: not enough events (avoids unnecessary processing)
            if len(self._keyboard_events) < self._key_repetition_threshold:
                return False, ""
            
            # Check last 30 seconds of events (optimized - events are tuples)
            current_time = time.time()
            recent_events = []
            recent_event_count = 0
            MAX_RECENT_EVENTS_TO_CHECK = 50  # Limit CPU usage by checking only recent events
            
            # Process in reverse (most recent first) and limit to recent events
            for e in reversed(self._keyboard_events):
                time_diff = current_time - e[1]
                if time_diff <= PATTERN_DETECTION_WINDOW_SECONDS:
                    recent_events.append((e[0], e[1]))
                    recent_event_count += 1
                    # Early exit: stop checking if we've processed enough recent events
                    if recent_event_count >= MAX_RECENT_EVENTS_TO_CHECK:
                        break
            
            # Early exit: not enough recent events
            if len(recent_events) < self._key_repetition_threshold:
                return False, ""
            
            # Count key occurrences (optimized, early exit on first match)
            key_counts = {}
            for key, timestamp in recent_events:
                if key not in key_counts:
                    key_counts[key] = {'count': 0, 'first': timestamp, 'last': timestamp}
                key_counts[key]['count'] += 1
                key_counts[key]['last'] = timestamp
            
            # Check if any key appears too many times AND at suspicious rate (early exit on first match)
            for key, data in key_counts.items():
                count = data['count']
                if count >= self._key_repetition_threshold:
                    # Fix duration calculation - handle negative values and ensure proper ordering
                    duration = abs(data['last'] - data['first'])
                    if duration > 0:
                        rate = count / duration
                        # Only flag if BOTH count and rate exceed thresholds (reduces false positives)
                        if rate >= self._key_repetition_rate_threshold:
                            return True, f"Key repetition detected: Key '{key}' pressed {count} times in {duration:.1f}s (rate: {rate:.1f}/sec, count threshold: {self._key_repetition_threshold}, rate threshold: {self._key_repetition_rate_threshold})"
                    else:
                        # Duration is 0 or negative - likely a timestamp issue, skip this detection
                        logger.debug("Key repetition detection skipped for '%s': invalid duration (%ss)", key, duration)
            
            return False, ""
        except Exception as e:
            logger.debug("Key repetition detection error: %s", e)
            return False, ""
    
    def _detect_held_key(self) -> Tuple[bool, str]:
        """Detect if a key is held down for too long (weight on keyboard) - optimized with early exits"""
        try:
            # Early exit: not enough events (avoids unnecessary processing)
            if len(self._keyboard_events) < 3:
                return False, ""
            
            current_time = time.time()
            
            # Check long-duration held key first (O(1) check, most efficient, no memory allocation)
            if self._current_held_key and self._current_held_key_start > 0:
                time_since_last_update = current_time - self._current_held_key_last_update
                if time_since_last_update > HELD_KEY_STALE_RESET_SECONDS:
                    # No repeats for a while - treat as released
                    self._reset_current_held_key()
                else:
                    duration = current_time - self._current_held_key_start
                    if duration >= self._held_key_threshold:
                        self._fudging_pattern_type = "held_key"
                        self._fudging_pattern_key = self._current_held_key
                        self._fudging_pattern_start_time = self._current_held_key_start
                        return True, f"Held key detected: Key '{self._current_held_key}' held for {duration:.1f}s (count: {self._current_held_key_count})"
            
            # Also check buffer for shorter holds (for cases where long-duration tracking missed it)
            # Limit to most recent keys to reduce CPU usage (max 3 keys checked)
            key_groups = {}
            key_count = 0
            MAX_KEYS_TO_CHECK = 3  # Limit CPU usage
            
            # Process events in reverse (most recent first) and limit key groups
            for key, timestamp in reversed(self._keyboard_events):
                if key not in key_groups:
                    key_groups[key] = []
                    key_count += 1
                    if key_count > MAX_KEYS_TO_CHECK:
                        break  # Only check most recent keys
                key_groups[key].append(timestamp)
            
            for key, timestamps in key_groups.items():
                # Early exit: need minimum events
                if len(timestamps) < 5:
                    continue
                
                sorted_timestamps = sorted(timestamps)
                first_time = sorted_timestamps[0]
                last_time = sorted_timestamps[-1]
                duration = last_time - first_time
                
                # Early exit: check duration threshold first (cheapest check)
                if duration < HELD_KEY_SHORT_DURATION_SECONDS:
                    continue  # Too short, skip expensive interval calculation
                
                # Check if intervals suggest held key
                # Use only recent intervals for calculation (performance)
                recent_timestamps = sorted_timestamps[-10:] if len(sorted_timestamps) > 10 else sorted_timestamps
                intervals = [recent_timestamps[i+1] - recent_timestamps[i] for i in range(len(recent_timestamps)-1)]
                if intervals:
                    # Check if intervals are consistently short (indicating held key)
                    avg_interval = sum(intervals) / len(intervals)
                    max_interval = max(intervals)
                    
                    # Key repeats typically occur every KEY_REPEAT_TYPICAL_INTERVAL_SECONDS when held
                    # Initial repeat delay can be up to KEY_REPEAT_INITIAL_DELAY_SECONDS
                    # If average interval is below threshold and max is below threshold (allowing for initial delay), key is being held
                    if avg_interval < KEY_REPEAT_AVG_THRESHOLD_SECONDS and max_interval < KEY_REPEAT_MAX_THRESHOLD_SECONDS:
                        # Also check if key is still being held (last event was recent)
                        time_since_last = current_time - last_time
                        key_count = len(timestamps)
                        
                        # For long duration (5+ seconds): require 5+ presses (current threshold)
                        # For short duration (3-5 seconds): require 18+ presses (to match key repetition threshold and reduce false positives)
                        if duration >= self._held_key_threshold:
                            # Long duration: keep current threshold (5+ presses)
                            if key_count >= 5:
                                self._fudging_pattern_type = "held_key"
                                self._fudging_pattern_key = key
                                self._fudging_pattern_start_time = first_time
                                return True, f"Held key detected: Key '{key}' held for {duration:.1f}s (intervals: avg={avg_interval:.3f}s, max={max_interval:.3f}s, count: {key_count})"
                        elif duration >= HELD_KEY_SHORT_DURATION_SECONDS and time_since_last < HELD_KEY_SHORT_INTERVAL_SECONDS:
                            # Short duration: require 18+ presses to match key repetition threshold (reduces false positives)
                            if key_count >= KEY_REPETITION_THRESHOLD:
                                self._fudging_pattern_type = "held_key"
                                self._fudging_pattern_key = key
                                self._fudging_pattern_start_time = first_time
                                return True, f"Held key detected: Key '{key}' held for {duration:.1f}s (intervals: avg={avg_interval:.3f}s, max={max_interval:.3f}s, count: {key_count})"
            
            return False, ""
        except Exception as e:
            logger.debug("Held key detection error: %s", e)
            return False, ""
    
    def _detect_uniform_mouse(self) -> Tuple[bool, str]:
        """
        Detect if mouse movements are too uniform (mechanical) - optimized with strict thresholds and early exits
        
        Best practices:
        - Requires many samples (15+) for statistical significance
        - Very strict variance threshold (0.05) - only detects near-perfect uniformity
        - Requires larger mean distance (10px+) - filters out micro-movements
        - Checks coefficient of variation (CV) for better normalization
        """
        try:
            # Early exit: not enough events (avoids unnecessary processing)
            if len(self._mouse_events) < self._uniform_mouse_min_samples:
                return False, ""
            
            # Extract distances from recent events only (limit to last 20 for performance)
            # Events are tuples: (distance, timestamp)
            recent_events = list(self._mouse_events)[-20:] if len(self._mouse_events) > 20 else list(self._mouse_events)
            distances = [e[0] for e in recent_events if e[0] >= MOUSE_MOVEMENT_MIN_PIXELS]  # Only movements >= 5px
            
            # Early exit: need minimum samples
            if len(distances) < self._uniform_mouse_min_samples:
                return False, ""
            
            # Check if distances are too uniform (low variance) - optimized calculation
            if len(distances) > 1:
                # Use simple variance calculation (faster than statistics.variance, O(n))
                mean = sum(distances) / len(distances)
                
                # Early exit: check mean threshold first (cheapest check)
                if mean < self._uniform_mouse_min_mean:
                    return False, ""  # Mean too small, skip expensive variance calculation
                
                variance = sum((x - mean) ** 2 for x in distances) / len(distances)
                
                # Early exit: check variance threshold before CV calculation
                if variance >= self._uniform_mouse_threshold:
                    return False, ""  # Variance too high, not uniform
                
                # Calculate coefficient of variation (CV) for better normalization
                # CV = std_dev / mean = sqrt(variance) / mean
                # This accounts for scale - a variance of 0.1 means different things for 5px vs 50px movements
                if mean > 0:
                    std_dev = variance ** 0.5
                    coefficient_of_variation = std_dev / mean
                else:
                    coefficient_of_variation = float('inf')
                
                # Stricter detection criteria (all must pass):
                # 1. Very low variance (< 0.05) - near-perfect uniformity
                # 2. Large mean distance (>= 10px) - significant movements
                # 3. Low coefficient of variation (< 0.1) - normalized consistency
                # 4. Minimum samples for statistical significance
                if (coefficient_of_variation < COEFFICIENT_OF_VARIATION_THRESHOLD and
                    len(distances) >= self._uniform_mouse_min_samples):
                    return True, f"Uniform mouse movement detected (variance: {variance:.4f}, mean: {mean:.1f}px, CV: {coefficient_of_variation:.3f}, samples: {len(distances)})"
            
            return False, ""
        except Exception as e:
            logger.debug("Uniform mouse detection error: %s", e)
            return False, ""
    
    def check_fudging(self) -> Tuple[bool, str, float]:
        """
        Check for fudging patterns and return detection result.
        
        Throttled to reduce CPU load - only checks every N seconds.
        
        Returns:
            (is_fudging, fudging_type, fudging_score)
            - is_fudging: True if fudging detected
            - fudging_type: Description of fudging pattern
            - fudging_score: Confidence score (0.0 to 1.0)
        """
        if not self._enabled:
            return False, "", 0.0
        
        # Throttle checks to reduce CPU load
        current_time = time.time()
        if current_time - self._last_check_time < self._check_interval:
            # Return cached result if recently checked
            # BUT: Only return cached detection if there's an active pattern
            if self._fudging_detected:
                # Check if there's actually an active pattern (not just stale flag)
                has_active_pattern = (
                    self._fudging_pattern_start_time is not None and
                    self._fudging_pattern_key is not None and
                    self._fudging_pattern_type is not None
                )
                
                if has_active_pattern:
                    return True, "Fudging detected (cached)", 0.8
                else:
                    # Pattern was reset but flag still True - this is recovery phase
                    # Don't return cached detection, let recovery continue
                    # The recovery mechanism will handle resetting the flag
                    return False, "", 0.0
            return False, "", 0.0
        
        self._last_check_time = current_time
        
        def _check():
            # Keyboard-only: mouse movement is not evidence of fudging.
            min_keyboard = 3
            
            keyboard_count = len(self._keyboard_events)
            
            # Early exit: not enough keyboard data for pattern detection.
            if keyboard_count < min_keyboard:
                # Not enough data, but still check recovery (lightweight operation)
                if self._fudging_detected and self._reduction_enabled:
                    self._reduction_factor = min(1.0, self._reduction_factor + self._recovery_rate)
                    if self._reduction_factor >= 1.0:
                        self._fudging_detected = False
                        self._fudging_start_time = None
                return False, "", 0.0
            
            # Check patterns (only if enough data, with early exit on first detection)
            # Prioritize keyboard patterns (more reliable indicators, checked first)
            # Use lazy evaluation - stop checking once a pattern is found
            if keyboard_count >= 3:
                # Check regular intervals first (most common fudging pattern)
                is_detected, description = self._detect_regular_intervals()
                if is_detected:
                    score = 0.8
                    if self._reduction_enabled:
                        self._reduction_factor = max(self._min_reduction_factor, self._reduction_factor - self._reduction_rate)
                    self._fudging_detected = True
                    if self._fudging_start_time is None:
                        self._fudging_start_time = time.time()
                    self._last_fudging_check = time.time()
                    return True, description, score
                
                # Check held key (fast O(1) check for long durations)
                is_detected, description = self._detect_held_key()
                if is_detected:
                    score = 0.8
                    if self._reduction_enabled:
                        self._reduction_factor = max(self._min_reduction_factor, self._reduction_factor - self._reduction_rate)
                    self._fudging_detected = True
                    if self._fudging_start_time is None:
                        self._fudging_start_time = time.time()
                    self._last_fudging_check = time.time()
                    return True, description, score
                
                # Check key repetition (less common, checked last)
                is_detected, description = self._detect_key_repetition()
                if is_detected:
                    score = 0.8
                    if self._reduction_enabled:
                        self._reduction_factor = max(self._min_reduction_factor, self._reduction_factor - self._reduction_rate)
                    self._fudging_detected = True
                    if self._fudging_start_time is None:
                        self._fudging_start_time = time.time()
                    self._last_fudging_check = time.time()
                    return True, description, score
            
            # No fudging detected - gradually recover
            if self._fudging_detected:
                # Gradually increase reduction factor
                if self._reduction_enabled:
                    old_reduction = self._reduction_factor
                    self._reduction_factor = min(1.0, self._reduction_factor + self._recovery_rate)
                    
                    # Log recovery progress
                    if self._reduction_factor < 1.0:
                        logger.info(
                            "[FUDGING] Recovery in progress — reduction factor: %.2f→%.2f (activity: %.0f%%)",
                            old_reduction, self._reduction_factor, self._reduction_factor * 100,
                        )

                    # Reset if fully recovered
                    if self._reduction_factor >= 1.0:
                        duration = time.time() - self._fudging_start_time if self._fudging_start_time else 0
                        pattern_duration = time.time() - self._fudging_pattern_start_time if self._fudging_pattern_start_time else 0
                        logger.info(
                            "[FUDGING] Recovery complete — activity fully restored. "
                            "Session fudged for %.1fs, pattern duration %.1fs.",
                            duration, pattern_duration,
                        )
                        self._fudging_detected = False
                        self._fudging_start_time = None
                        # Reset pattern tracking
                        self._fudging_pattern_start_time = None
                        self._fudging_pattern_key = None
                        self._fudging_pattern_type = None
            
            self._last_fudging_check = time.time()
            return False, "", 0.0
        
        return self._safe_operation(_check, (False, "", 0.0))
    
    def get_reduction_factor(self) -> float:
        """Get current activity reduction factor (1.0 = no reduction, 0.1 = 90% reduction)"""
        if not self._enabled or not self._reduction_enabled:
            return 1.0
        
        def _get():
            return self._reduction_factor
        
        return self._safe_operation(_get, 1.0)  # Fail-safe: return 1.0 (no reduction)
    
    def is_fudging_detected(self) -> bool:
        """Check if fudging is currently detected"""
        if not self._enabled:
            return False
        
        def _check():
            return self._fudging_detected
        
        return self._safe_operation(_check, False)  # Fail-safe: return False
    
    def get_fudging_info(self) -> Dict:
        """Get current fudging detection information"""
        if not self._enabled:
            return {}
        
        def _get():
            current_time = time.time()
            fudged_duration = 0.0
            
            # Calculate duration based on actual events, not time since pattern start
            # This gives accurate duration even if pattern was detected earlier
            if self._fudging_pattern_start_time is not None and self._fudging_pattern_key:
                if (self._fudging_pattern_type == "held_key" and 
                        self._current_held_key == self._fudging_pattern_key and
                        self._current_held_key_start):
                    # For held keys we track the start directly
                    fudged_duration = current_time - self._current_held_key_start
                else:
                    # Find actual last event for this key in buffer
                    last_event_time = None
                    for key, timestamp in self._keyboard_events:
                        if key == self._fudging_pattern_key:
                            if last_event_time is None or timestamp > last_event_time:
                                last_event_time = timestamp
                    
                    if last_event_time is not None:
                        # Use actual event duration (first to last event)
                        fudged_duration = last_event_time - self._fudging_pattern_start_time
                        
                        # If no recent events (pattern stopped), use last event time
                        time_since_last = current_time - last_event_time
                        if time_since_last > PATTERN_RESET_WINDOW_SECONDS:
                            # Pattern has stopped, duration is from start to last event
                            # Don't add current time, use actual last event time
                            pass  # fudged_duration already calculated correctly above
                    else:
                        # No events found in buffer, pattern may have been cleared
                        # Use time since pattern start as fallback, but cap it reasonably
                        time_since_start = current_time - self._fudging_pattern_start_time
                        if time_since_start > 60:  # If more than 60 seconds, likely pattern stopped
                            fudged_duration = 0.0  # Reset if too old
                        else:
                            fudged_duration = time_since_start
            
            return {
                'fudging_detected': self._fudging_detected,
                'reduction_factor': self._reduction_factor,
                'fudging_start_time': self._fudging_start_time,
                'fudging_pattern_start_time': self._fudging_pattern_start_time,
                'fudging_pattern_key': self._fudging_pattern_key,
                'fudging_pattern_type': self._fudging_pattern_type,
                'fudged_duration_seconds': fudged_duration,
                'keyboard_events_tracked': len(self._keyboard_events),
                'mouse_events_tracked': len(self._mouse_events)
            }
        
        return self._safe_operation(_get, {})  # Fail-safe: return empty dict
    
    def reset_pattern_tracking(self):
        """Reset pattern tracking (called when key is released)"""
        def _reset():
            self._fudging_pattern_start_time = None
            self._fudging_pattern_key = None
            self._fudging_pattern_type = None
        
        self._safe_operation(_reset, None)
    
    def reset(self):
        """Reset fudging detection state"""
        def _reset():
            self._reduction_factor = 1.0
            self._fudging_detected = False
            self._fudging_start_time = None
            self._keyboard_events.clear()
            self._mouse_events.clear()
            # Reset long-duration tracking
            self._reset_current_held_key()
            # Reset pattern tracking
            self._fudging_pattern_start_time = None
            self._fudging_pattern_key = None
            self._fudging_pattern_type = None
        
        self._safe_operation(_reset, None)


# Global singleton instance (thread-safe creation)
_fudging_detector: "Optional[FudgingDetector]" = None
_fudging_detector_lock = __import__("threading").Lock()


def get_fudging_detector() -> "FudgingDetector":
    """Get the global fudging detector instance (thread-safe)."""
    global _fudging_detector
    if _fudging_detector is None:
        with _fudging_detector_lock:
            if _fudging_detector is None:
                _fudging_detector = FudgingDetector()
    return _fudging_detector

