"""
Time/Date/Timezone Change Monitor

- OptimizedWindowsTimeMonitor: Windows-only; uses winreg for precise detection.
- CrossPlatformTimeMonitor: macOS and Linux; uses monotonic-clock drift to
  detect wall-clock jumps and /etc/localtime for timezone changes.
"""

import time
import logging
import psutil
import platform
from datetime import datetime, timedelta
from threading import Thread, Event, Lock
from typing import Dict, List, Optional

# Only import winreg on Windows
if platform.system() == "Windows":
    try:
        import winreg
    except ImportError:
        winreg = None
        logging.warning("winreg module not available - Windows time monitor will be disabled")
else:
    winreg = None

logger = logging.getLogger(__name__)

class OptimizedWindowsTimeMonitor:
    """Lightweight Windows time/date/timezone change monitor"""
    DEFAULT_RECENT_EVENTS_MINUTES = 60
    def __init__(self):
        # CRITICAL: Only allow initialization on Windows
        if platform.system() != "Windows":
            raise RuntimeError(
                f"OptimizedWindowsTimeMonitor is Windows-only. "
                f"Current platform: {platform.system()}. "
                f"This monitor should not be initialized on macOS or Linux."
            )
        
        if winreg is None:
            raise RuntimeError(
                "winreg module not available - cannot initialize Windows time monitor. "
                "This may indicate the module is being imported on a non-Windows system."
            )
        
        self.stop_event = Event()
        self.last_timezone = None
        self.last_system_time = None
        self.time_change_events = []
        self._monitor_thread = None  # Track monitor thread to prevent duplicates
        self._on_time_error_callback = None  # Called when timezone/datetime error is sent (so activity can skip last_api_call)
        
        # Resource usage thresholds
        MAX_CPU_USAGE_PERCENT = 5.0  # Max CPU usage percentage
        MAX_MEMORY_USAGE_PERCENT = 50.0  # Max memory usage percentage
        self.cpu_threshold = MAX_CPU_USAGE_PERCENT
        self.memory_threshold = MAX_MEMORY_USAGE_PERCENT
        
        # Smart polling intervals (adaptive)
        BASE_POLLING_INTERVAL_SECONDS = 60  # Start with 60 seconds
        MIN_POLLING_INTERVAL_SECONDS = 30   # Minimum 30 seconds
        MAX_POLLING_INTERVAL_SECONDS = 300  # Maximum 5 minutes (300 seconds)
        self.base_interval = BASE_POLLING_INTERVAL_SECONDS
        self.min_interval = MIN_POLLING_INTERVAL_SECONDS
        self.max_interval = MAX_POLLING_INTERVAL_SECONDS
        
        # Adaptive interval constants
        CONSECUTIVE_NO_CHANGES_THRESHOLD = 5  # No changes for N checks before increasing interval
        INTERVAL_INCREASE_FACTOR = 1.5  # Multiply interval by this when no changes detected
        FALLBACK_SLEEP_SECONDS = 60  # Fallback sleep interval
        TIMEZONE_CHECK_INTERVAL_SECONDS = 300  # Check timezone every 5 minutes
        MINUTES_PER_HOUR = 60  # Minutes in an hour
        SECONDS_PER_HOUR = 3600  # Seconds in an hour
        STATS_CHECK_INTERVAL_SECONDS = 60  # Check stats every minute
        
        # Time detection thresholds
        TIME_JUMP_THRESHOLD_SECONDS = 2  # Time jump detection threshold
        MAX_TIME_CHANGE_EVENTS = 100  # Maximum events to keep in memory
        INTERVAL_HALVING_FACTOR = 2  # Factor for halving interval when changes detected
        
        # Timezone mapping from Windows registry to standard format
        # Based on backend supported timezones
        self.timezone_mapping = {
            # India and South Asia
            "India Standard Time": "Asia/Kolkata",
            "Asia/Calcutta": "Asia/Kolkata",
            "IST": "Asia/Kolkata",
            "Pakistan Standard Time": "Asia/Karachi",
            "Bangladesh Standard Time": "Asia/Dhaka",
            "Sri Lanka Standard Time": "Asia/Colombo",
            "Nepal Standard Time": "Asia/Kathmandu",
            "Myanmar Standard Time": "Asia/Yangon",
            "Indochina Standard Time": "Asia/Saigon",
            "Indochina Daylight Time": "Asia/Saigon",
            
            # Southeast Asia
            "Singapore Standard Time": "Asia/Singapore",
            "Malay Peninsula Standard Time": "Asia/Kuala_Lumpur",
            "Philippine Standard Time": "Asia/Manila",
            "S.E. Asia Standard Time": "Asia/Bangkok",
            "S.E. Asia Daylight Time": "Asia/Bangkok",
            "Western Indonesian Time": "Asia/Jakarta",
            "Western Indonesian Daylight Time": "Asia/Jakarta",
            "Hong Kong Standard Time": "Asia/Hong_Kong",
            "Taipei Standard Time": "Asia/Taipei",
            "Brunei Darussalam Time": "Asia/Brunei",
            "Brunei Darussalam Daylight Time": "Asia/Brunei",
            
            # East Asia
            "China Standard Time": "Asia/Shanghai",
            "Tokyo Standard Time": "Asia/Tokyo",
            "Korea Standard Time": "Asia/Seoul",
            
            # Central Asia
            "Afghanistan Standard Time": "Asia/Kabul",
            "Central Asia Standard Time": "Asia/Dhaka",
            "North Asia Standard Time": "Asia/Krasnoyarsk",
            "North Asia East Standard Time": "Asia/Irkutsk",
            "Yakutsk Standard Time": "Asia/Yakutsk",
            "Vladivostok Standard Time": "Asia/Vladivostok",
            "Petropavlovsk-Kamchatski Standard Time": "Asia/Kamchatka",
            "Iran Standard Time": "Asia/Tehran",
            "Gulf Standard Time": "Asia/Dubai",
            "Gulf Daylight Time": "Asia/Dubai",
            "Abu Dhabi/Muscat": "Asia/Dubai",
            "West Asia Standard Time": "Asia/Tashkent",
            "Armenia Standard Time": "Asia/Yerevan",
            "Azerbaijan Standard Time": "Asia/Baku",
            "Georgia Standard Time": "Asia/Tbilisi",
            
            # Middle East
            "Arab Standard Time": "Asia/Riyadh",
            "Israel Standard Time": "Asia/Jerusalem",
            "Turkey Standard Time": "Europe/Istanbul",
            "GTB Standard Time": "Europe/Istanbul",
            "Bahrain Standard Time": "Asia/Bahrain",
            "Bahrain Time": "Asia/Bahrain",
            
            # US Timezones
            "Eastern Standard Time": "America/New_York",
            "Central Standard Time": "America/Chicago",
            "Mountain Standard Time": "America/Denver",
            "Pacific Standard Time": "America/Los_Angeles",
            "Alaskan Standard Time": "America/Anchorage",
            "Hawaiian Standard Time": "Pacific/Honolulu",
            
            # Europe
            "GMT Standard Time": "Europe/London",
            "Central Europe Standard Time": "Europe/Budapest",
            "Central European Standard Time": "Europe/Paris",
            "Eastern Europe Standard Time": "Europe/Bucharest",
            "Russian Standard Time": "Europe/Moscow",
            "Romance Standard Time": "Europe/Paris",
            "W. Europe Standard Time": "Europe/Berlin",
            "E. Europe Standard Time": "Europe/Kyiv",
            "FLE Standard Time": "Europe/Helsinki",
            
            # Africa
            "South Africa Standard Time": "Africa/Johannesburg",
            "Egypt Standard Time": "Africa/Cairo",
            "Morocco Standard Time": "Africa/Casablanca",
            "E. Africa Standard Time": "Africa/Nairobi",
            "E. Africa Daylight Time": "Africa/Nairobi",
            "Nairobi Standard Time": "Africa/Nairobi",
            "Nairobi Time": "Africa/Nairobi",
            
            # Australia and Pacific
            "AUS Eastern Standard Time": "Australia/Sydney",
            "Cen. Australia Standard Time": "Australia/Adelaide",
            "W. Australia Standard Time": "Australia/Perth",
            "Fiji Islands Standard Time": "Pacific/Fiji",
            "New Zealand Standard Time": "Pacific/Auckland",
            "Samoa Standard Time": "Pacific/Apia",
            
            # UTC and GMT
            "UTC": "UTC",
            "Greenwich Mean Time": "Europe/London",
            
            # Additional Windows Registry names for direct mapping
            "Eastern Daylight Time": "America/New_York",
            "Central Daylight Time": "America/Chicago",
            "Mountain Daylight Time": "America/Denver",
            "Pacific Daylight Time": "America/Los_Angeles",
            "Alaska Daylight Time": "America/Anchorage",
            "Hawaii Daylight Time": "Pacific/Honolulu",
            "Atlantic Standard Time": "America/Halifax",
            "Atlantic Daylight Time": "America/Halifax",
            "Newfoundland Standard Time": "America/St_Johns",
            "Newfoundland Daylight Time": "America/St_Johns",
            "Central America Standard Time": "America/Guatemala",
            "Central America Daylight Time": "America/Guatemala",
            "Mexico Standard Time": "America/Mexico_City",
            "Mexico Daylight Time": "America/Mexico_City",
            "US Mountain Standard Time": "America/Phoenix",
            "US Mountain Daylight Time": "America/Phoenix",
            "Mountain Standard Time (Mexico)": "America/Mazatlan",
            "Mountain Daylight Time (Mexico)": "America/Mazatlan",
            "Central Standard Time (Mexico)": "America/Mexico_City",
            "Central Daylight Time (Mexico)": "America/Mexico_City",
            "Eastern Standard Time (Mexico)": "America/Cancun",
            "Eastern Daylight Time (Mexico)": "America/Cancun",
            "SA Pacific Standard Time": "America/Bogota",
            "SA Pacific Daylight Time": "America/Bogota",
            "SA Western Standard Time": "America/Caracas",
            "SA Western Daylight Time": "America/Caracas",
            "SA Eastern Standard Time": "America/Argentina/Buenos_Aires",
            "SA Eastern Daylight Time": "America/Argentina/Buenos_Aires",
            "Central Brazilian Standard Time": "America/Campo_Grande",
            "Central Brazilian Daylight Time": "America/Campo_Grande",
            "Pacific SA Standard Time": "America/Santiago",
            "Pacific SA Daylight Time": "America/Santiago",
            "SA Eastern Standard Time": "America/Sao_Paulo",
            "SA Eastern Daylight Time": "America/Sao_Paulo",
            "Montevideo Standard Time": "America/Montevideo",
            "Montevideo Daylight Time": "America/Montevideo",
            "Azores Standard Time": "Atlantic/Azores",
            "Azores Daylight Time": "Atlantic/Azores",
            "Cape Verde Standard Time": "Atlantic/Cape_Verde",
            "Cape Verde Daylight Time": "Atlantic/Cape_Verde",
            "Greenwich Standard Time": "Europe/London",
            "Greenwich Daylight Time": "Europe/London",
            "Central Europe Standard Time": "Europe/Budapest",
            "Central Europe Daylight Time": "Europe/Budapest",
            "Central European Standard Time": "Europe/Paris",
            "Central European Daylight Time": "Europe/Paris",
            "Romance Standard Time": "Europe/Paris",
            "Romance Daylight Time": "Europe/Paris",
            "W. Europe Standard Time": "Europe/Berlin",
            "W. Europe Daylight Time": "Europe/Berlin",
            "E. Europe Standard Time": "Europe/Kyiv",
            "E. Europe Daylight Time": "Europe/Kyiv",
            "FLE Standard Time": "Europe/Helsinki",
            "FLE Daylight Time": "Europe/Helsinki",
            "GTB Standard Time": "Europe/Istanbul",
            "GTB Daylight Time": "Europe/Istanbul",
            "Israel Standard Time": "Asia/Jerusalem",
            "Israel Daylight Time": "Asia/Jerusalem",
            "Jordan Standard Time": "Asia/Amman",
            "Jordan Daylight Time": "Asia/Amman",
            "Arabic Standard Time": "Asia/Baghdad",
            "Arabic Daylight Time": "Asia/Baghdad",
            "Arab Standard Time": "Asia/Riyadh",
            "Arab Daylight Time": "Asia/Riyadh",
            "Kuwait Standard Time": "Asia/Kuwait",
            "Kuwait Daylight Time": "Asia/Kuwait",
            "Qatar Standard Time": "Asia/Qatar",
            "Qatar Daylight Time": "Asia/Qatar",
            "Muscat Standard Time": "Asia/Muscat",
            "Muscat Daylight Time": "Asia/Muscat",
            "Bahrain Standard Time": "Asia/Bahrain",
            "Bahrain Daylight Time": "Asia/Bahrain",
            "Syria Standard Time": "Asia/Damascus",
            "Syria Daylight Time": "Asia/Damascus",
            "Lebanon Standard Time": "Asia/Beirut",
            "Lebanon Daylight Time": "Asia/Beirut",
            "Gaza Standard Time": "Asia/Gaza",
            "Gaza Daylight Time": "Asia/Gaza",
            "Hebron Standard Time": "Asia/Hebron",
            "Hebron Daylight Time": "Asia/Hebron",
            "Yemen Standard Time": "Asia/Aden",
            "Yemen Daylight Time": "Asia/Aden",
            "Iran Standard Time": "Asia/Tehran",
            "Iran Daylight Time": "Asia/Tehran",
            "Arabian Standard Time": "Asia/Dubai",
            "Arabian Daylight Time": "Asia/Dubai",
            "West Asia Standard Time": "Asia/Tashkent",
            "West Asia Daylight Time": "Asia/Tashkent",
            "Central Asia Standard Time": "Asia/Dhaka",
            "Central Asia Daylight Time": "Asia/Dhaka",
            "Nepal Standard Time": "Asia/Kathmandu",
            "Nepal Daylight Time": "Asia/Kathmandu",
            "Myanmar Standard Time": "Asia/Yangon",
            "Myanmar Daylight Time": "Asia/Yangon",
            "SE Asia Standard Time": "Asia/Bangkok",
            "SE Asia Daylight Time": "Asia/Bangkok",
            "North Asia Standard Time": "Asia/Krasnoyarsk",
            "North Asia Daylight Time": "Asia/Krasnoyarsk",
            "North Asia East Standard Time": "Asia/Irkutsk",
            "North Asia East Daylight Time": "Asia/Irkutsk",
            "Yakutsk Standard Time": "Asia/Yakutsk",
            "Yakutsk Daylight Time": "Asia/Yakutsk",
            "Vladivostok Standard Time": "Asia/Vladivostok",
            "Vladivostok Daylight Time": "Asia/Vladivostok",
            "Petropavlovsk-Kamchatski Standard Time": "Asia/Kamchatka",
            "Petropavlovsk-Kamchatski Daylight Time": "Asia/Kamchatka",
            "Fiji Islands Standard Time": "Pacific/Fiji",
            "Fiji Islands Daylight Time": "Pacific/Fiji",
            "New Zealand Standard Time": "Pacific/Auckland",
            "New Zealand Daylight Time": "Pacific/Auckland",
            "Samoa Standard Time": "Pacific/Apia",
            "Samoa Daylight Time": "Pacific/Apia",
            "Philippine Standard Time": "Asia/Manila",
            "Philippine Daylight Time": "Asia/Manila",
            "Hong Kong Standard Time": "Asia/Hong_Kong",
            "Hong Kong Daylight Time": "Asia/Hong_Kong",
            "Taipei Standard Time": "Asia/Taipei",
            "Taipei Daylight Time": "Asia/Taipei",
            "Brunei Darussalam Time": "Asia/Brunei",
            "Brunei Darussalam Daylight Time": "Asia/Brunei",
            "China Standard Time": "Asia/Shanghai",
            "China Daylight Time": "Asia/Shanghai",
            "Tokyo Standard Time": "Asia/Tokyo",
            "Tokyo Daylight Time": "Asia/Tokyo",
            "Korea Standard Time": "Asia/Seoul",
            "Korea Daylight Time": "Asia/Seoul",
            "Singapore Standard Time": "Asia/Singapore",
            "Singapore Daylight Time": "Asia/Singapore",
            "Malay Peninsula Standard Time": "Asia/Kuala_Lumpur",
            "Malay Peninsula Daylight Time": "Asia/Kuala_Lumpur",
            "Western Indonesian Time": "Asia/Jakarta",
            "Western Indonesian Daylight Time": "Asia/Jakarta",
            "AUS Eastern Standard Time": "Australia/Sydney",
            "AUS Eastern Daylight Time": "Australia/Sydney",
            "Cen. Australia Standard Time": "Australia/Adelaide",
            "Cen. Australia Daylight Time": "Australia/Adelaide",
            "W. Australia Standard Time": "Australia/Perth",
            "W. Australia Daylight Time": "Australia/Perth",
            "Tasmania Standard Time": "Australia/Hobart",
            "Tasmania Daylight Time": "Australia/Hobart",
            "E. Australia Standard Time": "Australia/Brisbane",
            "E. Australia Daylight Time": "Australia/Brisbane",
            "Central Pacific Standard Time": "Pacific/Guadalcanal",
            "Central Pacific Daylight Time": "Pacific/Guadalcanal",
            "West Pacific Standard Time": "Pacific/Port_Moresby",
            "West Pacific Daylight Time": "Pacific/Port_Moresby",
        }
        
    def _map_timezone_to_standard(self, windows_timezone: str) -> str:
        """Map Windows timezone names to standard format using offset detection"""
        try:
            # First try to get the actual UTC offset from the registry
            # winreg is already imported conditionally at module level
            
            # Open timezone registry key
            timezone_key = winreg.OpenKey(
                winreg.HKEY_LOCAL_MACHINE,
                f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\{windows_timezone}",
                0,
                winreg.KEY_READ
            )
            
            # Get the bias (offset in minutes)
            bias, _ = winreg.QueryValueEx(timezone_key, "Bias")
            winreg.CloseKey(timezone_key)
            
            # Convert bias to hours (bias is in minutes, negative for UTC+)
            offset_hours = -bias / MINUTES_PER_HOUR
            
            logger.debug("Windows timezone '%s' has offset: %s hours", windows_timezone, offset_hours)
            
            # Use offset-based mapping
            return self._offset_to_timezone(offset_hours)
            
        except (OSError, PermissionError, FileNotFoundError, ValueError) as e:
            logger.debug("Failed to get offset for '%s': %s", windows_timezone, e)

            # Fallback to name-based mapping for known timezones.
            # Note: TimeZoneKeyName in the registry is always the English internal
            # Windows identifier regardless of UI locale, so the mapping is locale-safe.
            return self.timezone_mapping.get(windows_timezone, "UTC")
        
    def start_monitoring(self):
        """Start optimized monitoring"""
        logger.info("Starting optimized Windows time change monitoring...")
        
        # CRITICAL: Stop existing monitor thread before creating new one to prevent duplicates
        if hasattr(self, '_monitor_thread') and self._monitor_thread and self._monitor_thread.is_alive():
            logger.warning("Time monitor already running, stopping existing thread before starting new one")
            try:
                self.stop_event.set()  # Signal thread to exit
                self._monitor_thread.join(timeout=2)
                if self._monitor_thread.is_alive():
                    logger.warning("Time monitor thread did not stop within timeout, continuing anyway")
                self.stop_event.clear()  # Reset for new thread
            except Exception as e:
                logger.debug("Error stopping existing time monitor thread: %s", e)

        # stop_monitoring() leaves this set. Always reset it before a fresh run,
        # including when timezone_preference toggles checks on/off (0↔1).
        self.stop_event.clear()
        
        # Single monitoring thread with smart intervals
        self._monitor_thread = Thread(target=self._smart_monitor, daemon=True, name="TimeMonitor")
        self._monitor_thread.start()
        # Register worker thread for tracking
        from ws_package.thread_manager import register_worker
        register_worker("TimeMonitor", self._monitor_thread)
        
        logger.info("Optimized time monitoring started")
    
    def stop_monitoring(self):
        """Stop monitoring"""
        self.stop_event.set()
        logger.info("Time monitoring stopped")
    
    def _check_system_resources(self) -> bool:
        """Check if system resources are available for monitoring - Optimized to prevent memory leaks"""
        try:
            # interval=0 compares to last sample and returns immediately (non-blocking).
            cpu_percent = psutil.cpu_percent(interval=0)
            memory_percent = psutil.virtual_memory().percent
            
            if cpu_percent > self.cpu_threshold:
                logger.debug("CPU usage high (%s%%), skipping check", cpu_percent)
                return False
                
            if memory_percent > self.memory_threshold:
                logger.debug("Memory usage high (%s%%), skipping check", memory_percent)
                return False
                
            return True
            
        except Exception as e:
            logger.debug("Resource check failed: %s", e)
            return True  # Continue if we can't check resources
    
    def _smart_monitor(self):
        """Smart monitoring with adaptive intervals"""
        interval = self.base_interval
        consecutive_no_changes = 0
        
        while not self.stop_event.is_set():
            try:
                # Check system resources
                if not self._check_system_resources():
                    time.sleep(interval)
                    continue
                
                # Perform lightweight checks
                changes_detected = self._perform_lightweight_checks()
                
                # Adaptive interval based on activity
                if changes_detected:
                    interval = max(self.min_interval, interval // INTERVAL_HALVING_FACTOR)
                    consecutive_no_changes = 0
                    logger.debug("Changes detected, reducing interval to %ss", interval)
                else:
                    consecutive_no_changes += 1
                    if consecutive_no_changes >= CONSECUTIVE_NO_CHANGES_THRESHOLD:
                        interval = min(self.max_interval, int(interval * INTERVAL_INCREASE_FACTOR))
                        consecutive_no_changes = 0
                        logger.debug("No changes, increasing interval to %ss", interval)
                
                # Sleep with adaptive interval
                time.sleep(interval)
                
            except Exception as e:
                logger.warning("Smart monitoring error: %s", e)
                time.sleep(FALLBACK_SLEEP_SECONDS)
    
    def _perform_lightweight_checks(self) -> bool:
        """Perform lightweight time change checks"""
        changes_detected = False
        
        try:
            # 1. Quick system time check (most important)
            current_time = datetime.now()
            if self.last_system_time:
                time_diff = abs((current_time - self.last_system_time).total_seconds())
                if time_diff > TIME_JUMP_THRESHOLD_SECONDS:
                    self._log_time_event("System Time", {
                        "type": "time_jump",
                        "previous_time": self.last_system_time,
                        "current_time": current_time,
                        "difference_seconds": time_diff
                    })
                    changes_detected = True
            
            self.last_system_time = current_time
            
            # 2. Timezone check (less frequent, more expensive)
            if self._should_check_timezone():
                timezone_changed = self._check_timezone()
                if timezone_changed:
                    changes_detected = True
            
        except Exception as e:
            logger.warning("Lightweight check error: %s", e)
        
        return changes_detected
    
    def _should_check_timezone(self) -> bool:
        """Determine if we should check timezone (expensive operation)"""
        # Check timezone less frequently (every 5 minutes)
        if not hasattr(self, '_last_timezone_check'):
            self._last_timezone_check = datetime.now()
            return True
        
        time_since_check = (datetime.now() - self._last_timezone_check).total_seconds()
        if time_since_check > TIMEZONE_CHECK_INTERVAL_SECONDS:
            self._last_timezone_check = datetime.now()
            return True
        
        return False
    
    def _check_timezone(self) -> bool:
        """Check for timezone changes"""
        try:
            key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 
                                r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation")
            
            # Get the Windows timezone name
            windows_timezone = winreg.QueryValueEx(key, "TimeZoneKeyName")[0]
            winreg.CloseKey(key)
            
            # Map to standard format
            current_timezone = self._map_timezone_to_standard(windows_timezone)
            
            if self.last_timezone and self.last_timezone != current_timezone:
                self._log_time_event("Registry", {
                    "type": "timezone_change",
                    "windows_timezone": windows_timezone,
                    "old_timezone": self.last_timezone,
                    "new_timezone": current_timezone
                })
                self.last_timezone = current_timezone
                return True
            
            self.last_timezone = current_timezone
            return False
            
        except Exception as e:
            logger.debug("Timezone check failed: %s", e)
            return False
    
    def _log_time_event(self, source: str, data: Dict):
        """Log time change events efficiently"""
        event = {
            "timestamp": datetime.now(),
            "source": source,
            "data": data
        }
        
        # Keep only last N events to prevent memory bloat
        self.time_change_events.append(event)
        if len(self.time_change_events) > MAX_TIME_CHANGE_EVENTS:
            self.time_change_events = self.time_change_events[-MAX_TIME_CHANGE_EVENTS:]
        
        logger.info("Time change detected from %s: %s", source, data)
        self._handle_time_change(event)
    
    def set_on_time_error_callback(self, callback):
        """Set callback to run when timezone or datetime error is sent (so activity can skip last_api_call)."""
        self._on_time_error_callback = callback

    def _handle_time_change(self, event: Dict):
        """Handle time change events - send to Electron via IPC"""
        try:
            # Import electron_comm here to avoid circular imports
            from ws_package.utils.electron_comm import get_electron_comm
            
            electron_comm = get_electron_comm()
            
            # Check if electron_comm is available and connected
            if not electron_comm or not electron_comm.is_connected():
                logger.debug("Electron comm not available, skipping time change notification")
                return
            
            event_type = event.get('data', {}).get('type')
            
            if event_type == 'timezone_change':
                # Send timezone error to Electron
                error_data = {
                    'title': 'Timezone Error',
                    'message': 'Your system timezone must match with Workstatus account timezone to start the tracker',
                    'old_timezone': event.get('data', {}).get('old_timezone'),
                    'new_timezone': event.get('data', {}).get('new_timezone'),
                    'timestamp': event.get('timestamp').isoformat() if event.get('timestamp') else None
                }
                electron_comm.send_to_electron('timezone_error', error_data)
                logger.info("Sent timezone error to Electron: %s", error_data)
                if self._on_time_error_callback:
                    try:
                        self._on_time_error_callback()
                    except Exception as cb_e:
                        logger.error("Time error callback failed: %s", cb_e)
                
            elif event_type == 'time_jump':
                # Send datetime change error to Electron (not a timezone mismatch).
                time_diff = event.get('data', {}).get('difference_seconds', 0)
                error_data = {
                    'title': 'System Clock Adjustment Detected',
                    'message': (
                        "A large change was detected in your system clock "
                        "(for example after sleep, wake, or manual time change).\n"
                        "Please verify that date and time are set correctly, then start tracking again."
                    ),
                    'previous_time': str(event.get('data', {}).get('previous_time')),
                    'current_time': str(event.get('data', {}).get('current_time')),
                    'difference_seconds': time_diff,
                    'timestamp': event.get('timestamp').isoformat() if event.get('timestamp') else None
                }
                electron_comm.send_to_electron('datetime_error', error_data)
                logger.info("Sent datetime error to Electron: %s", error_data)
                if self._on_time_error_callback:
                    try:
                        self._on_time_error_callback()
                    except Exception as cb_e:
                        logger.error("Time error callback failed: %s", cb_e)
                
        except Exception as e:
            logger.error("Error sending time change event to Electron: %s", e)

    
    def get_recent_events(self, minutes: int = None) -> List[Dict]:
        """Get recent time change events"""
        if minutes is None:
            minutes = OptimizedWindowsTimeMonitor.DEFAULT_RECENT_EVENTS_MINUTES
        cutoff_time = datetime.now() - timedelta(minutes=minutes)
        
        return [
            event for event in self.time_change_events
            if event["timestamp"] > cutoff_time
        ]
    
    def get_timezone_info(self) -> Optional[Dict]:
        """Get current timezone information with maximum robustness"""
        
        # Method 1: Try HKLM registry (requires admin rights)
        try:
            key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 
                                r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation")
            
            # Get Windows timezone name
            windows_timezone = winreg.QueryValueEx(key, "TimeZoneKeyName")[0]
            bias = winreg.QueryValueEx(key, "Bias")[0]
            
            winreg.CloseKey(key)
            
            # Map to standard format
            standard_timezone = self._map_timezone_to_standard(windows_timezone)
            logger.info("Windows timezone: %s, Standard timezone: %s, Bias: %s", windows_timezone, standard_timezone, bias)
            return {
                "windows_timezone": windows_timezone,
                "standard_timezone": standard_timezone,
                "bias_minutes": bias,
                "current_time": datetime.now(),
                "source": "registry_hklm"
            }
            
        except (PermissionError, FileNotFoundError, OSError) as e:
            logger.debug("HKLM registry access failed: %s", e)
            
            # Method 2: Try HKCU registry (user-level access)
            try:
                key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 
                                    r"Control Panel\International")
                windows_timezone = winreg.QueryValueEx(key, "sTimeFormat")[0]
                winreg.CloseKey(key)
                
                # Extract timezone from time format or use system timezone
                standard_timezone = self._get_system_timezone()
                
                return {
                    "windows_timezone": windows_timezone,
                    "standard_timezone": standard_timezone,
                    "bias_minutes": 0,  # Will be calculated from standard_timezone
                    "current_time": datetime.now(),
                    "source": "registry_hkcu"
                }
                
            except (PermissionError, FileNotFoundError, OSError) as e:
                logger.debug("HKCU registry access failed: %s", e)
                
                # Method 3: Use Python's timezone detection (most reliable)
                try:
                    import time
                    import os
                    
                    # Get timezone from environment or system
                    tz_name = os.environ.get('TZ') or time.tzname[0]
                    standard_timezone = self._get_system_timezone()
                    
                    return {
                        "windows_timezone": tz_name,
                        "standard_timezone": standard_timezone,
                        "bias_minutes": 0,
                        "current_time": datetime.now(),
                        "source": "system_timezone"
                    }
                    
                except (OSError, AttributeError) as e:
                    logger.debug("System timezone detection failed: %s", e)
                    
                    # Method 4: Use datetime with UTC fallback (always works)
                    try:
                        from datetime import timezone
                        
                        # Get current UTC offset
                        now = datetime.now()
                        utc_now = datetime.now(timezone.utc)
                        offset_hours = (now - utc_now.replace(tzinfo=None)).total_seconds() / SECONDS_PER_HOUR
                        
                        # Map offset to timezone
                        standard_timezone = self._offset_to_timezone(offset_hours)
                        
                        return {
                            "windows_timezone": f"UTC{int(offset_hours):+d}",
                            "standard_timezone": standard_timezone,
                            "bias_minutes": int(offset_hours * MINUTES_PER_HOUR),
                            "current_time": now,
                            "source": "datetime_calculation"
                        }
                        
                    except Exception as e:
                        logger.debug("Datetime calculation failed: %s", e)
                        
                        # Method 5: Ultimate fallback - return UTC
                        logger.warning("All timezone detection methods failed, using UTC fallback")
                        return {
                            "windows_timezone": "UTC",
                            "standard_timezone": "UTC",
                            "bias_minutes": 0,
                            "current_time": datetime.now(),
                            "source": "fallback_utc"
                        }
    
    def _parse_gmt_timezone(self, gmt_timezone: str) -> str:
        """Parse GMT format timezone (e.g., 'GMT+05:30') and convert to IANA format"""
        try:
            # Handle common formats
            gmt_timezone = gmt_timezone.strip().upper()
            
            # Extract offset from formats like "GMT+05:30", "GMT-08:00", "UTC+05:30"
            if gmt_timezone.startswith(("GMT", "UTC")):
                offset_str = gmt_timezone[3:]  # Remove "GMT" or "UTC"
            else:
                offset_str = gmt_timezone
            
            # Parse the offset
            if offset_str.startswith('+'):
                sign = 1
                offset_str = offset_str[1:]
            elif offset_str.startswith('-'):
                sign = -1
                offset_str = offset_str[1:]
            else:
                sign = 1
            
            # Parse hours and minutes
            if ':' in offset_str:
                hours_str, minutes_str = offset_str.split(':')
                hours = int(hours_str)
                minutes = int(minutes_str)
            else:
                hours = int(offset_str)
                minutes = 0
            
            # Calculate total offset in hours
            total_offset = sign * (hours + minutes / MINUTES_PER_HOUR)
            
            logger.debug("Parsed GMT timezone '%s' -> offset: %s hours", gmt_timezone, total_offset)
            
            # Map to IANA timezone
            return self._offset_to_timezone(total_offset)
            
        except Exception as e:
            logger.warning("Failed to parse GMT timezone '%s': %s", gmt_timezone, e)
            return "UTC"
    
    def _offset_to_timezone(self, offset_hours: float) -> str:
        """Convert UTC offset to timezone name based on backend-supported timezones"""
        # Round to nearest hour for mapping
        offset_rounded = round(offset_hours)
        
        # Comprehensive offset mapping based on backend-supported timezones
        offset_mapping = {
            -12: "Pacific/Kwajalein",
            -11: "Pacific/Samoa",
            -10: "Pacific/Honolulu",
            -9: "America/Anchorage",
            -8: "America/Los_Angeles",
            -7: "America/Denver",
            -6: "America/Chicago",
            -5: "America/New_York",
            -4: "America/Halifax",
            -3: "America/Sao_Paulo",
            -2: "Atlantic/South_Georgia",
            -1: "Atlantic/Azores",
            0: "Europe/London",
            1: "Europe/Paris",
            2: "Europe/Kyiv",
            3: "Africa/Nairobi",
            4: "Asia/Dubai",
            5: "Asia/Karachi",
            6: "Asia/Dhaka",
            7: "Asia/Bangkok",
            8: "Asia/Shanghai",
            9: "Asia/Tokyo",
            10: "Australia/Sydney",
            11: "Pacific/Guadalcanal",
            12: "Pacific/Auckland"
        }
        
        return offset_mapping.get(offset_rounded, "UTC")
    
    def _get_system_timezone(self) -> str:
        """Get system timezone using UTC offset detection (more reliable than name mapping)"""
        try:
            import time
            
            # Get current timezone offset in hours
            utc_offset = -time.timezone // SECONDS_PER_HOUR  # Convert seconds to hours
            
            # Map common offsets to timezone names
            offset_mapping = {
                -12: "Pacific/Kwajalein",
                -11: "Pacific/Samoa",
                -10: "Pacific/Honolulu",
                -9: "America/Anchorage",
                -8: "America/Los_Angeles",
                -7: "America/Denver",
                -6: "America/Chicago",
                -5: "America/New_York",
                -4: "America/Halifax",
                -3: "America/Sao_Paulo",
                -2: "Atlantic/South_Georgia",
                -1: "Atlantic/Azores",
                0: "Europe/London",
                1: "Europe/Paris",
                2: "Europe/Kyiv",
                3: "Africa/Nairobi",
                4: "Asia/Dubai",
                5: "Asia/Karachi",
                5.5: "Asia/Kolkata",
                6: "Asia/Dhaka",
                7: "Asia/Bangkok",
                8: "Asia/Shanghai",
                9: "Asia/Tokyo",
                10: "Australia/Sydney",
                11: "Pacific/Guadalcanal",
                12: "Pacific/Auckland"
            }
            
            return offset_mapping.get(utc_offset, "UTC")
            
        except (OSError, AttributeError) as e:
            logger.debug("System timezone detection failed: %s", e)
            return "UTC"
    
    def get_monitoring_stats(self) -> Dict:
        """Get monitoring statistics - Optimized to prevent memory leaks"""
        try:
            return {
                "events_count": len(self.time_change_events),
                "cpu_usage": psutil.cpu_percent(),  # Non-blocking call
                "memory_usage": psutil.virtual_memory().percent,
                "is_monitoring": not self.stop_event.is_set()
            }
        except Exception as e:
            logger.debug("Error getting monitoring stats: %s", e)
            return {
                "events_count": len(self.time_change_events),
                "cpu_usage": 0,
                "memory_usage": 0,
                "is_monitoring": not self.stop_event.is_set()
            }

class CrossPlatformTimeMonitor:
    """Clock-jump and timezone-change detector for macOS and Linux.

    Uses time.monotonic() as a tamper-proof reference: since the monotonic clock
    is never affected by settimeofday() / adjtime(), a sudden divergence between
    the monotonic elapsed time and the wall-clock elapsed time means the system
    clock was adjusted.

    Same public interface as OptimizedWindowsTimeMonitor so activity_manager.py
    can treat both identically.
    """

    # Minimum wall-clock vs monotonic drift that counts as a deliberate change.
    # macOS sleep/wake and NTP sync often exceed 2s — use a higher threshold there.
    _JUMP_THRESHOLD_SECONDS = 2.0
    _JUMP_THRESHOLD_DARWIN_SECONDS = 300.0
    # On macOS, sleep/wake can produce a large positive wall-clock jump even when
    # the user did not manually adjust time. Treat such jumps as sleep events.
    _SLEEP_LIKE_FORWARD_JUMP_DARWIN_SECONDS = 120.0
    # Ignore drift right after monitor start or wake (baseline still settling).
    _STARTUP_GRACE_SECONDS = 180.0
    _POST_WAKE_GRACE_SECONDS = 120.0
    # Do not spam Electron with repeated datetime_error popups.
    _ERROR_COOLDOWN_SECONDS = 600.0
    # How often to check (seconds). Adaptive between _MIN and _MAX.
    _BASE_INTERVAL = 60
    _MIN_INTERVAL  = 30
    _MAX_INTERVAL  = 300
    # How often to check the timezone (seconds, more expensive).
    _TZ_CHECK_INTERVAL = 300

    def __init__(self):
        if platform.system() == "Windows":
            raise RuntimeError("CrossPlatformTimeMonitor must not run on Windows — use OptimizedWindowsTimeMonitor.")
        self.stop_event = Event()
        self._monitor_thread = None
        self._on_time_error_callback = None
        self.time_change_events: list = []
        self._baseline_lock = Lock()
        self._grace_until_mono = 0.0
        self._last_error_mono = 0.0
        self._wake_registered = False

    # ── Public API (mirrors OptimizedWindowsTimeMonitor) ────────────────────

    def set_on_time_error_callback(self, callback):
        self._on_time_error_callback = callback

    def _jump_threshold_seconds(self) -> float:
        if platform.system() == "Darwin":
            return self._JUMP_THRESHOLD_DARWIN_SECONDS
        return self._JUMP_THRESHOLD_SECONDS

    def _extend_grace_period(self, seconds: float, reason: str):
        until = time.monotonic() + seconds
        with self._baseline_lock:
            if until > self._grace_until_mono:
                self._grace_until_mono = until
        logger.info("Time monitor grace period extended %.0fs (%s)", seconds, reason)

    def _ensure_wake_callback(self):
        if self._wake_registered:
            return
        try:
            from ws_package.utils.system_wake_detector import get_system_wake_detector

            def _on_wake():
                self._extend_grace_period(self._POST_WAKE_GRACE_SECONDS, "system_wake")

            get_system_wake_detector().register_wake_callback(
                "cross_platform_time_monitor", _on_wake
            )
            self._wake_registered = True
            logger.info("Time monitor registered wake callback for baseline grace")
        except Exception as e:
            logger.debug("Could not register time monitor wake callback: %s", e)

    def start_monitoring(self):
        if self._monitor_thread and self._monitor_thread.is_alive():
            logger.warning("CrossPlatformTimeMonitor already running, restarting")
            self.stop_event.set()
            self._monitor_thread.join(timeout=2)
            self.stop_event.clear()
        # A prior stop may have completed before this call, in which case the
        # branch above does not run but the event still needs resetting.
        self.stop_event.clear()
        self._extend_grace_period(self._STARTUP_GRACE_SECONDS, "monitor_start")
        self._ensure_wake_callback()
        self._monitor_thread = Thread(
            target=self._run, daemon=True, name="CrossPlatformTimeMonitor"
        )
        self._monitor_thread.start()
        try:
            from ws_package.thread_manager import register_worker
            register_worker("CrossPlatformTimeMonitor", self._monitor_thread)
        except Exception:
            pass
        logger.info("Cross-platform time monitor started (%s)", platform.system())

    def stop_monitoring(self):
        self.stop_event.set()
        logger.info("Cross-platform time monitor stopped")

    def get_recent_events(self, minutes: int = 60) -> list:
        cutoff = datetime.now() - timedelta(minutes=minutes)
        return [e for e in self.time_change_events if e["timestamp"] > cutoff]

    # ── Internal helpers ─────────────────────────────────────────────────────

    @staticmethod
    def _read_timezone() -> str:
        """Return the current IANA timezone name without spawning any subprocess."""
        try:
            import os as _os
            link = _os.readlink("/etc/localtime")
            if "zoneinfo/" in link:
                return link.split("zoneinfo/")[-1]
        except OSError:
            pass
        try:
            with open("/etc/timezone", encoding="utf-8", errors="replace") as _f:
                tz = _f.read().strip()
                if tz:
                    return tz
        except OSError:
            pass
        # Fallback: use Python's own knowledge
        return time.strftime("%Z") or "UTC"

    def _run(self):
        mono_ref = time.monotonic()
        wall_ref = time.time()
        tz_ref   = self._read_timezone()
        last_tz_check = time.monotonic()
        interval  = self._BASE_INTERVAL
        no_change_streak = 0

        while not self.stop_event.is_set():
            self.stop_event.wait(interval)
            if self.stop_event.is_set():
                break

            changed = False

            # ── 1. Wall-clock jump detection ─────────────────────────────
            mono_now = time.monotonic()
            wall_now = time.time()
            expected_wall = wall_ref + (mono_now - mono_ref)
            drift = abs(wall_now - expected_wall)

            jump_threshold = self._jump_threshold_seconds()
            with self._baseline_lock:
                in_grace = time.monotonic() < self._grace_until_mono
            if drift > jump_threshold:
                diff_seconds = wall_now - expected_wall   # positive = jumped forward
                event = {
                    "timestamp": datetime.now(),
                    "source": "monotonic_drift",
                    "data": {
                        "type": "time_jump",
                        "difference_seconds": diff_seconds,
                        "previous_time": datetime.fromtimestamp(wall_ref),
                        "current_time":  datetime.fromtimestamp(wall_now),
                    },
                }
                self.time_change_events = (self.time_change_events + [event])[-100:]
                is_sleep_like_darwin_jump = (
                    platform.system() == "Darwin"
                    and diff_seconds >= self._SLEEP_LIKE_FORWARD_JUMP_DARWIN_SECONDS
                )
                if is_sleep_like_darwin_jump:
                    logger.info(
                        "Clock drift %.1fs treated as macOS sleep/wake; suppressing datetime_error",
                        diff_seconds,
                    )
                    # Keep a short grace window so post-wake settling/NTP sync
                    # does not trigger a false positive right away.
                    self._extend_grace_period(self._POST_WAKE_GRACE_SECONDS, "sleep_like_drift")
                elif in_grace:
                    logger.info(
                        "Clock drift %.1fs ignored during grace period (threshold %.1fs)",
                        diff_seconds,
                        jump_threshold,
                    )
                else:
                    logger.warning(
                        "Clock jump detected: %.1fs drift (threshold %.1fs)",
                        diff_seconds,
                        jump_threshold,
                    )
                    self._fire_datetime_error(event)
                changed = True

            # Reset references unconditionally so next check is relative to now.
            mono_ref = mono_now
            wall_ref = wall_now

            # ── 2. Timezone change detection ─────────────────────────────
            if (mono_now - last_tz_check) >= self._TZ_CHECK_INTERVAL:
                last_tz_check = mono_now
                try:
                    current_tz = self._read_timezone()
                    if tz_ref and current_tz != tz_ref:
                        event = {
                            "timestamp": datetime.now(),
                            "source": "localtime",
                            "data": {
                                "type": "timezone_change",
                                "old_timezone": tz_ref,
                                "new_timezone": current_tz,
                            },
                        }
                        self.time_change_events = (self.time_change_events + [event])[-100:]
                        logger.info("Timezone changed: %s → %s", tz_ref, current_tz)
                        self._fire_timezone_error(event)
                        changed = True
                    tz_ref = current_tz
                except Exception as e:
                    logger.debug("Timezone check failed: %s", e)

            # ── 3. Adaptive interval ──────────────────────────────────────
            if changed:
                interval = max(self._MIN_INTERVAL, interval // 2)
                no_change_streak = 0
            else:
                no_change_streak += 1
                if no_change_streak >= 5:
                    interval = min(self._MAX_INTERVAL, int(interval * 1.5))
                    no_change_streak = 0

    def _fire_datetime_error(self, event: dict):
        # Disabled by product decision: do not show "System Clock Adjustment Detected"
        # popup for time-jump events.
        logger.info("datetime_error popup disabled; skipping notification")
        return

    def _fire_timezone_error(self, event: dict):
        try:
            from ws_package.utils.electron_comm import get_electron_comm
            ec = get_electron_comm()
            if ec and ec.is_connected():
                data = event["data"]
                ec.send_to_electron("timezone_error", {
                    "title": "Timezone Error",
                    "message": (
                        "Your system timezone must match with Workstatus account timezone "
                        "to start the tracker"
                    ),
                    "old_timezone": data.get("old_timezone"),
                    "new_timezone": data.get("new_timezone"),
                    "timestamp": event["timestamp"].isoformat(),
                })
        except Exception as e:
            logger.error("Error sending timezone_error to Electron: %s", e)
        self._invoke_callback("timezone_error")

    def _invoke_callback(self, error_type: str = None):
        if self._on_time_error_callback:
            try:
                if error_type is not None:
                    self._on_time_error_callback(error_type)
                else:
                    self._on_time_error_callback()
            except TypeError:
                # Backward compatibility: legacy callbacks accept no args.
                try:
                    self._on_time_error_callback()
                except Exception as e:
                    logger.error("Time error callback raised: %s", e)
            except Exception as e:
                logger.error("Time error callback raised: %s", e)


# Usage example
if __name__ == "__main__":
    monitor = OptimizedWindowsTimeMonitor()
    
    try:
        monitor.start_monitoring()
        
        # Keep running with minimal impact
        while True:
            time.sleep(STATS_CHECK_INTERVAL_SECONDS)
            
    except KeyboardInterrupt:
        monitor.stop_monitoring()
        logger.info("Monitoring stopped") 