# ws_package/services/reverb_websocket_service.py
"""
Reverb WebSocket Service for Laravel Reverb server using Pusher protocol.

This service handles WebSocket connections to receive real-time commands
and control app functionality remotely.
"""

import json
import time
import threading
import asyncio
import platform
import requests
import subprocess
from typing import Optional, Dict, List, Callable, Any
from urllib.parse import urlparse
from ws_package.utils.api_log_policy import (
    api_name_from_url,
    log_api_response_summary,
    log_testing_request_headers,
)

# Try to import distro for Linux distribution detection
try:
    import distro
    DISTRO_AVAILABLE = True
except ImportError:
    DISTRO_AVAILABLE = False

# Import websocket-client with error handling
WEBSOCKET_AVAILABLE = False
import_error = None
try:
    import websocket
    WEBSOCKET_AVAILABLE = True
except ImportError as e:
    WEBSOCKET_AVAILABLE = False
    websocket = None
    import_error = str(e)

from ws_package.configs import config
from ws_package.entities.user_entity import UserEntity
from ws_package.log.writer import LogWriter
from ws_package.services.api_request_service import APIRequest
from ws_package.exceptions.custom_exceptions import NetworkError, ValidationError
from ws_package.utils.tracker_status import set_tracker_status

logger = LogWriter.file(__name__)


def get_detailed_os_version() -> str:
    """
    Get detailed OS version string like "MacOS Intel", "MacOS ARM", "Ubuntu 22.04", etc.
    
    Returns:
        str: Detailed OS version string
    """
    try:
        system = platform.system()
        
        if system == "Darwin":  # macOS
            # Get macOS version
            macos_version = platform.mac_ver()[0]
            # Get architecture
            architecture = platform.machine()
            
            if architecture == "arm64":
                return f"MacOS ARM {macos_version}"
            elif architecture == "x86_64":
                return f"MacOS Intel {macos_version}"
            else:
                return f"MacOS {macos_version}"
        
        elif system == "Linux":
            # Try to get distribution info using distro library
            if DISTRO_AVAILABLE:
                try:
                    dist_name = distro.name(pretty=True)
                    dist_version = distro.version(pretty=True)
                    if dist_name and dist_version:
                        return f"{dist_name} {dist_version}"
                    elif dist_name:
                        return dist_name
                except Exception:
                    pass
            
            # Fallback: Try /etc/os-release
            try:
                with open('/etc/os-release', 'r') as f:
                    os_release = f.read()
                    name = None
                    version = None
                    for line in os_release.split('\n'):
                        if line.startswith('NAME='):
                            name = line.split('=')[1].strip('"').strip("'")
                        elif line.startswith('VERSION_ID='):
                            version = line.split('=')[1].strip('"').strip("'")
                    if name and version:
                        return f"{name} {version}"
                    elif name:
                        return name
            except Exception:
                pass
            
            # Final fallback
            return f"Linux {platform.release()}"
        
        elif system == "Windows":
            # Get Windows version
            version = platform.version()
            release = platform.release()
            return f"Windows {release} {version}"
        
        else:
            return f"{system} {platform.release()}"
    
    except Exception as e:
        logger.warning("Error getting detailed OS version: %s", e)
        # Fallback to basic platform info
        return f"{platform.system()} {platform.release()}"


class ReverbWebSocketService:
    """
    WebSocket client service for Laravel Reverb server using Pusher protocol.
    
    Handles connection, authentication, channel subscription, and app control events.
    Thread-safe implementation with automatic reconnection.
    """
    
    def __init__(self, user_entity: UserEntity, activity_controller=None):
        """
        Initialize Reverb WebSocket service.
        
        Args:
            user_entity: User entity with authentication token
            activity_controller: ActivityController instance for app control
        """
        # Check if websocket-client is available
        if not WEBSOCKET_AVAILABLE:
            error_msg = f"websocket-client library not available. Import error: {import_error}. Please install with: pip install websocket-client"
            logger.error("=" * 80)
            logger.error("[WebSocket] ✗ %s", error_msg)
            logger.error("=" * 80)
            raise ImportError(error_msg)
        
        logger.info("=" * 80)
        logger.info("[WebSocket] Initializing ReverbWebSocketService...")
        logger.info("[WebSocket] User ID: %s, Org ID: %s", user_entity.user_id, user_entity.org_id)
        
        self.user_entity = user_entity
        self.activity_controller = activity_controller
        
        # Configuration
        websocket_url = config('websocket_url', 'Reverb', dtype='str')
        if not websocket_url:
            websocket_url = 'wss://api.workstatus.io'
        
        self.websocket_url = websocket_url
        
        broadcasting_auth_url = config('broadcasting_auth_url', 'Reverb', dtype='str')
        if not broadcasting_auth_url:
            # Derive from websocket_url
            base_url = websocket_url.replace('wss://', 'https://').replace('ws://', 'http://')
            broadcasting_auth_url = f"{base_url}/broadcasting/auth"
        self.broadcasting_auth_url = broadcasting_auth_url
        
        # API base URL for execution status updates
        api_base_url = config('api_base_url', 'Application', dtype='str')
        if not api_base_url:
            # Derive from websocket_url
            base_url = websocket_url.replace('wss://', 'https://').replace('ws://', 'http://')
            api_base_url = base_url
        self.api_base_url = api_base_url
        
        reconnect_enabled = config('reconnect_enabled', 'Reverb', dtype='bool')
        self.reconnect_enabled = reconnect_enabled if reconnect_enabled is not None else True
        
        reconnect_max_delay = config('reconnect_max_delay', 'Reverb', dtype='int')
        self.reconnect_max_delay = reconnect_max_delay if reconnect_max_delay is not None else 60
        
        reconnect_initial_delay = config('reconnect_initial_delay', 'Reverb', dtype='int')
        self.reconnect_initial_delay = reconnect_initial_delay if reconnect_initial_delay is not None else 1
        
        max_reconnect_attempts = config('max_reconnect_attempts', 'Reverb', dtype='int')
        self.max_reconnect_attempts = max_reconnect_attempts if max_reconnect_attempts is not None else 5
        
        # Connection state
        self.ws: Optional[websocket.WebSocketApp] = None
        self.socket_id: Optional[str] = None
        self.is_connected_flag = False
        self._connection_lock = threading.Lock()
        self._reconnect_delay = self.reconnect_initial_delay
        self._reconnect_attempts = 0
        self._consecutive_failures = 0  # Track consecutive failed reconnection attempts
        self._reconnecting = False  # Flag to prevent multiple simultaneous reconnection attempts
        self._connection_established_time: Optional[float] = None
        self._last_ping_time: Optional[float] = None
        self._last_message_time: Optional[float] = None  # Track when we last received any message
        self._ping_without_subscription_count = 0
        
        # Health monitoring (lightweight, no macOS event taps)
        self._health_monitor_enabled = True
        self._health_monitor_thread: Optional[threading.Thread] = None
        self._health_check_interval = 30  # Check every 30 seconds
        self._max_silence_seconds = 120  # If no messages for 2 minutes, connection is likely dead
        
        # Channel management
        self.subscribed_channels: Dict[str, str] = {}  # channel_name -> auth_signature
        self._channels_lock = threading.Lock()
        
        # Event handlers
        self.event_handlers: Dict[str, List[Callable]] = {}  # event_name -> [callbacks]
        self.app_control_handlers: Dict[str, Callable] = {}  # event_name -> handler
        self._handlers_lock = threading.Lock()
        
        # Thread management
        self._stop_event: Optional[threading.Event] = None
        self._ws_thread: Optional[threading.Thread] = None
        self._reconnect_thread: Optional[threading.Thread] = None  # Track reconnect thread
        self._reset_flag_thread: Optional[threading.Thread] = None  # Track reset flag thread
        
        # Auth cache
        self._auth_cache: Dict[str, str] = {}  # channel_name -> auth_signature
        self._auth_cache_lock = threading.Lock()
        
        logger.info("ReverbWebSocketService initialized for user %s, org %s", user_entity.user_id, user_entity.org_id)
    
    def connect(self) -> bool:
        """
        Establish WebSocket connection.
        
        Returns:
            bool: True if connection initiated successfully
        """
        try:
            with self._connection_lock:
                if self.is_connected_flag:
                    logger.warning("[WebSocket] WebSocket already connected")
                    return True
                
                # CRITICAL: Stop existing WebSocket thread before creating new one
                # This prevents thread accumulation and CPU rise on reconnects
                if self._ws_thread and self._ws_thread.is_alive():
                    logger.debug("[WebSocket] Stopping existing WebSocket thread before starting new one")
                    try:
                        # Close WebSocket to signal thread to exit
                        if self.ws:
                            try:
                                self.ws.close()
                            except Exception:
                                pass
                        # Wait for thread to finish (with timeout to prevent blocking)
                        self._ws_thread.join(timeout=3)
                        if self._ws_thread.is_alive():
                            logger.warning("[WebSocket] WebSocket thread did not stop within timeout, continuing anyway")
                    except Exception as e:
                        logger.debug("[WebSocket] Error stopping existing WebSocket thread: %s", e)
                
                # Build connection URL
                url = self._build_connection_url()
                logger.info("[WebSocket] Building connection URL: %s", url)
                
                # Create WebSocket connection
                logger.info("[WebSocket] Creating WebSocketApp instance...")
                self.ws = websocket.WebSocketApp(
                    url,
                    on_open=self._on_open,
                    on_message=self._on_message,
                    on_error=self._on_error,
                    on_close=self._on_close
                )
                logger.info("[WebSocket] ✓ WebSocketApp created")
                
                # Start WebSocket in a thread
                logger.info("[WebSocket] Starting WebSocket connection thread...")
                self._ws_thread = threading.Thread(
                    target=self._run_websocket,
                    name="ReverbWebSocket",
                    daemon=True
                )
                self._ws_thread.start()
                logger.info("[WebSocket] ✓ WebSocket connection thread started")
                
                # Wait a moment to see if connection succeeds
                time.sleep(0.5)
                return True
                
        except Exception as e:
            logger.error("[WebSocket] ✗ Failed to initiate WebSocket connection: %s", e, exc_info=True)
            return False
    
    def _build_connection_url(self) -> str:
        """Build WebSocket connection URL with Pusher protocol parameters."""
        base_url = self.websocket_url.strip()
        
        # If caller already provided full URL (with query params), respect it entirely
        if '?' in base_url:
            logger.debug("[WebSocket] Using provided connection URL without modification: %s", base_url)
            return base_url
        
        base_url = base_url.rstrip('/')
        if not base_url.endswith('/app/localkey'):
            if not base_url.endswith('/app'):
                base_url = f"{base_url}/app"
            base_url = f"{base_url}/localkey"
        
        # Add default Pusher protocol parameters matching production expectations
        params = "protocol=7&client=js&version=8.4.0&flash=false"
        url = f"{base_url}?{params}"
        logger.debug("[WebSocket] Constructed connection URL: %s", url)
        return url
    
    def _run_websocket(self):
        """Run WebSocket connection in thread."""
        try:
            self.ws.run_forever()
        except Exception as e:
            logger.error("WebSocket thread error: %s", e)
            self.is_connected_flag = False
    
    def _on_open(self, ws):
        """Handle WebSocket connection opened."""
        logger.info("=" * 80)
        logger.info("[WebSocket] Connection opened - waiting for pusher:connection_established")
        logger.info("=" * 80)
        self.is_connected_flag = True
        self._reconnect_delay = self.reconnect_initial_delay
        self._reconnect_attempts = 0
    
    def _on_message(self, ws, message):
        """Handle incoming WebSocket messages."""
        try:
            # Update last message time (lightweight tracking, no system calls)
            self._last_message_time = time.time()
            
            # Log raw message (truncated if too long)
            message_preview = message[:200] if len(message) > 200 else message
            logger.info("[WebSocket] Received message: %s", message_preview)
            
            data = json.loads(message)
            event = data.get('event')
            channel = data.get('channel')
            event_data_raw = data.get('data')  # Keep raw data for connection_established
            
            # Pusher protocol: data is a JSON string, parse it
            event_data = event_data_raw
            if event_data_raw and isinstance(event_data_raw, str):
                try:
                    event_data = json.loads(event_data_raw)
                except json.JSONDecodeError:
                    # If not JSON, keep as string
                    pass
            
            # Log parsed event details
            logger.info("[WebSocket] Event: %s, Channel: %s", event, channel)
            if event_data:
                # Log data payload (truncated if too long)
                data_str = json.dumps(event_data) if isinstance(event_data, (dict, list)) else str(event_data)
                data_preview = data_str[:500] if len(data_str) > 500 else data_str
                logger.info("[WebSocket] Data: %s", data_preview)
            
            # Check for incomplete connection: ping with no channel and no subscriptions
            if event == 'pusher:ping' and channel is None:
                self._check_connection_health()
            
            # Handle system events
            if event == 'pusher:connection_established':
                # Pass raw data string to handler - it needs to parse socket_id
                self._handle_connection_established(event_data_raw)
            elif event == 'pusher_internal:subscription_succeeded':
                self._handle_subscription_succeeded(channel)
            elif event == 'pusher:error':
                self._handle_pusher_error(event_data)
            elif event == 'pusher:ping':
                self._handle_ping()
            elif event == 'pusher:pong':
                logger.debug("Received pong")
            else:
                # Handle custom events
                self._handle_custom_event(event, channel, event_data)
                
        except json.JSONDecodeError as e:
            logger.error("Failed to parse WebSocket message: %s, message: %s", e, message[:500])
        except Exception as e:
            logger.error("Error handling WebSocket message: %s", e, exc_info=True)
    
    def _on_error(self, ws, error):
        """Handle WebSocket errors."""
        logger.error("WebSocket error: %s", error)
        self.is_connected_flag = False
    
    def _on_close(self, ws, close_status_code, close_msg):
        """Handle WebSocket connection closed."""
        logger.warning("WebSocket connection closed: status=%s, msg=%s", close_status_code, close_msg)
        self.is_connected_flag = False
        self.socket_id = None
        
        # Attempt reconnection if enabled
        if self.reconnect_enabled and not (self._stop_event and self._stop_event.is_set()):
            self._reconnect_with_backoff()
    
    def _handle_connection_established(self, data):
        """Handle pusher:connection_established event."""
        try:
            logger.info("=" * 80)
            logger.info("[WebSocket] === HANDLING pusher:connection_established ===")
            logger.info("[WebSocket] Raw data type: %s, value: %s", type(data), data)
            logger.info("=" * 80)
            
            # Pusher protocol: data is a JSON string containing socket_id
            if isinstance(data, str):
                try:
                    parsed_data = json.loads(data)
                    self.socket_id = parsed_data.get('socket_id')
                    logger.info("[WebSocket] Parsed socket_id from JSON string: %s", self.socket_id)
                except json.JSONDecodeError:
                    # If not JSON, try to extract socket_id directly
                    logger.warning("Connection data is not JSON, trying direct extraction: %s", data)
                    self.socket_id = data
            elif isinstance(data, dict):
                self.socket_id = data.get('socket_id')
                logger.info("[WebSocket] Extracted socket_id from dict: %s", self.socket_id)
            else:
                logger.error("Unexpected data type in connection_established: %s", type(data))
                return
            
            if self.socket_id:
                # Reset failure counter on successful connection
                self._consecutive_failures = 0
                self._reconnect_delay = self.reconnect_initial_delay  # Reset backoff delay
                with self._connection_lock:
                    self._reconnecting = False  # Clear reconnecting flag
                self._connection_established_time = time.time()
                self._last_message_time = time.time()  # Initialize message tracking
                self._ping_without_subscription_count = 0
                # Clear auth cache because socket_id changed and signatures become invalid
                self.refresh_auth_cache()
                logger.info("=" * 80)
                logger.info("[WebSocket] ✓✓✓ CONNECTION ESTABLISHED SUCCESSFULLY ✓✓✓")
                logger.info("[WebSocket] Socket ID: %s", self.socket_id)
                logger.info("[WebSocket] URL: %s", self.websocket_url)
                logger.info("[WebSocket] User ID: %s, Org ID: %s", self.user_entity.user_id, self.user_entity.org_id)
                logger.info("[WebSocket] Subscribing to default channels...")
                logger.info("=" * 80)
                # Subscribe to default channels
                self._subscribe_default_channels()
                
                # Start lightweight health monitor (no macOS event taps)
                self._start_health_monitor()
            else:
                logger.error("=" * 80)
                logger.error("[WebSocket] ✗ Connection established but socket_id not found in data")
                logger.error("[WebSocket] Received data: %s", data)
                logger.error("[WebSocket] Data type: %s", type(data))
                logger.error("=" * 80)
        except Exception as e:
            logger.error("Error handling connection established: %s", e, exc_info=True)
    
    def _handle_subscription_succeeded(self, channel):
        """Handle pusher_internal:subscription_succeeded event."""
        # Reset ping counter and failure counter on successful subscription
        self._ping_without_subscription_count = 0
        # Only reset failures if we have at least one successful subscription
        # This ensures we're truly connected
        with self._channels_lock:
            subscribed_count = len(self.subscribed_channels)
        
        if subscribed_count == 0:  # First subscription
            self._consecutive_failures = 0
            self._reconnect_delay = self.reconnect_initial_delay
        
        logger.info("=" * 80)
        logger.info("[WebSocket] ✓ SUCCESSFULLY SUBSCRIBED TO CHANNEL: %s", channel)
        with self._channels_lock:
            if channel not in self.subscribed_channels:
                logger.warning("[WebSocket] ⚠ Subscription succeeded for unknown channel: %s", channel)
            else:
                # Log all subscribed channels
                subscribed_count = len(self.subscribed_channels)
                logger.info("[WebSocket] Total subscribed channels: %s", subscribed_count)
                if subscribed_count > 0:
                    logger.info("[WebSocket] All subscribed channels:")
                    for ch in self.subscribed_channels.keys():
                        logger.info("[WebSocket]   - %s", ch)
        logger.info("=" * 80)
    
    def _handle_pusher_error(self, data):
        """Handle pusher:error event."""
        try:
            # Parse error data
            if isinstance(data, str):
                try:
                    error_data = json.loads(data)
                except json.JSONDecodeError:
                    error_data = {"message": data}
            elif isinstance(data, dict):
                error_data = data
            else:
                error_data = {"message": str(data)}
            
            error_code = error_data.get('code')
            error_message = error_data.get('message', 'Unknown error')
            
            logger.error("Pusher error: Code %s, Message: %s", error_code, error_message)
            
            # Error 4009 = Unauthorized - usually due to idle/disconnected connection
            # Token is still valid, just need to reconnect
            if error_code == 4009:
                logger.warning("=" * 80)
                logger.warning("[WebSocket] ⚠ Connection unauthorized (Error 4009)")
                logger.warning("[WebSocket] Likely due to idle/disconnected connection")
                logger.warning("[WebSocket] Closing connection and reconnecting...")
                logger.warning("=" * 80)
                
                # Clear socket_id (will get new one on reconnect)
                self.socket_id = None
                
                # Close current connection to trigger reconnection
                with self._connection_lock:
                    if self.ws:
                        try:
                            self.ws.close()
                        except Exception as e:
                            logger.debug("Error closing connection: %s", e)
                        self.ws = None
                    
                    self.is_connected_flag = False
                    # Ensure auth cache is flushed so next socket gets fresh signatures
                    self.refresh_auth_cache()
                
                # Trigger reconnection (auth cache is kept since token is valid)
                if self.reconnect_enabled and not (self._stop_event and self._stop_event.is_set()):
                    # CRITICAL: Stop existing reconnect thread before creating new one to prevent accumulation
                    if self._reconnect_thread and self._reconnect_thread.is_alive():
                        logger.debug("[WebSocket] Stopping existing reconnect thread before starting new one")
                        try:
                            self._reconnect_thread.join(timeout=1)
                            if self._reconnect_thread.is_alive():
                                logger.warning("[WebSocket] Reconnect thread did not stop within timeout, continuing anyway")
                        except Exception as e:
                            logger.debug("[WebSocket] Error stopping existing reconnect thread: %s", e)
                    
                    # Use a thread to avoid blocking
                    def trigger_reconnect():
                        time.sleep(1)  # Brief delay before reconnecting
                        self._reconnect_with_backoff()
                    
                    self._reconnect_thread = threading.Thread(target=trigger_reconnect, name="WebSocketReconnect", daemon=True)
                    self._reconnect_thread.start()
                    # Register worker thread for tracking
                    from ws_package.thread_manager import register_worker
                    register_worker("WebSocketReconnect", self._reconnect_thread)
            else:
                # For other errors, just log them
                error_msg = json.dumps(error_data) if isinstance(error_data, dict) else str(data)
                logger.error("Pusher error (non-critical): %s", error_msg)
                
        except Exception as e:
            error_msg = data if isinstance(data, str) else json.dumps(data)
            logger.error("Error handling pusher error: %s, Original error: %s", e, error_msg)
    
    def _check_connection_health(self):
        """
        Check if connection is healthy - if we're getting pings but no subscriptions succeeded,
        the connection is incomplete and should be retried.
        """
        self._last_ping_time = time.time()
        
        # Check if we have any subscribed channels
        with self._channels_lock:
            has_subscriptions = len(self.subscribed_channels) > 0
        
        # If we've been connected for more than 15 seconds and still no subscriptions
        if self._connection_established_time:
            time_since_connection = time.time() - self._connection_established_time
            
            if time_since_connection > 15 and not has_subscriptions:
                self._ping_without_subscription_count += 1
                
                if self._ping_without_subscription_count >= 3:
                    logger.error("=" * 80)
                    logger.error("[WebSocket] ✗ INCOMPLETE CONNECTION DETECTED")
                    logger.error("[WebSocket] Connection established %ss ago", time_since_connection)
                    logger.error("[WebSocket] Received %s pings but no subscriptions succeeded", self._ping_without_subscription_count)
                    logger.error("[WebSocket] Channel is None - connection not properly established")
                    logger.error("[WebSocket] Closing connection and retrying...")
                    logger.error("=" * 80)
                    
                    # Reset counters
                    self._ping_without_subscription_count = 0
                    self._connection_established_time = None
                    
                    # Close current connection and trigger reconnection
                    with self._connection_lock:
                        if self.ws:
                            try:
                                self.ws.close()
                            except Exception as e:
                                logger.debug("Error closing incomplete connection: %s", e)
                            self.ws = None
                        self.is_connected_flag = False
                        self.socket_id = None
                    
                    # Trigger reconnection
                    if self.reconnect_enabled:
                        self._reconnect_with_backoff()
    
    def _handle_ping(self):
        """Handle pusher:ping event - respond with pong."""
        try:
            # Update last message time (ping counts as activity)
            self._last_message_time = time.time()
            
            # Pusher protocol: data must be JSON string
            pong_message = json.dumps({"event": "pusher:pong", "data": json.dumps({})})
            if self.ws:
                self.ws.send(pong_message)
            logger.debug("Sent pong response")
        except Exception as e:
            logger.error("Error sending pong: %s", e)
    
    def _handle_custom_event(self, event: str, channel: str, data: Any):
        """Handle custom application events."""
        try:
            # Parse data if it's a string
            if isinstance(data, str):
                try:
                    data = json.loads(data)
                except json.JSONDecodeError:
                    pass  # Keep as string if not JSON
            
            # Log full event details
            data_str = json.dumps(data) if isinstance(data, (dict, list)) else str(data)
            data_preview = data_str[:500] if len(data_str) > 500 else data_str
            logger.info("[WebSocket] Handling custom event: %s on channel: %s, data: %s", event, channel, data_preview)
            
            # Check if it's an app control event
            if event.startswith('app.control.'):
                self._handle_app_control_event(event, data)
            else:
                # Call registered event handlers
                with self._handlers_lock:
                    handlers = self.event_handlers.get(event, [])
                    for handler in handlers:
                        try:
                            handler(event, channel, data)
                        except Exception as e:
                            logger.error("Error in event handler for %s: %s", event, e)
        except Exception as e:
            logger.error("Error handling custom event: %s", e, exc_info=True)
    
    def _handle_app_control_event(self, event: str, data: Any):
        """Handle app control events."""
        logger.info("Received app control event: %s", event)
        
        with self._handlers_lock:
            handler = self.app_control_handlers.get(event)
            if handler:
                try:
                    handler(data)
                except Exception as e:
                    logger.error("Error in app control handler for %s: %s", event, e, exc_info=True)
            else:
                logger.warning("No handler registered for app control event: %s", event)
    
    def _subscribe_default_channels(self):
        """Subscribe to default channels after connection."""
        if not self.socket_id:
            logger.error("[WebSocket] ✗ Cannot subscribe: socket_id not available")
            return
        
        org_id = self.user_entity.org_id
        user_id = self.user_entity.user_id
        
        default_channels = [
            f"private-org.{org_id}.messages",
            f"private-org.{org_id}.user.{user_id}.messages",
            f"private-user.{user_id}",
            "private-global.messages"
        ]
        
        logger.info("=" * 80)
        logger.info("[WebSocket] === SUBSCRIBING TO %s CHANNELS ===", len(default_channels))
        logger.info("[WebSocket] Socket ID: %s", self.socket_id)
        logger.info("[WebSocket] Org ID: %s, User ID: %s", org_id, user_id)
        logger.info("=" * 80)
        
        for channel in default_channels:
            logger.info("[WebSocket] Subscribing to: %s", channel)
            success = self.subscribe(channel)
            if success:
                logger.info("[WebSocket] ✓ Subscription request sent for: %s", channel)
            else:
                logger.warning("[WebSocket] ✗ Failed to send subscription request for: %s", channel)
    
    def subscribe(self, channel_name: str, callback: Optional[Callable] = None) -> bool:
        """
        Subscribe to a channel.
        
        Args:
            channel_name: Name of the channel to subscribe to
            callback: Optional callback for events on this channel
            
        Returns:
            bool: True if subscription initiated successfully
        """
        if not self.is_connected_flag or not self.socket_id:
            logger.warning("Cannot subscribe to %s: not connected", channel_name)
            return False
        
        try:
            # Get auth signature for private/presence channels
            if channel_name.startswith('private-') or channel_name.startswith('presence-'):
                auth_signature = self._authenticate_channel(channel_name)
                if not auth_signature:
                    logger.error("Failed to get auth signature for channel: %s", channel_name)
                    return False
            else:
                auth_signature = None
            
            # Send subscription message (Pusher protocol format)
            # Data must be a JSON string, not an object
            subscribe_data = {
                "channel": channel_name
            }
            if auth_signature:
                subscribe_data["auth"] = auth_signature
            
            subscribe_message = {
                "event": "pusher:subscribe",
                "data": json.dumps(subscribe_data)  # Data must be JSON string in Pusher protocol
            }
            
            message_json = json.dumps(subscribe_message)
            if self.ws:
                self.ws.send(message_json)
                logger.info("Sent subscription request for channel: %s", channel_name)
                
                # Store channel subscription
                with self._channels_lock:
                    self.subscribed_channels[channel_name] = auth_signature or ""
                
                # Register callback if provided
                if callback:
                    self.on_event(channel_name, callback)
                
                return True
            else:
                logger.error("WebSocket not available for subscription")
                return False
                
        except Exception as e:
            logger.error("Error subscribing to channel %s: %s", channel_name, e)
            return False
    
    def unsubscribe(self, channel_name: str) -> bool:
        """
        Unsubscribe from a channel.
        
        Args:
            channel_name: Name of the channel to unsubscribe from
            
        Returns:
            bool: True if unsubscription successful
        """
        try:
            # Pusher protocol: data must be JSON string
            unsubscribe_data = {"channel": channel_name}
            unsubscribe_message = {
                "event": "pusher:unsubscribe",
                "data": json.dumps(unsubscribe_data)
            }
            
            message_json = json.dumps(unsubscribe_message)
            if self.ws:
                self.ws.send(message_json)
                logger.info("Unsubscribed from channel: %s", channel_name)
                
                with self._channels_lock:
                    self.subscribed_channels.pop(channel_name, None)
                
                return True
            else:
                logger.error("WebSocket not available for unsubscription")
                return False
                
        except Exception as e:
            logger.error("Error unsubscribing from channel %s: %s", channel_name, e)
            return False
    
    def _authenticate_channel(self, channel_name: str) -> Optional[str]:
        """
        Get authentication signature for a private/presence channel.
        
        Args:
            channel_name: Name of the channel to authenticate
            
        Returns:
            str: Auth signature (format: localkey:...) or None if failed
        """
        if not self.socket_id:
            logger.error("Cannot authenticate channel: socket_id not available")
            return None
        
        # Check cache first
        with self._auth_cache_lock:
            if channel_name in self._auth_cache:
                logger.debug("Using cached auth for channel: %s", channel_name)
                return self._auth_cache[channel_name]
        
        try:
            # Call broadcasting/auth API
            payload = {
                "socket_id": self.socket_id,
                "channel_name": channel_name
            }
            
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.user_entity.sd_token}"
            }
            
            api_request = APIRequest(
                url=self.broadcasting_auth_url,
                headers=headers,
                timeout=10
            )
            api_request.post(payload)
            
            response_data = api_request.data()
            if response_data and isinstance(response_data, dict):
                auth = response_data.get('auth')
                if auth:
                    # Cache the auth signature
                    with self._auth_cache_lock:
                        self._auth_cache[channel_name] = auth
                    logger.info("Authenticated channel: %s", channel_name)
                    return auth
                else:
                    logger.error("No auth in response for channel: %s", channel_name)
            else:
                logger.error("Invalid response from broadcasting/auth API: %s", response_data)
                
        except Exception as e:
            logger.error("Error authenticating channel %s: %s", channel_name, e)
        
        return None
    
    def on_event(self, event_name: str, callback: Callable):
        """
        Register callback for a specific event.
        
        Args:
            event_name: Name of the event to listen for
            callback: Callback function(event, channel, data)
        """
        with self._handlers_lock:
            if event_name not in self.event_handlers:
                self.event_handlers[event_name] = []
            self.event_handlers[event_name].append(callback)
            logger.debug("Registered event handler for: %s", event_name)
    
    def register_app_control_handler(self, event_name: str, handler_function: Callable):
        """
        Register handler for app control events.
        
        Args:
            event_name: App control event name (e.g., 'app.control.screenshot')
            handler_function: Handler function(data)
        """
        with self._handlers_lock:
            self.app_control_handlers[event_name] = handler_function
            logger.info("Registered app control handler for: %s", event_name)
    
    def _reconnect_with_backoff(self):
        """Reconnect with exponential backoff."""
        if not self.reconnect_enabled:
            return
        
        if self._stop_event and self._stop_event.is_set():
            logger.info("Stop event set, not reconnecting")
            return
        
        # Record lifecycle event for health monitoring
        try:
            from ws_package.utils.health_monitor import record_lifecycle_event
            record_lifecycle_event('reconnect')
        except Exception:
            pass  # Fail silently if health monitor not available
        
        # Prevent multiple simultaneous reconnection attempts
        with self._connection_lock:
            if self._reconnecting:
                logger.debug("[WebSocket] Reconnection already in progress, skipping")
                return
            self._reconnecting = True
        
        try:
            # Check if we've exceeded max retry attempts
            if self._consecutive_failures >= self.max_reconnect_attempts:
                from ws_package.utils.error_codes import ErrorCode
                logger.error("=" * 80)
                logger.error("%s — max attempts reached (%s). Real-time sync disabled.",
                             ErrorCode.WEBSOCKET_FAILED, self.max_reconnect_attempts)
                logger.error("[WebSocket] To retry, restart the application or manually reset")
                logger.error("=" * 80)
                self.reconnect_enabled = False
                try:
                    from ws_package.utils.electron_comm import get_electron_comm
                    ec = get_electron_comm()
                    if ec and ec.is_connected():
                        ec.send_to_electron("tracking_error", {
                            "code": str(ErrorCode.WEBSOCKET_FAILED),
                            "message": (
                                "Real-time connection to the server has been lost after "
                                f"{self.max_reconnect_attempts} attempts. "
                                "Activity data is still being saved locally and will sync "
                                "when the connection is restored. "
                                "Check your internet connection or firewall settings."
                            ),
                            "recoverable": True,
                            "source": "websocket",
                        })
                except Exception as _notify_err:
                    logger.debug("[WebSocket] Could not notify Electron: %s", _notify_err)
                return
            
            self._reconnect_attempts += 1
            self._consecutive_failures += 1
            delay = min(self._reconnect_delay, self.reconnect_max_delay)
            
            logger.warning("=" * 80)
            logger.warning("[WebSocket] Reconnecting in %s seconds...", delay)
            logger.warning("[WebSocket] Attempt %s/%s", self._consecutive_failures, self.max_reconnect_attempts)
            logger.warning("=" * 80)
            time.sleep(delay)
            
            # Exponential backoff
            self._reconnect_delay = min(self._reconnect_delay * 2, self.reconnect_max_delay)
            
            # Stop health monitor before reconnecting
            self._stop_health_monitor()
            
            # CRITICAL: Stop existing WebSocket thread before reconnecting
            # This prevents thread accumulation and CPU rise on reconnects
            if self._ws_thread and self._ws_thread.is_alive():
                logger.debug("[WebSocket] Stopping existing WebSocket thread before reconnecting")
                try:
                    # Close WebSocket to signal thread to exit
                    if self.ws:
                        try:
                            self.ws.close()
                        except Exception:
                            pass
                    # Wait for thread to finish (with timeout to prevent blocking)
                    self._ws_thread.join(timeout=3)
                    if self._ws_thread.is_alive():
                        logger.warning("[WebSocket] WebSocket thread did not stop within timeout, continuing anyway")
                    self._ws_thread = None
                except Exception as e:
                    logger.debug("[WebSocket] Error stopping WebSocket thread during reconnect: %s", e)
            
            # Clear connection state before reconnecting
            with self._connection_lock:
                self.is_connected_flag = False
                self.socket_id = None
                self._connection_established_time = None
                self._last_ping_time = None
                self._last_message_time = None
                self._ping_without_subscription_count = 0
                if self.ws:
                    try:
                        self.ws.close()
                    except Exception:
                        pass
                    self.ws = None
                # Clear cached auth signatures tied to previous socket_id
                self.refresh_auth_cache()
            
            # Note: We keep auth cache since token is still valid
            # Only clear socket_id to get a fresh one on reconnect
            
            # Attempt reconnection
            logger.info("[WebSocket] Attempting to reconnect...")
            self.connect()
        finally:
            # CRITICAL: Stop existing reset flag thread before creating new one to prevent accumulation
            if self._reset_flag_thread and self._reset_flag_thread.is_alive():
                logger.debug("[WebSocket] Stopping existing reset flag thread before starting new one")
                try:
                    self._reset_flag_thread.join(timeout=1)
                    if self._reset_flag_thread.is_alive():
                        logger.warning("[WebSocket] Reset flag thread did not stop within timeout, continuing anyway")
                except Exception as e:
                    logger.debug("[WebSocket] Error stopping existing reset flag thread: %s", e)
            
            # Reset reconnecting flag after a delay to allow connection to establish
            def reset_flag():
                time.sleep(5)  # Wait for connection to establish
                with self._connection_lock:
                    self._reconnecting = False
            
            self._reset_flag_thread = threading.Thread(target=reset_flag, daemon=True, name="WebSocketResetFlag")
            self._reset_flag_thread.start()
            # Register worker thread for tracking
            from ws_package.thread_manager import register_worker
            register_worker("WebSocketResetFlag", self._reset_flag_thread)
    
    def _start_health_monitor(self):
        """Start lightweight connection health monitor (no macOS event taps)."""
        if not self._health_monitor_enabled:
            return
        
        # Stop existing monitor if running
        self._stop_health_monitor()
        
        # Start new monitor thread
        self._health_monitor_thread = threading.Thread(
            target=self._health_monitor_loop,
            name="WebSocketHealthMonitor",
            daemon=True
        )
        self._health_monitor_thread.start()
        logger.debug("[WebSocket] Health monitor started")
    
    def _stop_health_monitor(self):
        """Stop connection health monitor."""
        was_enabled = self._health_monitor_enabled
        self._health_monitor_enabled = False
        if self._health_monitor_thread and self._health_monitor_thread.is_alive():
            # Thread will stop on next check when _health_monitor_enabled is False
            self._health_monitor_thread.join(timeout=2)
        self._health_monitor_thread = None
        # Restore enabled flag if it was enabled (for reconnection)
        if was_enabled:
            self._health_monitor_enabled = True
    
    def _health_monitor_loop(self):
        """
        Lightweight health monitor loop - checks connection activity without using macOS APIs.
        Uses only time-based checks and thread status - no event taps or system calls.
        """
        logger.debug("[WebSocket] Health monitor loop started")
        
        while self._health_monitor_enabled:
            try:
                time.sleep(self._health_check_interval)
                
                if not self._health_monitor_enabled:
                    break
                
                # Check if connection is supposed to be alive
                if not self.is_connected_flag or not self.socket_id:
                    continue  # Not connected, skip check
                
                current_time = time.time()
                
                # Check 1: WebSocket thread is alive (lightweight check)
                ws_thread_alive = self._ws_thread and self._ws_thread.is_alive()
                
                # Check 2: Last message received time (lightweight timestamp check)
                time_since_last_message = None
                if self._last_message_time:
                    time_since_last_message = current_time - self._last_message_time
                
                # Check 3: Connection established time
                time_since_connection = None
                if self._connection_established_time:
                    time_since_connection = current_time - self._connection_established_time
                
                # Detect silent connection failure
                # If we've been connected for more than 30 seconds but haven't received any messages
                # for more than max_silence_seconds, connection is likely dead
                if (time_since_connection and time_since_connection > 30 and 
                    time_since_last_message and time_since_last_message > self._max_silence_seconds):
                    logger.warning("=" * 80)
                    logger.warning("[WebSocket] ⚠ SILENT CONNECTION FAILURE DETECTED")
                    logger.warning("[WebSocket] Connection established %ss ago", time_since_connection)
                    logger.warning("[WebSocket] No messages received for %ss", time_since_last_message)
                    logger.warning("[WebSocket] WebSocket thread alive: %s", ws_thread_alive)
                    logger.warning("[WebSocket] Connection appears dead - triggering reconnection...")
                    logger.warning("=" * 80)
                    
                    # Close connection and trigger reconnection
                    with self._connection_lock:
                        if self.ws:
                            try:
                                self.ws.close()
                            except Exception as e:
                                logger.debug("Error closing dead connection: %s", e)
                            self.ws = None
                        self.is_connected_flag = False
                        self.socket_id = None
                        self._last_message_time = None
                    
                    # Trigger reconnection if enabled
                    if self.reconnect_enabled and not (self._stop_event and self._stop_event.is_set()):
                        # CRITICAL: Stop existing reconnect thread before creating new one to prevent accumulation
                        if self._reconnect_thread and self._reconnect_thread.is_alive():
                            logger.debug("[WebSocket] Stopping existing reconnect thread (from health monitor) before starting new one")
                            try:
                                self._reconnect_thread.join(timeout=1)
                                if self._reconnect_thread.is_alive():
                                    logger.warning("[WebSocket] Reconnect thread did not stop within timeout, continuing anyway")
                            except Exception as e:
                                logger.debug("[WebSocket] Error stopping existing reconnect thread: %s", e)
                        
                        def trigger_reconnect():
                            time.sleep(1)  # Brief delay
                            self._reconnect_with_backoff()
                        
                        self._reconnect_thread = threading.Thread(
                            target=trigger_reconnect,
                            name="WebSocketReconnectFromHealth",
                            daemon=True
                        )
                        self._reconnect_thread.start()
                        # Register worker thread for tracking
                        from ws_package.thread_manager import register_worker
                        register_worker("WebSocketReconnectFromHealth", self._reconnect_thread)
                    
                    # Stop health monitor (will restart on next connection)
                    break
                
                # Log periodic health status (debug level to avoid spam)
                if time_since_last_message is not None:
                    logger.debug("[WebSocket] Health check: Thread alive=%s, Last message %.1fs ago, Connected %.1fs ago",
                               ws_thread_alive, time_since_last_message, time_since_connection)
                
            except Exception as e:
                logger.error("[WebSocket] Health monitor error: %s", e, exc_info=True)
                # Continue monitoring despite errors
                time.sleep(self._health_check_interval)
        
        logger.debug("[WebSocket] Health monitor loop stopped")
    
    def disconnect(self):
        """Disconnect WebSocket gracefully."""
        logger.info("Disconnecting WebSocket...")
        self.reconnect_enabled = False
        
        # Stop health monitor
        self._stop_health_monitor()
        
        with self._connection_lock:
            if self.ws:
                try:
                    self.ws.close()
                except Exception as e:
                    logger.error("Error closing WebSocket: %s", e)
                self.ws = None
            
            self.is_connected_flag = False
            self.socket_id = None
            self._last_message_time = None
        
        # Wait for thread to finish
        if self._ws_thread and self._ws_thread.is_alive():
            self._ws_thread.join(timeout=5)
    
    def is_connected(self) -> bool:
        """Check if WebSocket is connected."""
        return self.is_connected_flag and self.ws is not None
    
    def get_socket_id(self) -> Optional[str]:
        """Get current socket ID."""
        return self.socket_id
    
    def get_connection_status(self) -> Dict[str, Any]:
        """
        Get detailed connection status.
        
        Returns:
            dict: Connection status information including:
                - connected: bool
                - socket_id: str or None
                - subscribed_channels: list
                - reconnect_enabled: bool
                - reconnect_attempts: int
                - health_monitor_enabled: bool
                - last_message_seconds_ago: float or None
        """
        with self._channels_lock:
            channels = list(self.subscribed_channels.keys())
        
        # Calculate time since last message (lightweight, no system calls)
        last_message_seconds_ago = None
        if self._last_message_time:
            last_message_seconds_ago = time.time() - self._last_message_time
        
        return {
            "connected": self.is_connected(),
            "socket_id": self.socket_id,
            "subscribed_channels": channels,
            "reconnect_enabled": self.reconnect_enabled,
            "reconnect_attempts": self._reconnect_attempts,
            "websocket_url": self.websocket_url,
            "health_monitor_enabled": self._health_monitor_enabled,
            "last_message_seconds_ago": last_message_seconds_ago
        }
    
    def log_connection_status(self):
        """Log current connection status (useful for debugging)."""
        status = self.get_connection_status()
        logger.info("=" * 80)
        logger.info("[WebSocket] Connection Status:")
        logger.info("  Connected: %s", '✓ YES' if status['connected'] else '✗ NO')
        logger.info("  Socket ID: %s", status['socket_id'] or 'N/A')
        logger.info("  Subscribed Channels: %s", len(status['subscribed_channels']))
        if status['subscribed_channels']:
            for channel in status['subscribed_channels']:
                logger.info("    - %s", channel)
        logger.info("  Reconnect Enabled: %s", status['reconnect_enabled'])
        logger.info("  Reconnect Attempts: %s", status['reconnect_attempts'])
        logger.info("  WebSocket URL: %s", status['websocket_url'])
        logger.info("  Health Monitor: %s", '✓ Enabled' if status['health_monitor_enabled'] else '✗ Disabled')
        if status['last_message_seconds_ago'] is not None:
            logger.info("  Last Message: %ss ago", status['last_message_seconds_ago'])
        else:
            logger.info("  Last Message: Never (no messages received yet)")
        logger.info("=" * 80)
    
    def get_subscribed_channels(self) -> List[str]:
        """Get list of subscribed channels."""
        with self._channels_lock:
            return list(self.subscribed_channels.keys())
    
    def refresh_auth_cache(self):
        """Clear cached auth signatures."""
        with self._auth_cache_lock:
            self._auth_cache.clear()
        logger.info("Auth cache cleared")
    
    def set_reconnect_enabled(self, enabled: bool):
        """Enable or disable automatic reconnection."""
        self.reconnect_enabled = enabled
        logger.info("Reconnection %s", 'enabled' if enabled else 'disabled')
    
    def _setup_app_control_handlers(self):
        """Setup default app control event handlers."""
        if not self.activity_controller:
            logger.warning("ActivityController not provided, app control handlers not available")
            return
        
        # Register handlers for each app control event
        self.register_app_control_handler("app.control.screenshot", self._handle_screenshot_control)
        self.register_app_control_handler("app.control.tracking", self._handle_tracking_control)
        self.register_app_control_handler("app.control.sync", self._handle_sync_control)
        # app.control.update handler removed - update functionality has been removed
        self.register_app_control_handler("app.control.restart", self._handle_restart_control)
        self.register_app_control_handler("app.control.shutdown", self._handle_shutdown_control)
        self.register_app_control_handler("app.control.settings", self._handle_settings_control)
    
    def _handle_screenshot_control(self, data: Any):
        """Handle screenshot control event (enable/disable)."""
        try:
            if isinstance(data, str):
                data = json.loads(data) if data else {}
            elif data is None:
                data = {}
            
            enabled = data.get('enabled', True)
            logger.info("Received screenshot control: enabled=%s", enabled)
            
            if self.activity_controller and hasattr(self.activity_controller, 'screenshot_controller'):
                # Control screenshot capture via screenshot controller
                # Note: This may require adding methods to ScreenshotController
                logger.info("Screenshot control: %s", 'enabled' if enabled else 'disabled')
            else:
                logger.warning("Screenshot controller not available")
        except Exception as e:
            logger.error("Error handling screenshot control: %s", e)
    
    def _update_execution_status_screenshot(self, log_id: Optional[int], is_executed: bool, error_message: Optional[str] = None, action: Optional[str] = None, screenshot_url=None):
        """
        Update execution status for a screenshot action, including the captured S3 URL(s).

        Args:
            log_id: The ID of the action log (from WebSocket event)
            is_executed: True if screenshot was captured successfully, False otherwise
            error_message: Optional error message if execution failed
            action: Action label (e.g. "screen")
            screenshot_url: S3 URL string or list of URLs for the captured screenshot(s)
        """
        if not log_id:
            logger.warning("[WebSocket] Cannot update execution status: log_id is missing")
            return

        try:
            url = f"{self.api_base_url}/api/websocket/action/update-execution-status"

            payload = {
                "log_id": log_id,
                "is_executed": is_executed,
                "action": action,
                "image_url": screenshot_url,
            }

            if error_message:
                payload["error_message"] = error_message
            else:
                os_name = platform.system()
                os_version = platform.release()
                payload["client_info"] = f"Python {os_name} {os_version}"

            if self.activity_controller and hasattr(self.activity_controller, 'get_os_type_caps'):
                device_type = self.activity_controller.get_os_type_caps()
            else:
                platform_name = platform.system()
                device_type = "Mac" if platform_name == "Darwin" else platform_name

            app_version = None
            if self.activity_controller and hasattr(self.activity_controller, 'appVersion'):
                app_version = str(self.activity_controller.appVersion)

            os_version = get_detailed_os_version()

            headers = {
                "Authorization": f"Bearer {self.user_entity.token}",
                "Content-Type": "application/json",
                "OrgID": str(self.user_entity.org_id),
                "UserID": str(self.user_entity.user_id),
                "deviceType": device_type,
                "osversion": os_version,
            }

            if app_version:
                headers["appversion"] = app_version

            from ws_package.utils.desktop_key import apply_desktop_key
            headers = apply_desktop_key(headers)

            logger.info("[WebSocket] screenshot execution status payload: %s", payload)

            log_testing_request_headers(logger, "POST", url, headers, payload)
            response = requests.post(url, headers=headers, json=payload, timeout=10)
            log_api_response_summary(
                logger,
                api_name_from_url(url),
                response.status_code,
                response.text,
            )

            if response.status_code == 200:
                try:
                    if not response.text or not response.text.strip():
                        logger.warning("[WebSocket] Empty response from screenshot execution status API")
                        return
                    response_data = response.json()
                    if response_data and response_data.get('success'):
                        logger.info("[WebSocket] Screenshot execution status updated for log_id: %s", log_id)
                    else:
                        logger.warning("[WebSocket] Screenshot execution status update failed: %s", response_data)
                except ValueError as e:
                    logger.warning("[WebSocket] Invalid JSON in screenshot execution status response: %s", e)
            else:
                logger.warning("[WebSocket] Screenshot execution status HTTP %s: %s", response.status_code, response.text[:500])
        except requests.exceptions.RequestException as e:
            logger.error("[WebSocket] Network error updating screenshot execution status: %s", e)
        except Exception as e:
            logger.error("[WebSocket] Error updating screenshot execution status: %s", e, exc_info=True)

    def _update_execution_status(self, log_id: Optional[int], is_executed: bool, error_message: Optional[str] = None, action: Optional[str] = None):
        """
        Update execution status for a tracking control action.
        
        Args:
            log_id: The ID of the action log (from WebSocket event)
            is_executed: True if action was executed successfully, False otherwise
            error_message: Optional error message if execution failed
            action: Optional action type (e.g. "save_screen") for save_screenshot events
        """
        if not log_id:
            logger.warning("[WebSocket] Cannot update execution status: log_id is missing")
            return
        
        try:
            url = f"{self.api_base_url}/api/websocket/action/update-execution-status"
            
            payload = {
                "log_id": log_id,
                "is_executed": is_executed,
            }
            if action is not None:
                payload["action"] = action
            
            if error_message:
                payload["error_message"] = error_message
            else:
                # Add client info for successful execution
                os_name = platform.system()
                os_version = platform.release()
                payload["client_info"] = f"Python {os_name} {os_version}"
            
            # Get device type - use activity_controller if available, otherwise derive from platform
            if self.activity_controller and hasattr(self.activity_controller, 'get_os_type_caps'):
                device_type = self.activity_controller.get_os_type_caps()
            else:
                # Fallback: derive device type directly
                platform_name = platform.system()
                device_type = "Mac" if platform_name == "Darwin" else platform_name
            
            # Get app version from activity_controller
            app_version = None
            if self.activity_controller and hasattr(self.activity_controller, 'appVersion'):
                app_version = str(self.activity_controller.appVersion)
            
            # Get detailed OS version
            os_version = get_detailed_os_version()
            
            # Build headers dictionary
            headers = {
                "Authorization": f"Bearer {self.user_entity.token}",
                "Content-Type": "application/json",
                "OrgID": str(self.user_entity.org_id),
                "UserID": str(self.user_entity.user_id),
                "deviceType": device_type,
                "osversion": os_version,
            }
            
            # Add app_version if available
            if app_version:
                headers["appversion"] = app_version
            
            from ws_package.utils.desktop_key import apply_desktop_key
            headers = apply_desktop_key(headers)
            
            log_testing_request_headers(logger, "POST", url, headers, payload)
            response = requests.post(url, headers=headers, json=payload, timeout=10)
            log_api_response_summary(
                logger,
                api_name_from_url(url),
                response.status_code,
                response.text,
            )
            
            if response.status_code == 200:
                try:
                    # Check if response has content
                    if not response.text or not response.text.strip():
                        logger.warning("[WebSocket] Empty response body from execution status API (status 200)")
                        return
                    
                    response_data = response.json()
                    
                    if response_data and response_data.get('success'):
                        logger.info("[WebSocket] Execution status updated successfully for log_id: %s, is_executed: %s", log_id, is_executed)
                    else:
                        logger.warning("[WebSocket] Failed to update execution status: %s", response_data)
                except ValueError as e:
                    # JSON decode error
                    logger.warning("[WebSocket] Invalid JSON response from execution status API: %s", e)
                    logger.warning("[WebSocket] Response text: %s", response.text[:500])
                except KeyError as e:
                    logger.warning("[WebSocket] Missing key in response: %s, Response: %s", e, response_data)
            else:
                logger.warning("[WebSocket] Failed to update execution status: HTTP %s, Response: %s", response.status_code, response.text[:500])
        except requests.exceptions.RequestException as e:
            logger.error("[WebSocket] Network error updating execution status: %s", e)
        except Exception as e:
            logger.error("[WebSocket] Error updating execution status: %s", e, exc_info=True)
    
    def _handle_tracking_control(self, data: Any):
        """Handle tracking control event (start/stop)."""
        log_id = None
        try:
            if isinstance(data, str):
                data = json.loads(data) if data else {}
            elif data is None:
                data = {}
            
            # Handle nested data structure: {"data": "{\"action\":\"stop\", \"log_id\": 123}"}
            # If data has a 'data' key that's a string, parse it
            if isinstance(data, dict) and 'data' in data and isinstance(data['data'], str):
                try:
                    # Parse the nested JSON string
                    nested_data = json.loads(data['data'])
                    # If nested_data is also a dict with 'data', unwrap again (double JSON-encoded)
                    if isinstance(nested_data, dict) and 'data' in nested_data:
                        if isinstance(nested_data['data'], str):
                            nested_data = json.loads(nested_data['data'])
                        else:
                            nested_data = nested_data['data']
                    data = nested_data
                    logger.debug("[WebSocket] Unwrapped nested data structure: %s", data)
                except (json.JSONDecodeError, TypeError) as e:
                    logger.warning("[WebSocket] Failed to parse nested data: %s, using original data", e)
                    # Keep original data if parsing fails
            
            action = data.get('action', 'start')  # 'start' or 'stop'
            log_id = data.get('log_id')  # Extract log_id for execution status update
            logger.info("[WebSocket] Received tracking control: action=%s, log_id=%s, parsed data: %s", action, log_id, data)
            
            if not self.activity_controller:
                logger.warning("ActivityController not available for tracking control")
                if log_id:
                    self._update_execution_status(log_id, False, "ActivityController not available")
                return
            
            if action == 'stop':
                try:
                    # Save status persistently
                    set_tracker_status('stop')
                    # Stop tracking by setting force_stop flag
                    self.activity_controller.force_stop = True
                    self.activity_controller.status = False
                    # Reset resume log flag so resume message appears when tracking starts again
                    if hasattr(self.activity_controller, '_tracking_resumed_logged'):
                        self.activity_controller._tracking_resumed_logged = False
                    logger.info("=" * 80)
                    logger.info("[WebSocket] ✗ Tracking STOPPED via WebSocket command")
                    logger.info("[WebSocket] Status saved persistently - tracking will stop on next loop iteration")
                    logger.info("=" * 80)
                    # Update execution status on success
                    if log_id:
                        self._update_execution_status(log_id, True)
                except Exception as e:
                    logger.error("[WebSocket] Error stopping tracking: %s", e, exc_info=True)
                    # Update execution status on failure
                    if log_id:
                        self._update_execution_status(log_id, False, f"Failed to stop tracking: {str(e)}")
            elif action == 'start':
                try:
                    # Save status persistently
                    set_tracker_status('start')
                    # Start tracking by clearing force_stop flag
                    self.activity_controller.force_stop = False
                    self.activity_controller.status = True
                    # Reset resume log flag so resume message appears
                    if hasattr(self.activity_controller, '_tracking_resumed_logged'):
                        self.activity_controller._tracking_resumed_logged = False
                    logger.info("=" * 80)
                    logger.info("[WebSocket] ✓ Tracking STARTED via WebSocket command")
                    logger.info("[WebSocket] Status saved persistently - tracking will resume on next loop iteration")
                    logger.info("=" * 80)
                    # Update execution status on success
                    if log_id:
                        self._update_execution_status(log_id, True)
                except Exception as e:
                    logger.error("[WebSocket] Error starting tracking: %s", e, exc_info=True)
                    # Update execution status on failure
                    if log_id:
                        self._update_execution_status(log_id, False, f"Failed to start tracking: {str(e)}")
            elif action == 'screenshot':
                try:
                    logger.info("=" * 80)
                    logger.info("[WebSocket] === RECEIVED SCREENSHOT ACTION VIA TRACKING CONTROL ===")
                    logger.info("[WebSocket] log_id=%s", log_id)
                    logger.info("=" * 80)

                    if not hasattr(self.activity_controller, 'screenshot_controller'):
                        logger.error("[WebSocket] ScreenshotController not available in ActivityController")
                        if log_id:
                            self._update_execution_status(log_id, False, "ScreenshotController not available")
                        return

                    screenshot_controller = self.activity_controller.screenshot_controller
                    user_entity = self.user_entity

                    def run_screenshot_in_thread():
                        """Run screenshot capture in a new thread with its own event loop."""
                        try:
                            logger.info("[WebSocket] [THREAD] Starting on-demand screenshot capture thread...")
                            new_loop = asyncio.new_event_loop()
                            asyncio.set_event_loop(new_loop)

                            async def capture_screenshot():
                                try:
                                    logger.info("[WebSocket] [COROUTINE] Triggering on-demand screenshot capture...")
                                    stop_event = self._stop_event if self._stop_event else None
                                    result = await screenshot_controller.on_demand_screenshot(user_entity, stop_event)
                                    success, screenshot_url = result if isinstance(result, tuple) else (result, None)
                                    logger.info("[WebSocket] [COROUTINE] on_demand_screenshot returned: success=%s, url=%s", success, screenshot_url)
                                    if success:
                                        logger.info("[WebSocket] ✓ On-demand screenshot captured. URL: %s", screenshot_url)
                                        if log_id:
                                            self._update_execution_status_screenshot(log_id, True, action="screen", screenshot_url=screenshot_url)
                                    else:
                                        logger.warning("[WebSocket] ✗ On-demand screenshot capture failed")
                                        if log_id:
                                            self._update_execution_status(log_id, False, "Screenshot capture failed")
                                    return success
                                except Exception as e:
                                    logger.error("[WebSocket] [COROUTINE] Error capturing screenshot: %s", e, exc_info=True)
                                    if log_id:
                                        self._update_execution_status(log_id, False, f"Error capturing screenshot: {str(e)}")
                                    return False

                            logger.info("[WebSocket] [THREAD] Running on-demand screenshot in event loop...")
                            result = new_loop.run_until_complete(capture_screenshot())
                            logger.info("[WebSocket] [THREAD] On-demand screenshot completed, result: %s", result)
                            new_loop.close()
                        except Exception as e:
                            logger.error("[WebSocket] [THREAD] Error in screenshot capture thread: %s", e, exc_info=True)
                            if log_id:
                                self._update_execution_status(log_id, False, f"Error in screenshot thread: {str(e)}")

                    screenshot_thread = threading.Thread(
                        target=run_screenshot_in_thread,
                        name="OnDemandScreenshotCapture",
                        daemon=True,
                    )
                    screenshot_thread.start()
                    logger.info("[WebSocket] ✓ On-demand screenshot capture thread started")
                    logger.info("=" * 80)

                except Exception as e:
                    logger.error("[WebSocket] Error handling screenshot action: %s", e, exc_info=True)
                    if log_id:
                        self._update_execution_status(log_id, False, f"Error handling screenshot action: {str(e)}")
            elif action == "save_screenshot":
                try:
                    logger.info("=" * 80)
                    logger.info("[WebSocket] === RECEIVED SAVE SCREENSHOT ACTION VIA TRACKING CONTROL ===")
                    logger.info("=" * 80)
                    image_url_raw = data.get("image_url")
                    triggered_by = data.get("triggered_by")
                    image_urls = []
                    if image_url_raw is not None:
                        if isinstance(image_url_raw, list):
                            image_urls = [str(u).strip() for u in image_url_raw if u and str(u).strip()]
                        elif isinstance(image_url_raw, str) and image_url_raw.strip():
                            image_urls = [image_url_raw.strip()]

                    if not image_urls:
                        logger.warning("[WebSocket] save_screenshot: missing or empty image_url in payload")
                        if log_id:
                            self._update_execution_status(log_id, False, "image_url required for save_screenshot", action="save_screen")
                        return
                    if not hasattr(self.activity_controller, "screenshot_controller"):
                        logger.error("[WebSocket] ScreenshotController not available in ActivityController")
                        if log_id:
                            self._update_execution_status(log_id, False, "ScreenshotController not available", action="save_screen")
                        return
                    screenshot_controller = self.activity_controller.screenshot_controller
                    user_entity = self.user_entity

                    def run_save_screenshot_in_thread():
                        try:
                            logger.info("[WebSocket] [THREAD] Starting save screenshot (from %d image_url(s)) thread...", len(image_urls))
                            new_loop = asyncio.new_event_loop()
                            asyncio.set_event_loop(new_loop)

                            async def save_screenshot_from_url():
                                try:
                                    if hasattr(self.activity_controller, "has_on_demand"):
                                        self.activity_controller.has_on_demand = True
                                    logger.info("[WebSocket] [COROUTINE] Saving %d screenshot(s) from image_url(s) to DB...", len(image_urls))
                                    success_count = 0
                                    for url in image_urls:
                                        result = await screenshot_controller.save_screenshot_from_remote_url(
                                            user_entity, url, triggered_by=triggered_by
                                        )
                                        if result:
                                            success_count += 1
                                    success = success_count > 0
                                    logger.info("[WebSocket] [COROUTINE] save_screenshot_from_remote_url: %d/%d saved", success_count, len(image_urls))
                                    if success:
                                        logger.info("[WebSocket] ✓ Screenshot(s) (image_url) saved to DB: %d/%d", success_count, len(image_urls))
                                        if log_id:
                                            self._update_execution_status(log_id, True, action="save_screen")
                                    else:
                                        logger.warning("[WebSocket] ✗ Screenshot save to DB failed (0/%d)", len(image_urls))
                                        if log_id:
                                            self._update_execution_status(log_id, False, "Screenshot save failed", action="save_screen")
                                    return success
                                except Exception as e:
                                    logger.error("[WebSocket] [COROUTINE] Error saving screenshot: %s", e, exc_info=True)
                                    if log_id:
                                        self._update_execution_status(log_id, False, f"Error saving screenshot: {str(e)}", action="save_screen")
                                    return False

                            result = new_loop.run_until_complete(save_screenshot_from_url())
                            logger.info("[WebSocket] [THREAD] Save screenshot completed, result: %s", result)
                            new_loop.close()
                        except Exception as e:
                            logger.error("[WebSocket] [THREAD] Error in save screenshot thread: %s", e, exc_info=True)
                            if log_id:
                                self._update_execution_status(log_id, False, f"Error in save screenshot thread: {str(e)}", action="save_screen")

                    logger.info("[WebSocket] Starting save screenshot thread (%d image_url(s) -> DB)...", len(image_urls))
                    save_screenshot_thread = threading.Thread(
                        target=run_save_screenshot_in_thread,
                        name="SaveScreenshot",
                        daemon=True,
                    )
                    save_screenshot_thread.start()
                    logger.info("[WebSocket] ✓ Save screenshot thread started")
                except Exception as e:
                    logger.error("[WebSocket] Error handling save screenshot action: %s", e, exc_info=True)
                    if log_id:
                        self._update_execution_status(log_id, False, f"Error handling save screenshot action: {str(e)}", action="save_screen")
            else:
                logger.warning("[WebSocket] Unknown tracking action: %s", action)
                if log_id:
                    self._update_execution_status(log_id, False, f"Unknown action: {action}")
        except Exception as e:
            logger.error("[WebSocket] Error handling tracking control: %s", e, exc_info=True)
            # Update execution status on failure
            if log_id:
                self._update_execution_status(log_id, False, f"Error handling tracking control: {str(e)}")
    
    def _handle_sync_control(self, data: Any):
        """Handle sync control event (force data sync)."""
        try:
            logger.info("Received sync control event - triggering data sync")
            # The existing push thread handles syncing, so we just log it
            # In the future, could trigger immediate sync if needed
            logger.info("Sync control acknowledged (sync handled by existing push thread)")
        except Exception as e:
            logger.error("Error handling sync control: %s", e)
    
    def _handle_restart_control(self, data: Any):
        """Handle restart control event (restart app)."""
        try:
            logger.info("Received restart control event - initiating graceful restart")
            # Set flag to trigger restart
            # The main loop should check this flag and restart
            import sys
            import os
            logger.info("Restart command received - app will restart")
            # In production, this would trigger a graceful restart
            # For now, we log it
            logger.warning("Restart functionality needs to be integrated with main app loop")
        except Exception as e:
            logger.error("Error handling restart control: %s", e)
    
    def _handle_shutdown_control(self, data: Any):
        """Handle shutdown control event (shutdown app)."""
        try:
            logger.info("Received shutdown control event - initiating graceful shutdown")
            # Set stop event to trigger shutdown
            if self._stop_event:
                self._stop_event.set()
            # Also set force_stop on activity controller
            if self.activity_controller:
                self.activity_controller.force_stop = True
            logger.info("Shutdown command processed")
        except Exception as e:
            logger.error("Error handling shutdown control: %s", e)
    
    def _handle_settings_control(self, data: Any):
        """Handle settings control event (update app settings)."""
        try:
            if isinstance(data, str):
                data = json.loads(data) if data else {}
            elif data is None:
                data = {}
            
            logger.info("Received settings control event: %s", data)
            # Update settings via config system
            # This would require implementing settings update logic
            logger.info("Settings control acknowledged (settings update needs implementation)")
        except Exception as e:
            logger.error("Error handling settings control: %s", e)
    
    async def start(self, stop_event: threading.Event):
        """
        Start WebSocket service (async interface for thread integration).
        
        Args:
            stop_event: Event to signal when to stop
        """
        logger.info("=" * 80)
        logger.info("[WebSocket] === STARTING WEBSOCKET SERVICE ===")
        logger.info("[WebSocket] WebSocket URL: %s", self.websocket_url)
        logger.info("[WebSocket] Broadcasting Auth URL: %s", self.broadcasting_auth_url)
        logger.info("[WebSocket] Reconnect Enabled: %s", self.reconnect_enabled)
        logger.info("=" * 80)
        
        self._stop_event = stop_event
        
        # Setup app control handlers
        logger.info("[WebSocket] Setting up app control handlers...")
        self._setup_app_control_handlers()
        logger.info("[WebSocket] ✓ App control handlers registered")
        
        # Connect
        logger.info("[WebSocket] Attempting to connect to WebSocket server...")
        if not self.connect():
            logger.error("[WebSocket] ✗ Failed to start WebSocket connection")
            return
        
        logger.info("[WebSocket] Connection initiated, waiting for connection established...")
        
        # Wait a bit longer for connection to establish and log status
        await asyncio.sleep(3)
        logger.info("[WebSocket] Checking connection status...")
        self.log_connection_status()
        
        # Wait for stop event and periodically log status
        last_status_log = time.time()
        status_log_interval = 300  # Log status every 5 minutes
        
        try:
            while not stop_event.is_set():
                if not self.is_connected_flag and self.reconnect_enabled:
                    # Connection lost, will be handled by on_close
                    time.sleep(1)
                else:
                    # Periodically log connection status
                    current_time = time.time()
                    if current_time - last_status_log >= status_log_interval:
                        self.log_connection_status()
                        last_status_log = current_time
                    
                    time.sleep(5)  # Check every 5 seconds
        except Exception as e:
            logger.error("[WebSocket] Error in WebSocket service loop: %s", e)
        finally:
            logger.info("[WebSocket] Stopping WebSocket service...")
            self.disconnect()
            logger.info("[WebSocket] WebSocket service stopped")

