# CRITICAL: Send ready signal IMMEDIATELY at module import time
# This happens before any heavy imports, so Electron knows Python is starting
import json
import sys
import os
import platform

# Python app version — read from VERSION file at the repo root.
# Fallback to hardcoded string only if the file is missing (e.g. during
# local development without the full repo layout).
# Single source of truth: /VERSION (one line, semver-like, no "v" prefix).
def _read_version() -> str:
    try:
        _here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        _version_file = os.path.join(_here, "VERSION")
        with open(_version_file, encoding="utf-8") as _f:
            return _f.read().strip()
    except Exception:
        return "1.2"  # bump this only when VERSION file is absent

PYTHON_APP_VERSION = _read_version()

# Safe print function to handle broken pipe errors (e.g., after system sleep)
def safe_print(message: str):
    """Safely print to stdout, handling BrokenPipeError gracefully"""
    try:
        print(message, flush=True)
        sys.stdout.flush()
    except (OSError, IOError, ValueError, AttributeError, BrokenPipeError):
        # Silently ignore stdout errors (stdout might be closed by Electron or broken after sleep)
        # BrokenPipeError is a subclass of OSError, but explicitly catch it for clarity
        pass

# Send ready signal FIRST before any heavy imports (visible/Electron only)
try:
    from ws_package.utils.build_mode import read_build_mode_at_import
    _silent_at_import = read_build_mode_at_import() == "silent"
except Exception:
    _silent_at_import = False

if not _silent_at_import:
    try:
        ready_msg = json.dumps({"type": "ready", "data": {"status": "starting", "message": "Python process loading", "version": PYTHON_APP_VERSION}})
        safe_print(ready_msg)
    except (OSError, IOError, ValueError, AttributeError):
        pass

# Standard library imports (fast)
import asyncio
import time, threading
import subprocess
import atexit
from pathlib import Path
import socket, datetime
from threading import Thread, Event
import queue
import zoneinfo
from tzlocal import get_localzone


# Platform startup — windows.py is stdlib-only so it can be imported before ws_package.
from ws_package.startup.windows import (
    _quarantine_stale_appdata_install,
    _ensure_silent_appdata_install,
)
_quarantine_stale_appdata_install()
_ensure_silent_appdata_install(_silent_at_import)


# CRITICAL: Defer ALL heavy imports to main() to avoid 15-second startup delay
# These imports happen at module level and block until main() is called
# Only import bootstrap_tcp here (lightweight, needed early)
from ws_package.bootstrap_tcp import bootstrap_tcp
from ws_package.log.writer import LogWriter, initialize_thread_logging
from ws_package.utils.error_codes import ErrorCode, ExitCode

# Lightweight runtime imports only — everything heavy is imported inside the
# functions that first need them (SQLAlchemy, requests, boto3, etc.).
from ws_package import thread_manager

sys.path.insert(1, "/".join(os.path.realpath(__file__).split("/")[0:-2]))

SINGLE_INSTANCE_PORT = 12345
instance_socket = None
logger = LogWriter.file(__name__)

# Monitoring session threads only — db_writer/websocket/resource_monitor live for the process.
ACTIVITY_MANAGED_THREAD_NAMES = frozenset({"device", "screenshot", "push", "status"})


def start_db_writer_thread():
    """Start the dedicated database writer thread (single writer for the write queue)."""
    stop_event = Event()
    try:
        async_stop_event = asyncio.Event()

        def run_writer():
            try:
                from ws_package.services.db_writer_service import db_writer_service
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                loop.run_until_complete(db_writer_service.start(async_stop_event))
            except Exception as e:
                logger.error("Database writer thread error: %s", e, exc_info=True)

        thread = Thread(target=run_writer, name="db_writer", daemon=True)
        thread.start()

        thread_manager.thread_controls["db_writer"] = {
            "thread": thread,
            "stop_event": stop_event,
            "async_stop_event": async_stop_event,
            "restart_count": 0,
            "last_restart": time.time(),
        }
        logger.info("Database writer thread started")
    except Exception as e:
        logger.error("Failed to start database writer thread: %s", e, exc_info=True)


def ensure_db_writer_thread_running():
    """Ensure db_writer is alive; restart if a prior start/stop killed it."""
    info = thread_manager.thread_controls.get("db_writer")
    if info:
        thread = info.get("thread")
        if thread and thread.is_alive():
            try:
                from ws_package.services.db_writer_service import db_writer_service
                if db_writer_service.is_running:
                    return
            except Exception:
                pass
    logger.warning("Database writer thread not running — restarting")
    start_db_writer_thread()


# ── Platform startup modules ─────────────────────────────────────────────────
# Each module handles one OS completely. They are stdlib-safe at import time;
# only setup_*() functions do ws_package-level work.
import ws_package.startup.macos as _mac
import ws_package.startup.linux as _lin
import ws_package.startup.windows as _win


def get_app_support_dir() -> str:
    """Application Support / data directory for the current platform."""
    return _mac.get_app_support_dir(logger)


def check_and_run_setup() -> bool:
    """Dispatch first-run setup to the correct platform module."""
    if not getattr(sys, "frozen", False):
        return True
    if sys.platform == "win32":
        return _win.setup_windows(logger)
    if sys.platform.startswith("linux"):
        return _lin.setup_linux(logger)
    if sys.platform == "darwin":
        return _mac.setup_macos(logger)
    return True


def start_permission_monitor() -> None:
    _mac.start_permission_monitor(logger)


def request_macos_permissions() -> None:
    _mac.request_macos_permissions_non_blocking(logger)


def verify_and_fix_launchagent() -> bool:
    return _mac.verify_and_fix_launchagent(logger)


# ----------------- DATABASE FUNCTIONS -----------------
async def get_last_counter():
    from sqlalchemy import select
    from ws_package.models.base import ActivityInterval, async_session
    async with async_session() as session:
        query = select(ActivityInterval).order_by(ActivityInterval.counter.desc()).limit(1)
        result = await session.scalars(query)
        last_entry = result.first()
        return last_entry.counter if last_entry else 0

async def get_last_org():
    """
    Get the latest organization configuration.

    CRITICAL FIX: Returns OrgEntity instead of ORM object to prevent lazy-loading
    issues and ensure proper attribute access after session closes.
    """
    from sqlalchemy import select, desc
    from ws_package.models.base import async_session, OrgConfigurations
    from ws_package.entities.org_entity import OrgEntity
    from ws_package.utils.db_lock_manager import db_lock_manager
    
    try:
        with db_lock_manager.read_lock():
            async with async_session() as session:
                query = select(OrgConfigurations).order_by(desc(OrgConfigurations.id)).limit(1)
                result = await session.scalars(query)
                record = result.first()
                
                # CRITICAL FIX: Extract all attributes while session is still open
                if record is None:
                    return None
                
                # Create OrgEntity with all values loaded
                return OrgEntity(
                    org_id=record.org_id,
                    user_id=record.user_id,
                    screenshot_interval=record.screenshot_interval,
                    date=record.date,
                    appandurlstatus=record.appandurlstatus,
                    ssfrequencystatus=record.ssfrequencystatus,
                    screenshotblurstatus=record.screenshotblurstatus,
                    livescreenshotstatus=record.livescreenshotstatus,
                    idlenotes_status=record.idlenotes_status,
                    break_limit=record.break_limit,
                    break_status=record.break_status,
                    notes_char_limit=record.notes_char_limit,
                    idle_limit=record.idle_limit,
                    can_delete_scrnshots=record.can_delete_scrnshots,
                    restrict_timer_to_jobsite=record.restrict_timer_to_jobsite,
                    todos_files_upload_limit=record.todos_files_upload_limit,
                    org_timezone=record.org_timezone,
                    user_timezone=record.user_timezone,
                    user_timezone_gmt=record.user_timezone_gmt,
                    user_timezone_gmt_dst=record.user_timezone_gmt_dst,
                    user_timezone_gmt_shortcode=record.user_timezone_gmt_shortcode,
                    bulkInvite=record.bulkInvite,
                    timeInterval=record.timeInterval,
                    blur_intensity=record.blur_intensity,
                    img_quality=record.img_quality,
                    notification_status=record.notification_status,
                    weekly_limit=getattr(record, 'weekly_limit', None),
                    org_week_start_day=getattr(record, 'org_week_start_day', None),
                    worked_total_hours_week=getattr(record, 'worked_total_hours_week', None),
                    worked_total_hours_week_leaving_today=getattr(record, 'worked_total_hours_week_leaving_today', None),
                    daily_limit=getattr(record, 'daily_limit', None),
                    weekly_limit_hours=getattr(record, 'weekly_limit_hours', None),
                    worked_total_hours_today=getattr(record, 'worked_total_hours_today', None),
                    limit_action=getattr(record, 'limit_action', None),
                    timezone_preference=getattr(record, 'timezone_preference', 1),
                )
    except Exception as e:
        from ws_package.log.writer import LogWriter
        logger = LogWriter.file(__name__)
        logger.error("Error getting last org: %s", e)
        return None

async def get_last_user():
    from sqlalchemy import select, desc
    from ws_package.models.base import async_session, UserDetail
    from ws_package.entities.user_entity import UserEntity
    async with async_session() as session:
        query = select(UserDetail).order_by(desc(UserDetail.id)).limit(1)
        result = await session.execute(query)
        user_data = result.scalars().first()
        return UserEntity(
            username=user_data.username,
            name=user_data.name,
            timezone=user_data.timezone,
            token=user_data.token,
            org_id=user_data.org_id,
            user_id=user_data.user_id,
            sd_token=user_data.sd_token,
            bucket=user_data.bucket,
            timezone_preference=getattr(user_data, "timezone_preference", 1),
        ) if user_data else None

def change(timeInterval):
    return {"5 Min.": 300, "15 Min.": 900}.get(timeInterval, 600)

def ensure_single_instance():
    global instance_socket
    instance_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     # Use the correct directory for process.txt
    process_file_dir = get_app_support_dir()
    process_file_dir = Path(process_file_dir) / "logs"
    os.makedirs(process_file_dir, exist_ok=True)
    process_file_path = os.path.join(process_file_dir, "process.txt")
    logger.processNo(process_file_path, f"{os.getpid()}")
    try:
        instance_socket.bind(("127.0.0.1", SINGLE_INSTANCE_PORT))
        logger.info("Instance started on port %s", SINGLE_INSTANCE_PORT)
    except socket.error:
        logger.critical("%s", ErrorCode.ALREADY_RUNNING)
        sys.exit(ExitCode.STARTUP_FAILURE)

def run_async_in_thread(coro, *args):
    """Run async coroutine in thread with proper exception handling."""
    max_restarts = 5
    restart_count = 0
    restart_delay = 5  # seconds
    
    while restart_count < max_restarts:
        try:
            asyncio.run(coro(*args))
            # If coro completes normally, exit thread
            break
        except Exception as e:
            restart_count += 1
            logger.exception("Uncaught exception in thread (attempt %s/%s): %s", restart_count, max_restarts, e)
            
            if restart_count < max_restarts:
                logger.info("Restarting thread after %s seconds...", restart_delay)
                time.sleep(restart_delay)
                # Exponential backoff for restart delay
                restart_delay = min(restart_delay * 2, 60)
            else:
                logger.error("Thread failed %s times. Thread will exit. Monitoring system will restart it.", max_restarts)
                # Don't let thread exceptions crash the main process
                # The thread monitoring will restart the thread if needed
                break


def _run_coroutine_in_new_loop(coro):
    """
    Helper function to run a coroutine in a new event loop in a thread.
    This is used for heavy initialization that should not block the main thread.
    """
    def _run():
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            try:
                loop.run_until_complete(coro)
            finally:
                loop.close()
        except Exception as e:
            logger.exception("Error running coroutine in new loop: %s", e)
            raise

    return _run


# ----------------- THREAD MONITORING -----------------
def run_thread_with_monitor(name, target, args, stop_event):
    thread = Thread(target=run_async_in_thread, args=(target, *args, stop_event), name=name, daemon=True)
    thread.start()
    logger.info("Thread '%s' started.", name)
    return thread

def monitor_threads(activity, user_entity, stop_monitoring_event: Event):
    from ws_package.utils.resource_recovery import resource_recovery
    # Logger for thread monitoring
    monitor_logger = LogWriter.file(__name__)
    
    def start_named_thread(name, restart=False):
        # CRITICAL: Stop existing thread before creating new one to prevent thread accumulation
        # This prevents CPU rise from multiple threads running simultaneously
        if name in thread_manager.thread_controls:
            old_info = thread_manager.thread_controls[name]
            old_thread = old_info.get("thread")
            old_stop_event = old_info.get("stop_event")
            
            if old_thread and old_thread.is_alive():
                monitor_logger.debug("Stopping existing thread '%s' before starting new one", name)
                try:
                    # Set stop event to signal thread to exit
                    if old_stop_event:
                        old_stop_event.set()
                    # Wait for thread to finish (with timeout to prevent blocking)
                    old_thread.join(timeout=5)
                    if old_thread.is_alive():
                        monitor_logger.warning("Thread '%s' did not stop within timeout, continuing anyway", name)
                except Exception as e:
                    monitor_logger.debug("Error stopping existing thread '%s': %s", name, e)
        
        stop_event = Event()
        try:
            target_func = getattr(activity, f"start_{name}_activity")
        except AttributeError:
            monitor_logger.error("No method found for activity: start_%s_activity", name)
            return

        thread = run_thread_with_monitor(name, target_func, (user_entity,), stop_event)
        
        # Preserve restart_count and last_restart if restarting
        restart_count = 0
        last_restart = time.time()
        if restart and name in thread_manager.thread_controls:
            old_info = thread_manager.thread_controls[name]
            restart_count = old_info.get("restart_count", 0) + 1
            last_restart = time.time()
        
        thread_manager.thread_controls[name] = {
            "thread": thread, 
            "stop_event": stop_event,
            "restart_count": restart_count,
            "last_restart": last_restart
        }
        if restart:
            monitor_logger.info("Thread '%s' restarted (restart_count=%s).", name, restart_count)

    # Set up stack size once (before starting threads)
    if platform.system() == "Windows":
        try:
            threading.stack_size(6 * 1024 * 1024)  # 6 MB
            logger.info("Thread stack size set to %s bytes", threading.stack_size())
        except (ValueError, RuntimeError) as e:
            logger.error("Failed to set thread stack size: %s", e)

    # Start all initial threads
    for name in ["device", "screenshot", "push", "status"]:
        start_named_thread(name)
    
    def start_resource_monitor_service():
        """Start background resource monitor to check and recover resources periodically"""
        if not getattr(sys, 'frozen', False):
            monitor_logger.debug("Resource monitor only runs in PyInstaller bundles")
            return

        # Must NOT alias stop_monitoring_event: monitor_threads shutdown sets each thread's
        # stop_event — if that were the global, stale cleanup would set() after a new session
        # clear() and kill MonitorThread immediately (stop_event_set True, db_writer stopped).
        resource_monitor_stop = Event()

        def monitor_loop():
            monitor_logger.info("Resource monitor thread started")
            check_interval = 300  # Check every 5 minutes
            while not resource_monitor_stop.is_set():
                try:
                    if platform.system() == "Windows":
                        # Check if critical resources are accessible
                        resources_to_check = [
                            ("aws_resources", "ws_package/aws_resources", "aws_resources"),
                        ]
                        
                        for resource_name, bundle_path, subdir in resources_to_check:
                            resource_path = resource_recovery.get_validated_resource(
                                resource_name, bundle_path, subdir
                            )
                            if not resource_path:
                                monitor_logger.warning("Resource %s missing, recovering...", resource_name)
                                recovered = resource_recovery.recover_resource(
                                    resource_name, bundle_path, subdir, optional=True
                                )
                                if recovered:
                                    monitor_logger.info("Resource %s recovered successfully", resource_name)
                                else:
                                    monitor_logger.debug("Resource %s recovery skipped (optional)", resource_name)
                    
                    # Wait for next check
                    resource_monitor_stop.wait(check_interval)
                except Exception as e:
                    monitor_logger.error("Resource monitor error: %s", e)
                    resource_monitor_stop.wait(60)  # Retry in 1 minute on error
        
        resource_monitor_thread = Thread(
            target=monitor_loop,
            name="BundleResourceRecovery",
            daemon=True
        )
        resource_monitor_thread.start()
        
        thread_manager.thread_controls["resource_monitor"] = {
            "thread": resource_monitor_thread,
            "stop_event": resource_monitor_stop,
            "restart_count": 0,
            "last_restart": time.time()
        }
        
        monitor_logger.info("Resource monitor service started")
    
    start_resource_monitor_service()

    # Start system wake detection
    from ws_package.utils.system_wake_detector import start_system_wake_monitoring
    wake_detector = start_system_wake_monitoring()

    # Register wake callback to restart threads and check status
    def on_system_wake():
        # After user stops monitoring, stop_monitoring_event stays set — do not spawn orphan
        # device/screenshot threads or "restart" websocket/db_writer (no start_*_activity methods).
        if stop_monitoring_event.is_set():
            monitor_logger.debug("System wake: monitoring session not active, skip thread_monitor wake handler")
            return
        monitor_logger.info("System wake detected - checking thread health and user status")
        # Force immediate thread health check
        check_thread_health()
        
        # Check user status on wake
        try:
            from ws_package.controllers.status_controller import StatusController
            from ws_package.controllers.user_controller_factory import get_user_for_wake
            from ws_package.utils.tracker_status import get_tracker_status
            
            # Run status check in async context
            async def check_status_on_wake():
                try:
                    user_entity = await get_user_for_wake()
                    if user_entity:
                        status_controller = StatusController()
                        await status_controller.set_cred(user_entity)
                        status_result = await status_controller.user_status()
                        monitor_logger.info("User status check on wake: %s", status_result)
                        
                        # Update activity controller flags based on tracker status
                        tracker_status = get_tracker_status()
                        if tracker_status == 'stop':
                            activity.force_stop = True
                            activity.status = False
                            monitor_logger.info("[SystemWake] Tracker status is 'stop' — tracking disabled")
                        else:
                            activity.force_stop = False
                            activity.status = True
                            monitor_logger.info("[SystemWake] Tracker status is 'start' — tracking enabled")
                except Exception as e:
                    monitor_logger.error("Error checking status on wake: %s", e)
            
            # Run async function without calling asyncio.run() inside an active loop.
            try:
                loop = asyncio.get_running_loop()
                if loop.is_running():
                    loop.create_task(check_status_on_wake())
                else:
                    from threading import Thread
                    Thread(
                        target=lambda: asyncio.run(check_status_on_wake()),
                        daemon=True,
                        name="WakeStatusCheckThread",
                    ).start()
            except RuntimeError:
                from threading import Thread
                Thread(
                    target=lambda: asyncio.run(check_status_on_wake()),
                    daemon=True,
                    name="WakeStatusCheckThread",
                ).start()
        except Exception as e:
            monitor_logger.error("Error in wake callback status check: %s", e)
    
    wake_detector.register_wake_callback("thread_monitor", on_system_wake)

    # Only these threads are started via ActivityController.start_<name>_activity.
    # websocket / db_writer / resource_monitor are managed elsewhere — never "restart" via health check.
    _ACTIVITY_MANAGED_THREAD_NAMES = frozenset({"device", "screenshot", "push", "status"})

    def check_thread_health():
        """Check and restart dead threads"""
        # Create a snapshot to avoid RuntimeError if dictionary is modified during iteration
        thread_items = list(thread_manager.thread_controls.items())
        for name, info in thread_items:
            if name not in _ACTIVITY_MANAGED_THREAD_NAMES:
                continue
            if not info["thread"].is_alive():
                # Check restart frequency to prevent infinite restart loops
                restart_count = info.get("restart_count", 0)
                last_restart = info.get("last_restart", 0)
                time_since_restart = time.time() - last_restart
                
                if restart_count > 5 and time_since_restart < 300:  # 5 restarts in 5 minutes
                    monitor_logger.error(
                        "Thread '%s' failed too frequently. Skipping restart (restart_count=%s, time_since_restart=%.1f).",
                        name, restart_count, time_since_restart,
                    )
                    continue

                monitor_logger.warning(
                    "Thread '%s' died. Restarting... (restart_count=%s)",
                    name, restart_count,
                )
                start_named_thread(name, restart=True)

    # Monitor threads in a loop with better error handling and more frequent checks
    try:
        while not stop_monitoring_event.is_set():
            try:
                stop_monitoring_event.wait(60)  # Check every 1 minute instead of 5 minutes
                if stop_monitoring_event.is_set():
                    break

                monitor_logger.info("Checking thread status")

                # Check thread health
                check_thread_health()

            except Exception as e:
                monitor_logger.error("Error in thread monitoring: %s", e)
                logger.error("Error in thread monitoring: %s", e, exc_info=True)
                time.sleep(30)  # Wait before retrying
    finally:
        # Always drop wake handler (crash or normal exit) so wake cannot touch a dead session.
        try:
            wake_detector.unregister_wake_callback("thread_monitor")
        except Exception as _unreg_err:
            monitor_logger.debug("Could not unregister thread_monitor wake callback: %s", _unreg_err)

    logger.info("Monitor threads received stop signal. Stopping activity monitoring threads.")
    
    # Stop only activity-managed threads; db_writer must keep running to persist the queue.
    thread_items = list(thread_manager.thread_controls.items())
    for name, info in thread_items:
        if name not in ACTIVITY_MANAGED_THREAD_NAMES:
            continue
        info["stop_event"].set()
        info["thread"].join(timeout=5)
        if info["thread"].is_alive():
            logger.warning("Thread '%s' did not terminate gracefully.", name)

# ----------------- FALLBACK MODE -----------------
def fallback_user(activity, stop_monitoring_event: Event):
    user_entity = asyncio.run(get_last_user())
    user_org = asyncio.run(get_last_org())

    if user_org:
        activity.org_id = user_org.org_id
        activity.screenshot_interval = user_org.ssfrequencystatus
        activity.interval = change(user_org.timeInterval)
        activity.appandurlstatus = user_org.appandurlstatus
        activity.blur_intensity = user_org.screenshotblurstatus
        activity.notification_status = user_org.notification_status if user_org.notification_status is not None else activity.notification_status

    monitor_threads(activity, user_entity, stop_monitoring_event)

def _run_background_setup():
    """
    Deferred resource recovery and AWS/SSL health check.

    IMPORTANT: This must be started AFTER the main thread's STEP-2 heavy imports
    complete (see call site in main()).  Starting it earlier caused a 65-second
    first-launch regression on Windows because:

      1. s3.py imports models.base, api_request_service, and resource_recovery at
         module level.  If these imports run in a background thread while the main
         thread is also importing the same modules (STEP 2), Python's per-module
         import locks stall the main thread until the background imports finish.

      2. On first launch, the background thread copies 500+ DLLs through
         Windows Defender.  This competes for disk I/O with the main
         thread's module loading from the PyInstaller bundle, slowing
         everything dramatically.

    By starting this thread only after STEP 2 imports are done, both of the above
    races are eliminated.  The S3 work still runs in parallel with user
    authentication and DB init, which is plenty early enough.
    """
    if not getattr(sys, 'frozen', False):
        return
    try:
        from ws_package.utils.s3 import check_aws_health, recover_aws_services, setup_ssl_for_boto3
        from ws_package.utils.resource_recovery import resource_recovery

        logger.info("[SETUP] Pre-copying critical resources to AppData...")
        pre_copy_results = resource_recovery.pre_copy_critical_resources()
        for resource, success in pre_copy_results.items():
            if success:
                logger.info("[SETUP] %s pre-copied successfully", resource)
            else:
                logger.debug("[SETUP] %s pre-copy skipped (optional)", resource)

        logger.info("[SETUP] Checking for missing bundled resources...")
        recovery_results = resource_recovery.check_and_recover_all()
        for resource, success in recovery_results.items():
            if not success:
                logger.warning("[SETUP] Resource recovery failed: %s", resource)

        logger.info("[SETUP] Checking AWS services health...")
        if not check_aws_health():
            logger.warning("[SETUP] AWS unhealthy — attempting recovery...")
            if recover_aws_services():
                logger.info("[SETUP] AWS services recovered")
            else:
                logger.error("[SETUP] AWS recovery failed — uploads may not work")

        logger.info("[SETUP] Configuring SSL for AWS services...")
        if not setup_ssl_for_boto3():
            logger.warning("[SETUP] SSL setup failed — SSL issues may occur")

    except Exception as e:
        logger.error("[SETUP] Background setup error (non-fatal): %s", e, exc_info=True)
def get_windows_timezone():
    if platform.system() == "Linux":
        output = subprocess.check_output(
            ["timedatectl", "show", "-p", "Timezone"]
        ).decode().strip()
        return output.split("=")[1]
    elif platform.system() == "Windows":
        # Import inside function to avoid blocking startup and scope issues
        import winreg
        try:
            registry_path = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path) as key:
                tz_name, _ = winreg.QueryValueEx(key, "TimeZoneKeyName")
                return tz_name
        except (FileNotFoundError, PermissionError, OSError):
            return None
    else:
        LOCALTIME_PATH = "/etc/localtime"
        tz_path = os.path.realpath(LOCALTIME_PATH)
        # Extract zone name (e.g. "America/New_York")
        if "zoneinfo" in tz_path:
            zone_name = tz_path.split("zoneinfo/")[-1]
            return zoneinfo.ZoneInfo(zone_name)
        else:
            # fallback if unexpected
            return datetime.timezone.utc

def system_gmt():
    local_time = datetime.datetime.now().astimezone()
    offset = local_time.utcoffset()
    hours = int(offset.total_seconds() // 3600)
    minutes = int((offset.total_seconds() % 3600) // 60)
    gmt_offset_str = f"GMT{hours:+03d}:{abs(minutes):02d}"
    return gmt_offset_str

def normalize_timezone(tz_name):
    if tz_name in ("Asia/Calcutta", "IST", "India Standard Time"):
        return "Asia/Kolkata"
    return tz_name

def detect_environment():
    info = {}

    # Platform information
    info['platform'] = platform.system()  # e.g. 'Windows', 'Linux', 'Darwin'
    info['platform_release'] = platform.release()
    info['architecture'] = platform.machine()

    # Check if running in PyInstaller bundle
    info['frozen'] = getattr(sys, 'frozen', False)
    if info['frozen']:
        info['type'] = 'PyInstaller Executable'
        info['executable_path'] = sys.executable
    else:
        info['type'] = 'Standard Python Script'
        info['script_path'] = os.path.abspath(__file__)
    # Check if running inside virtual environment
    info['virtual_env'] = (
        hasattr(sys, 'real_prefix') or
        (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or
        'VIRTUAL_ENV' in os.environ
    )

    # Python version
    info['python_version'] = platform.python_version()

    return info

# ----------------- MAIN FUNCTION -----------------

def timing_log(stage: str) -> None:
    """Log a timestamped stage marker for startup profiling (log file only, never stdout)."""
    try:
        logger.debug("[Timing] %s", stage)
    except Exception:
        pass


def _notify_tracking_error(electron_comm_ref, code, message: str, recoverable: bool = True,
                            source: str = "engine") -> None:
    """
    Send a tracking_error message to Electron so the user can see the failure.

    Safe to call at any time — silently no-ops if IPC is not yet connected.
    Always logs the error regardless of IPC state.

    Args:
        electron_comm_ref: ElectronComm instance or None.
        code: ErrorCode enum value (e.g. ErrorCode.SS_CAPTURE_FAILED).
        message: Human-readable explanation with remediation hint.
        recoverable: True if the app will retry / self-heal; False means user action needed.
        source: Subsystem label ("screenshot", "activity", "network", "ipc", …).
    """
    try:
        if electron_comm_ref and electron_comm_ref.is_connected():
            from ws_package.utils.user_error_categories import enrich_electron_payload
            payload = enrich_electron_payload(str(code), {
                "code": str(code),
                "message": message,
                "recoverable": recoverable,
                "source": source,
            })
            electron_comm_ref.send_to_electron("tracking_error", payload)
    except Exception as _err:
        logger.debug("[ENGINE] Could not send tracking_error to Electron: %s", _err)


def _activity_should_skip_counter_bump_dedup(activity, within_seconds: int = 12) -> bool:
    """Dedup counter bump on repeated start IPC; works with plain .py and older .pyd builds."""
    fn = getattr(activity, "should_skip_counter_bump_dedup", None)
    if callable(fn):
        return fn(within_seconds=within_seconds)
    if getattr(activity, "_bump_counter_on_next_monitoring_start", False):
        return False
    last_bump = getattr(activity, "_last_counter_bump_at", None)
    if last_bump is None:
        return False
    return (datetime.datetime.now() - last_bump).total_seconds() < within_seconds


def _activity_increment_session_counter(activity, reason: str = "new_session") -> int:
    """Bump session counter; delegates to ActivityController when present (native .pyd)."""
    fn = getattr(activity, "increment_session_counter", None)
    if callable(fn):
        return fn(reason=reason)
    old_counter = activity.last_count
    activity.last_count = old_counter + 1
    activity._last_counter_bump_at = datetime.datetime.now()
    activity._bump_counter_on_next_monitoring_start = False
    activity.start_date = datetime.datetime.now().replace(microsecond=0)
    activity.current_session_start_time = None
    activity._activity_log_payload_start = None
    screenshot_ctrl = getattr(activity, "screenshot_controller", None)
    if screenshot_ctrl is not None:
        screenshot_ctrl.last_count = activity.last_count
    logger.info(
        "=== SESSION COUNTER === Incremented %s -> %s (%s); intervals use counter=%s",
        old_counter,
        activity.last_count,
        reason,
        activity.last_count + 1,
    )
    return activity.last_count


def main():
    global logger 

    # Register atexit handler to ensure HardwareEventValidator cleanup on any exit
    def cleanup_on_exit():
        try:
            from ws_package.utils.hardware_event_validator import cleanup_hardware_validator
            cleanup_hardware_validator()
            if platform.system() == "Darwin":
                time.sleep(0.2)  # Brief wait for WindowServer to flush
        except Exception:
            pass  # Fail silently on exit
        
        # Stop health monitoring
        try:
            from ws_package.utils.health_monitor import stop_health_monitoring
            stop_health_monitoring()
        except Exception:
            pass  # Fail silently on exit

        # Stop disk/memory resource monitoring
        try:
            from ws_package.utils.resource_monitor import stop_resource_monitoring
            stop_resource_monitoring()
        except Exception:
            pass  # Fail silently on exit
    
    atexit.register(cleanup_on_exit)
    
    # Initialize thread-based logging system first
    initialize_thread_logging()

    # Set SSL cert path early so every HTTPS call (requests, boto3, urllib3) uses the
    # bundled cacert.pem.  This must happen before any network activity, including the
    # auth API call.  On onedir builds _MEIPASS == _internal; certifi.where() resolves
    # there automatically, but setting the env vars makes it explicit and robust.
    if getattr(sys, 'frozen', False):
        try:
            from ws_package.utils.ssl_setup import setup_ssl_for_boto3
            setup_ssl_for_boto3()
        except Exception as _ssl_err:
            logger.warning("Early SSL setup failed (non-fatal): %s", _ssl_err)

    # Detect third-party AV products that require manual exclusion (Windows only).
    # Windows Defender is excluded automatically via the installer; Bitdefender has no
    # CLI API so we can only inform the admin/user to add the exclusion manually.
    if platform.system() == "Windows" and getattr(sys, 'frozen', False):
        try:
            import winreg
            _bd_key_paths = [
                r"SOFTWARE\Bitdefender",
                r"SOFTWARE\WOW6432Node\Bitdefender",
            ]
            _bd_detected = False
            for _kp in _bd_key_paths:
                try:
                    winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, _kp)
                    _bd_detected = True
                    break
                except OSError:
                    pass
            if _bd_detected:
                logger.warning(
                    "Bitdefender detected. For best performance add the Workstatus install "
                    "directory to Bitdefender's exclusion list: "
                    "Protection > Antivirus > Settings > Exceptions > Add folder."
                )
        except Exception:
            pass

    # CRITICAL: Ensure _internal folder is available for PyInstaller DLL loading (Windows)
    # This must happen before any other operations that might trigger DLL loading.
    #
    # ONEDIR NOTE: in onedir mode, _internal/ lives permanently next to the EXE
    # (it is what _MEIPASS points at) and is bundled by the installer.  If it is
    # missing from the install dir, the install itself is broken and the user
    # must reinstall — falling back to copying from AppData here would resurrect
    # the stale-AppData class of bugs we deliberately quarantine.  So in onedir
    # we never read from AppData here.  Onefile builds still get the recovery path.
    if platform.system() == "Windows" and getattr(sys, 'frozen', False):
        # Onedir builds: _internal/ is permanently next to the EXE.
        # No AppData copy or recovery needed — check_and_run_setup() already
        # enforces this and exits cleanly if _internal/ is absent.
        if logger:
            logger.debug("Windows frozen: onedir build, _internal/ managed by installer.")
    
    # Try to ensure Application Support directory exists early on macOS
    # This is a best-effort attempt - if it fails, the setup process will handle it
    if platform.system() == "Darwin":
        try:
            app_support_dir = get_app_support_dir()
            if logger:
                logger.info("[INFO] Application Support directory verified: %s", app_support_dir)
        except (PermissionError, OSError) as e:
            # Don't exit - allow setup process to handle directory creation
            # During installation, the directory might not exist yet, which is normal
            warning_msg = f"Could not create Application Support directory yet: {e}"
            print(f"[WARNING] {warning_msg}", file=sys.stderr)
            print("[INFO] Setup process will attempt to create it...", file=sys.stderr)
            if logger:
                logger.warning("[WARN] %s", warning_msg)
                logger.info("[INFO] Setup process will attempt to create the directory during installation.")
        
    env_info = detect_environment()
    for key, value in env_info.items():
        logger.info("%s: %s", key, value)

    setup = check_and_run_setup()
    logger.info("Setup status: %s", setup)

    if not setup and getattr(sys, "frozen", False) and sys.platform.startswith("linux"):
        logger.info(
            "[INFO] Linux setup incomplete — exiting without starting IPC/monitoring "
            "(run ~/Workstatus/setup.sh if this is first install)."
        )
        return

    # macOS: single instance + permissions only after setup handoff to Application Support.
    if platform.system() == "Darwin" and setup:
        timing_log("Ensuring single instance (before permissions)...")
        ensure_single_instance()
        timing_log("Single instance check completed")
        try:
            _mac.sync_bundle_identity_and_maybe_invalidate(logger)
        except Exception as e:
            logger.warning("[PERMISSIONS] Bundle identity sync failed: %s", e)
        try:
            verify_and_fix_launchagent()
        except Exception as e:
            logger.warning("Could not verify LaunchAgent for auto-start: %s", e)
        try:
            request_macos_permissions()
        except Exception as e:
            logger.error("[PERMISSIONS] Error requesting permissions: %s", e, exc_info=True)
        try:
            start_permission_monitor()
        except Exception as e:
            logger.error("[PERMISSIONS] Error starting permission monitor: %s", e, exc_info=True)
    
    # DB table creation is deferred to the background heavy_init() thread inside
    # the `if setup:` block below.  heavy_init_complete.wait() guards all DB
    # access so nothing touches the DB before tables exist.  Keeping this
    # blocking asyncio.run() here would delay bootstrap_tcp() by 2-5 seconds,
    # which is what caused the slow Electron→Python connection on startup.

    if setup:
        try:
            ready_msg = json.dumps({"type": "ready", "data": {"status": "starting", "message": "Python main() entered"}})
            print(ready_msg, flush=True)
            sys.stdout.flush()
        except (OSError, IOError, ValueError, AttributeError):
            # Silently ignore stdout errors (stdout might be closed or invalid on Windows)
            pass

        # pick version dynamically from activity controller
        logger.info("App Version 7.0")

        if platform.system() != "Darwin":
            timing_log("Ensuring single instance...")
            ensure_single_instance()
            timing_log("Single instance check completed")

        logger = LogWriter.file(__name__)  # Initialize logger now
        timing_log("main() entered - START")

        from ws_package.utils.build_mode import (
            is_silent_build,
            SILENT_DEFAULT_PROJECT_ID,
            SILENT_DEFAULT_TASK_ID,
        )
        silent_build = is_silent_build()
        if silent_build:
            logger.info(
                "[Silent] build=silent — stealth_login auth, auto-start, no Electron DB"
            )

        # ----------------------------------------------------------------
        # STEP 1 — IPC (visible/Electron only). Silent MDM skips TCP/pipe server.
        # ----------------------------------------------------------------
        from ws_package.utils.electron_comm import (
            start_electron_communication,
            get_electron_comm,
            stop_electron_communication,
            install_silent_ipc_stub,
        )

        if silent_build:
            electron_comm = install_silent_ipc_stub()
            timing_log("[Silent] IPC skipped — null electron communicator")
        else:
            from ws_package.utils.ipc_session import init_ipc_session
            init_ipc_session()
            bootstrap_tcp()
            start_electron_communication()
            electron_comm = get_electron_comm()
            timing_log("IPC bootstrap + electron_comm started")

        if not silent_build:
            # Send ready signal via stdout as early fallback (before TCP connects)
            try:
                ready_msg = json.dumps({"type": "ready", "data": {
                    "status": "initializing",
                    "message": "WS_Engine starting",
                    "version": PYTHON_APP_VERSION
                }})
                print(ready_msg, flush=True)
                sys.stdout.flush()
            except (OSError, IOError, ValueError, AttributeError):
                pass

        # ── PRE-READY INIT ───────────────────────────────────────────────────────
        # Initialise minimum state, start background threads, and send the "ready"
        # signal BEFORE STEP-2 module imports.  STEP-2 imports take 15-20 s on
        # Windows cold start; previously the ready signal was sent after them,
        # making Electron wait 25 s.  Now Electron connects in ~2-3 s and STEP-2
        # runs in parallel with Electron's own startup work.
        # ────────────────────────────────────────────────────────────────────────
        stop_monitoring_event = Event()
        stop_monitoring_event.set()
        auth_complete = Event()
        user_entity_global = [None]
        auth_error = [None]
        auth_retry_used = [False]
        deferred_queue = queue.Queue()
        monitoring_started = [False]
        break_stop_thread_ref = [None]
        monitor_thread_ref = [None]

        def handle_early_messages():
            """Handle messages received before authentication is complete"""
            logger.info("handle_early_messages thread started loop")
            try:
                while not auth_complete.is_set():
                    try:
                        message = electron_comm.get_message(timeout=0.1)
                        if message:
                            msg_type = message.get('type')
                            if msg_type == 'stop':
                                logger.info("[Early] Received stop command from Electron")
                                stop_monitoring_event.set()
                            logger.info("[Early] Deferring message until auth complete: %s", msg_type)
                            deferred_queue.put(message)
                    except Exception as e:
                        logger.error("Error in handle_early_messages loop: %s", e)
                        time.sleep(1)
            except Exception as e:
                logger.error("Fatal error in handle_early_messages: %s", e)
            finally:
                logger.info("handle_early_messages thread exiting")

        early_msg_thread = None
        if not silent_build:
            early_msg_thread = Thread(target=handle_early_messages, daemon=True)
            early_msg_thread.start()
            logger.info("Early message handler started")
            timing_log("Early message handler started (pre-ready)")
        else:
            logger.info("[Silent] Skipping early Electron message handler")

        def detect_env_background():
            timing_log("Detecting environment (background)...")
            env_info = detect_environment()
            for key, value in env_info.items():
                logger.info("%s: %s", key, value)
            timing_log("Environment detection completed")

        Thread(target=detect_env_background, daemon=True, name="EnvDetectThread").start()

        heavy_init_complete = Event()
        heavy_init_error = [None]

        async def heavy_init():
            """Background DB initialization.

            IMPORTANT: This function MUST NOT import controllers or any module
            that STEP-2 (below) also imports. Doing so causes Python's per-module
            import locks to serialize the two import sets — destroying the
            parallelism we were trying to gain and adding lock-contention overhead
            on top. Controllers are imported lazily where they are actually used.

            Scope here: open the DB and create tables. That's it.
            """
            try:
                timing_log("Heavy init: Starting database table creation...")
                from ws_package.models.base import create_all_tables
                await create_all_tables()
                logger.info("Database tables initialized successfully")
                timing_log("Heavy init: Database tables initialized")
                heavy_init_complete.set()
                timing_log("Heavy init: Completed successfully")
            except Exception as e:
                logger.error("Failed in heavy initialization: %s", e, exc_info=True)
                heavy_init_error[0] = str(e)
                heavy_init_complete.set()
                raise

        timing_log("Starting heavy initialization in background thread (non-blocking)...")
        heavy_init_thread = Thread(target=_run_coroutine_in_new_loop(heavy_init()), daemon=True, name="HeavyInitThread")
        heavy_init_thread.start()
        timing_log("Heavy initialization thread started - continuing without waiting")

        # ► READY SIGNAL — sent BEFORE STEP-2 imports (visible / Electron only)
        if not silent_build:
            try:
                electron_comm.send_to_electron("ready", {
                    "status": "ready",
                    "message": "WS_Engine ready (initialization continuing in background)",
                    "capabilities": ["set_project_task", "stop"],
                    "version": PYTHON_APP_VERSION
                })
                logger.info("✓ Ready signal sent to Electron via TCP - monitoring can start immediately")
                timing_log("Ready signal sent to Electron via TCP (BEFORE STEP-2 imports)")
            except Exception as e:
                logger.warning("Failed to send ready signal to Electron via TCP: %s", e)

            try:
                ready_msg = json.dumps({"type": "ready", "data": {
                    "status": "ready",
                    "message": "WS_Engine ready (monitoring can start, initialization in background)",
                    "version": PYTHON_APP_VERSION
                }})
                print(ready_msg, flush=True)
                sys.stdout.flush()
                timing_log("Ready signal sent to stdout (fallback)")
            except (OSError, IOError, ValueError, AttributeError):
                pass
        else:
            logger.info("[Silent] Skipping Electron ready signals")

        # ----------------------------------------------------------------
        # STEP 2 — Now pull in heavy modules (boto3, SQLAlchemy).
        # IPC is already listening, so Electron can connect during this phase.
        # ----------------------------------------------------------------
        timing_log("Importing heavy modules...")

        from ws_package.entities.user_entity import UserEntity
        from ws_package.utils.system_wake_detector import get_system_wake_detector, start_system_wake_monitoring
        from ws_package.models.base import ActivityInterval, async_session, UserDetail, OrgConfigurations
        from sqlalchemy import select, desc
        from ws_package.configs import config
        from ws_package.services.api_request_service import APIRequest
        from ws_package.utils.resource_recovery import resource_recovery
        from ws_package.controllers.user_controller_factory import (
            authenticate_user,
            fetch_and_apply_org_settings,
        )
        from ws_package.utils.tracker_status import get_tracker_status

        # silent_build set at main() entry (re-read after config may have loaded)
        silent_build = is_silent_build()
        if silent_build:
            logger.info("[Silent] build=silent — Python-only MDM mode (no Electron dependency)")

        timing_log("Heavy modules imported")

        # Start the deferred background setup NOW — after STEP-2 imports are
        # complete.  Starting it earlier (in check_and_run_setup) caused a
        # 65-second first-launch regression: s3.py's module-level imports of
        # models.base and resource_recovery raced the main thread's STEP-2
        # imports via Python's import locks, stalling the main thread.
        Thread(target=_run_background_setup, name="BackgroundSetup", daemon=True).start()
        timing_log("BackgroundSetup thread started (deferred, post-imports)")

        # IPC server should be ready by now; fail fast if not (visible mode only).
        from ws_package.utils.ipc_server import is_ipc_ready
        if not is_ipc_ready():
            if silent_build:
                logger.warning(
                    "[Silent] IPC server not ready — continuing without Electron (expected for MDM)"
                )
            else:
                logger.error("IPC server failed to start — exiting")
                logger.critical("%s — Electron communication is unavailable. "
                                "Possible causes: port 54321 blocked by firewall/antivirus [E106], "
                                "Named Pipe denied by security policy [E110], "
                                "or another Workstatus instance is running [E001].",
                                ErrorCode.IPC_SERVER_FAILED)
                _notify_tracking_error(electron_comm, ErrorCode.IPC_SERVER_FAILED,
                    "Python could not open the IPC channel to Electron. "
                    "If antivirus is blocking port 54321, add Workstatus to the exclusion list "
                    "and restart. If another instance is running, close it first.",
                    recoverable=False, source="ipc")
                sys.exit(ExitCode.STARTUP_FAILURE)

        # Also attempt an early TCP ready signal now that the server is confirmed.
        if not silent_build:
            try:
                electron_comm.send_to_electron("ready", {
                    "status": "initializing",
                    "message": "WS_Engine starting (initialization in progress)",
                    "capabilities": ["set_project_task", "stop"],
                    "version": PYTHON_APP_VERSION
                })
                timing_log("✓ Early TCP ready signal sent to Electron")
            except Exception:
                pass

        # Start database writer thread (runs for entire Python process lifetime)
        start_db_writer_thread()

        # Disk + memory monitoring for the full process lifetime (all platforms).
        try:
            from ws_package.utils.resource_monitor import start_resource_monitoring
            start_resource_monitoring()
        except Exception as res_mon_err:
            logger.warning("Could not start disk/memory monitoring (non-fatal): %s", res_mon_err)

        # NOW do heavy initialization with timing logs (in background where possible)
        timing_log("Initializing thread-based logging system...")
        initialize_thread_logging()
        timing_log("Thread logging initialized")


        # Wait for heavy_init to complete before accessing database
        # NOTE: This wait happens AFTER sending ready signal, so Electron doesn't block
        timing_log("Waiting for database to be ready...")
        if not heavy_init_complete.wait(timeout=30):  # 30 second timeout
            logger.warning("Heavy initialization timeout - database may not be ready")
        elif heavy_init_error[0]:
            logger.error("Heavy initialization failed: %s", heavy_init_error[0])
            logger.error("Application cannot continue without database tables. Exiting.")
            return

        timing_log("Database is ready, getting last counter...")
        last_count = asyncio.run(get_last_counter())
        timing_log("Last counter retrieved")

        # Import ActivityController now (after heavy_init has imported it)
        from ws_package.controllers.activity_controller import ActivityController

        timing_log("ActivityController initialization started")
        activity = ActivityController(last_count)
        timing_log("ActivityController initialized")
        # Do NOT reassign stop_monitoring_event here — a second Event() breaks handle_early_messages
        # vs process_message/monitor_threads (orphan event) and causes post-sleep reconnect failures.
        try:
            windows_tz = get_windows_timezone()
            logger.info(windows_tz)
            activity.ws_timezone = windows_tz
        except Exception as e:
            logger.error("Failed to get System's Timezone: %s", e)
            activity.ws_timezone = str(get_localzone())
        try:
            if not silent_build:
                electron_comm.send_to_electron("ready", {
                    "status": "initialized",
                    "message": "WS_Engine fully initialized (authentication in progress)",
                    "capabilities": ["set_project_task", "stop"],
                    "version": PYTHON_APP_VERSION
                })
                logger.info("✓ Final ready signal sent to Electron - full initialization complete")
                timing_log("Final ready signal sent to Electron (after full initialization)")
            else:
                logger.info("[Silent] Skipping final Electron ready signal")
        except Exception as e:
            logger.warning("Failed to send final ready signal to Electron: %s", e)
         # Start WebSocket service for real-time commands

        def start_websocket_service(user_entity: UserEntity):
            """Start the WebSocket service thread"""
            try:
                logger.info("=" * 80)
                logger.info("[WebSocket] === INITIALIZING WEBSOCKET SERVICE ===")
                from ws_package.services.reverb_websocket_service import ReverbWebSocketService
                logger.info("[WebSocket] ✓ ReverbWebSocketService imported successfully")
                
                # Initialize WebSocket service
                logger.info("[WebSocket] User ID: %s, Org ID: %s", user_entity.user_id, user_entity.org_id)
                websocket_service = ReverbWebSocketService(
                    user_entity=user_entity,
                    activity_controller=activity
                )
                logger.info("[WebSocket] ✓ WebSocket service initialized successfully")
                
                # Create stop event for WebSocket service
                websocket_stop_event = Event()
                async_stop_event = asyncio.Event()
                
                # Run WebSocket service in a thread
                def run_websocket():
                    try:
                        # Create new event loop for this thread
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        loop.run_until_complete(websocket_service.start(websocket_stop_event))
                    except Exception as e:
                        logger.error("WebSocket service thread error: %s", e)
                        logger.error("[WebSocket] ✗ WebSocket service thread error: %s", e, exc_info=True)
                
                thread = Thread(target=run_websocket, name="websocket", daemon=True)
                thread.start()
                logger.info("=" * 80)
                
                thread_manager.thread_controls["websocket"] = {
                    "thread": thread,
                    "stop_event": websocket_stop_event,
                    "async_stop_event": async_stop_event,
                    "restart_count": 0,
                    "last_restart": time.time(),
                    "service": websocket_service
                }
                
                logger.info("WebSocket service thread started")
                logger.info("[WebSocket] ✓ WebSocket service registered with thread manager")
            except ImportError as e:
                error_msg = f"Failed to import WebSocket service: {e}. websocket-client may not be installed."
                logger.error(error_msg)
                logger.error("[WebSocket] ✗ %s", error_msg)
                logger.error("[WebSocket] Install with: pip install websocket-client")
            except Exception as e:
                logger.error("Failed to start WebSocket service thread: %s", e)
                logger.error("[WebSocket] ✗ Failed to start WebSocket service thread: %s", e, exc_info=True)

        def _handle_set_project_task_from_electron(project_id, task_id):
            """Apply project/task change; close/open activity/log when timer is running."""
            if silent_build:
                logger.debug("[Silent] Ignoring project/task change from IPC")
                return
            if not activity:
                logger.warning(
                    "Cannot set project/task - activity controller not initialized yet"
                )
                return

            project_id = str(project_id) if project_id is not None else None
            task_id = str(task_id) if task_id is not None else None

            monitoring_active = (
                not stop_monitoring_event.is_set()
                and monitoring_started[0]
                and getattr(activity, "status", False)
            )

            from ws_package.utils.project_task_utils import project_task_changed

            if monitoring_active and project_task_changed(activity, project_id, task_id):
                switch_fn = getattr(activity, "project_switch_event", None)
                if not callable(switch_fn):
                    logger.error(
                        "project_switch_event not available — updating project/task IDs only"
                    )
                    compat = getattr(activity, "_set_project_task_ids_compat", None)
                    if callable(compat):
                        compat(project_id, task_id)
                    else:
                        try:
                            activity.set_project_task_ids(project_id, task_id)
                        except TypeError:
                            activity.set_project_task_ids(project_id, task_id)
                    return

                def run_project_switch():
                    try:
                        user = user_entity_global[0]
                        if not user:
                            logger.warning(
                                "Project switch skipped - user not authenticated; updating IDs only"
                            )
                            activity.set_project_task_ids(project_id, task_id)
                            return
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        try:
                            loop.run_until_complete(
                                switch_fn(user, project_id, task_id)
                            )
                        finally:
                            loop.close()
                    except Exception as e:
                        logger.error(
                            "Project switch thread failed: %s", e, exc_info=True
                        )

                Thread(
                    target=run_project_switch,
                    daemon=False,
                    name="ProjectSwitchThread",
                ).start()
                logger.info(
                    "Started project switch thread (close old activity/log, open new)"
                )
            else:
                activity.set_project_task_ids(project_id, task_id)
                logger.info(
                    "Set project_id=%s, task_id=%s (monitoring inactive or unchanged)",
                    activity.current_project_id,
                    activity.current_task_id,
                )

        def process_message(message):
            """
            Dispatch a single decoded JSON message received from Electron.

            Called from two contexts:
              - The main message loop (post-auth, steady-state).
              - The deferred-queue drain (immediately after auth completes).

            NOT thread-safe — must only be called from one thread at a time.
            The deferred queue is drained exclusively on the main thread;
            the auth thread no longer drains it (see authenticate_and_setup).

            Unhandled message types are logged at WARNING level so they appear
            in support logs without crashing the loop.

            Args:
                message: Decoded dict from json.loads(); must have a "type" key.
            """
            if not message:
                logger.warning("process_message called with None or empty message")
                return

            msg_type = message.get('type')
            data = message.get('data')
            action = message.get('action')  # Support action field for persistent mode
            
            # CRITICAL FIX: Handle case where type field contains entire message as JSON string
            # This happens when message is double-encoded or malformed
            if isinstance(msg_type, str) and msg_type.startswith('{'):
                try:
                    parsed_message = json.loads(msg_type)
                    logger.warning("Message type field contains JSON string - parsing: %s", parsed_message)
                    # Extract actual type, action, and data from parsed message
                    msg_type = parsed_message.get('type', msg_type)
                    action = parsed_message.get('action', action)
                    data = parsed_message.get('data', data)
                    # CRITICAL FIX: Also update message dict with all fields from parsed_message
                    # This ensures idle_popup_response can access projectId, taskId, etc.
                    message.update(parsed_message)
                    logger.info("Extracted from JSON string: type=%s, action=%s, data=%s", msg_type, action, data)
                except (json.JSONDecodeError, TypeError, AttributeError) as e:
                    logger.warning("Could not parse type field as JSON: %s, using original type=%s", e, msg_type)
            
            logger.info("=== Processing message from Electron: type=%s, action=%s, data=%s ===", msg_type, action, data)

            if silent_build and (
                msg_type in ('break_start', 'break_stop')
                or action in ('start_break', 'stop_break')
            ):
                logger.info("[Silent] Ignoring break IPC message: type=%s action=%s", msg_type, action)
                return

            if msg_type == 'force_upgrade_update_app':
                try:
                    from ws_package.services.activity_service import ActivityService
                    from ws_package.utils.electron_comm import get_electron_comm

                    service = ActivityService()

                    if not user_entity_global or not user_entity_global[0]:
                        logger.warning("[ForceUpgrade] Cannot run check_version_update - user_entity_global is not set")
                        get_electron_comm().send_to_electron(
                            "version_check_result",
                            {
                                "force_upgrade": False,
                                "updated": False,
                                "source": "force_upgrade_update_app",
                                "error": "user_not_ready",
                            },
                        )
                        return

                    user_entity = user_entity_global[0]

                    def _run_update():
                        ec = get_electron_comm()
                        try:
                            loop = asyncio.new_event_loop()
                            asyncio.set_event_loop(loop)
                            logger.info("[ForceUpgrade] Starting explicit check_version_update from Electron")
                            result = loop.run_until_complete(
                                service.check_version_update(user_entity, perform_upgrade=True)
                            )
                            logger.info("[ForceUpgrade] check_version_update completed, result=%s", result)
                            # On success, swap_build() calls sys.exit — we never reach here.
                            payload = {
                                "force_upgrade": True,
                                "updated": bool(result),
                                "source": "force_upgrade_update_app",
                            }
                            if not result:
                                payload["error"] = "download_or_validation_failed"
                                if platform.system() == "Windows":
                                    log_hint = Path.home() / ".workstatus" / "logs" / "windows_force_upgrade.log"
                                elif platform.system() == "Darwin":
                                    log_hint = Path.home() / ".workstatus" / "logs" / "mac_force_upgrade.log"
                                else:
                                    log_hint = Path.home() / ".workstatus" / "logs" / "linux_force_upgrade.log"
                                payload["upgrade_log"] = str(log_hint)
                            ec.send_to_electron("version_check_result", payload)
                        except Exception as e:
                            logger.error(
                                "[ForceUpgrade] Error running check_version_update: %s",
                                e,
                                exc_info=True,
                            )
                            ec.send_to_electron(
                                "version_check_result",
                                {
                                    "force_upgrade": False,
                                    "updated": False,
                                    "source": "force_upgrade_update_app",
                                    "error": str(e),
                                },
                            )
                        finally:
                            try:
                                loop.close()
                            except Exception:
                                pass

                    # Non-daemon: download + handoff must finish even if main thread winds down.
                    t = threading.Thread(target=_run_update, daemon=False, name="ForceUpgradeUpdateApp")
                    t.start()
                except Exception as e:
                    logger.error("[ForceUpgrade] Failed to initialize check_version_update: %s", e, exc_info=True)
                return

            if msg_type == 'check_updates_only':
                try:
                    from ws_package.services.activity_service import ActivityService
                    service = ActivityService()

                    if not user_entity_global or not user_entity_global[0]:
                        logger.warning("[ForceUpgrade] Cannot run check_version_update (check-only) - user_entity_global is not set")
                        return

                    user_entity = user_entity_global[0]

                    def _run_check_only():
                        try:
                            loop = asyncio.new_event_loop()
                            asyncio.set_event_loop(loop)
                            logger.info("[ForceUpgrade] Starting check_version_update (perform_upgrade=False) from Electron")
                            result = loop.run_until_complete(service.check_version_update(user_entity, perform_upgrade=False))
                            logger.info("[ForceUpgrade] check_version_update (check-only) completed, result=%s", result)
                            try:
                                from ws_package.utils.electron_comm import get_electron_comm
                                ec = get_electron_comm()
                                if ec and ec.is_connected():
                                    if isinstance(result, dict):
                                        payload = dict(result)
                                    else:
                                        payload = {
                                            "force_upgrade": bool(result),
                                            "update_available": bool(result),
                                        }
                                    payload["source"] = "check_updates_only"
                                    ec.send_to_electron("version_check_result", payload)
                            except Exception as send_e:
                                logger.warning("[ForceUpgrade] Failed to send version_check_result (check-only) to Electron: %s", send_e)
                        except Exception as e:
                            logger.error("[ForceUpgrade] Error running check_version_update (check-only): %s", e, exc_info=True)
                        finally:
                            try:
                                loop.close()
                            except Exception:
                                pass

                    t = threading.Thread(target=_run_check_only, daemon=True, name="ForceUpgradeCheckOnly")
                    t.start()
                except Exception as e:
                    logger.error("[ForceUpgrade] Failed to initialize check_version_update (check-only): %s", e, exc_info=True)
                return
            
            # Handle idle popup shown confirmation (Electron confirms popup was displayed - start cooldown in Python)
            if msg_type == 'idle_popup_shown':
                if activity and hasattr(activity, 'handle_idle_popup_shown'):
                    activity.handle_idle_popup_shown()
                    logger.info("=== Idle popup shown confirmed - cooldown started ===")
                return
            
            # Handle idle popup response
            if msg_type == 'idle_popup_response':
                # CRITICAL FIX: After JSON string parsing, extract fields from parsed_message if available
                # The parsed_message contains the actual data (projectId, taskId, etc) but message dict wasn't updated
                keep_idle_time = message.get('keep_idle_time', False)
                popup_action = message.get('action', action if action else 'continue')  # Use extracted action or fallback
                project_id = message.get('projectId')  # For reassign action
                task_id = message.get('taskId')  # For reassign action (optional)
                note = message.get('note')  # Extract note from message
                file_path = message.get('file_path')  # Extract file path from message
                
                # If project_id/task_id are None, try to get them from the parsed data
                # This handles the case where message was double-encoded as JSON string
                if (project_id is None or task_id is None) and data:
                    if isinstance(data, dict):
                        project_id = data.get('projectId', project_id)
                        task_id = data.get('taskId', task_id)
                        file_path = data.get('file_path', file_path)
                
                logger.info("=== Received idle popup response: keep_idle_time=%s, action=%s, project_id=%s, task_id=%s, note=%s, file_path=%s ===", keep_idle_time, popup_action, project_id, task_id, note, file_path)
                
                # Handle the idle popup response in activity controller
                if activity:
                    def handle_idle_popup_response():
                        """Handle idle popup response in a separate thread with its own event loop"""
                        try:
                            loop = asyncio.new_event_loop()
                            asyncio.set_event_loop(loop)
                            try:
                                loop.run_until_complete(activity.handle_idle_popup_response(keep_idle_time, popup_action, project_id, task_id, note, file_path))
                            finally:
                                loop.close()
                        except Exception as e:
                            logger.error("Error handling idle popup response: %s", e, exc_info=True)
                    
                    # Execute in a separate thread to avoid blocking
                    response_thread = threading.Thread(target=handle_idle_popup_response, daemon=True, name="IdlePopupResponseHandler")
                    response_thread.start()
                
                # CRITICAL: If action is 'stop', trigger stop event after handling idle popup response
                if popup_action == 'stop':
                    logger.info("=== Idle popup action is 'stop' - triggering stop event ===")
                    # CRITICAL: Wait for IdlePopupResponseHandler to finish so is_uploaded=1 is committed
                    # before StopEventThread runs stop_event() and queries unsent intervals. Prevents
                    # idle intervals from being sent to activity/log when keep_idle_time=False.
                    if activity and response_thread.is_alive():
                        response_thread.join(timeout=30)
                        if response_thread.is_alive():
                            logger.warning("IdlePopupResponseHandler did not complete within 30s - proceeding with stop event")
                        else:
                            logger.info("IdlePopupResponseHandler completed - idle intervals marked before stop_event will run")
                    # CRITICAL: Preserve break_id when stop is called during a break
                    if activity:
                        current_break_id = getattr(activity, 'break_id', 0) or 0
                        if current_break_id != 0:
                            logger.info("Stop command received during break (break_id=%s) - preserving break_id for next start", current_break_id)
                    
                    logger.info("=== Received stop command from idle popup - stopping activity monitoring (Python process will stay alive) ===")
                    
                    # IMMEDIATELY stop all monitoring threads by setting their stop events
                    logger.info("Setting stop events for all monitoring threads...")
                    threads_stopped = []
                    # Create a snapshot to avoid RuntimeError if dictionary is modified during iteration
                    thread_items = list(thread_manager.thread_controls.items())
                    for name, info in thread_items:
                        if name not in ACTIVITY_MANAGED_THREAD_NAMES:
                            continue
                        if info.get("stop_event"):
                            info["stop_event"].set()
                            threads_stopped.append(name)
                            logger.info("Stop signal sent to thread: %s", name)
                        if info.get("async_stop_event"):
                            info["async_stop_event"].set()
                            logger.info("Async stop signal sent to thread: %s", name)
                    
                    logger.info("Stop signals sent to %s thread(s): %s", len(threads_stopped), threads_stopped)
                    
                    # Set the main monitoring event to stop the monitor_threads loop
                    stop_monitoring_event.set()
                    monitoring_started[0] = False  # Mark monitoring as stopped
                    
                    # Call activity.stop_event to trigger last_api_call (pass keep_idle_time so unkeep/auto-stop don't send last interval)
                    def call_stop_event():
                        """Call stop_event in a separate thread with its own event loop"""
                        try:
                            if user_entity_global[0] and activity:
                                logger.info("Calling activity.stop_event(keep_idle_time=%s) to trigger last_api_call...", keep_idle_time)
                                loop = asyncio.new_event_loop()
                                asyncio.set_event_loop(loop)
                                try:
                                    loop.run_until_complete(activity.stop_event(user_entity_global[0], keep_idle_time=keep_idle_time))
                                    logger.info("activity.stop_event() completed successfully - last_api_call executed")
                                    
                                    # Send stop_completed message to Electron
                                    try:
                                        electron_comm.send_to_electron("stop_completed", {
                                            "status": "success",
                                            "message": "Final data upload complete (idle stop)"
                                        })
                                        logger.info("✓ stop_completed signal sent to Electron via TCP")
                                    except Exception as e:
                                        logger.warning("Failed to send stop_completed signal to Electron: %s", e)
                                except Exception as e:
                                    logger.error("Error in activity.stop_event(): %s", e, exc_info=True)
                                finally:
                                    loop.close()
                        except Exception as e:
                            logger.error("Error in call_stop_event thread: %s", e, exc_info=True)
                    
                    stop_thread = threading.Thread(target=call_stop_event, daemon=True, name="StopEventThread")
                    stop_thread.start()
                    logger.info("Stop event thread started for idle popup stop action")
                
                return  # Don't process further
            
            # Handle add note for current running interval
            if msg_type == 'add_note' or (action and action == 'add_note'):
                logger.info("=== Received add_note command ===")
                logger.info("[AddNote] Full message: %s", message)
                logger.info("[AddNote] Message type: %s, action: %s, data: %s", msg_type, action, data)
                
                # Try multiple ways to extract note text
                note_text = message.get('note')
                if not note_text and isinstance(data, dict):
                    note_text = data.get('note')
                if not note_text:
                    note_text = message.get('data', {}).get('note') if isinstance(message.get('data'), dict) else None
                
                file_path = message.get('file_path')
                if not file_path and isinstance(data, dict):
                    file_path = data.get('file_path')
                if not file_path:
                    file_path = message.get('data', {}).get('file_path') if isinstance(message.get('data'), dict) else None
                
                logger.info("[AddNote] Extracted note_text: %s..., file_path: %s", note_text, file_path)
                
                if not note_text:
                    logger.error("add_note message received but no note text could be extracted from message")
                    logger.error("[AddNote] Available keys in message: %s", list(message.keys()))
                    return
                
                logger.info("=== Saving note to current interval: note_length=%s, has_file=%s ===", len(note_text), file_path is not None)
                
                if not activity:
                    logger.error("[AddNote] Activity controller not available - cannot save note")
                    return
                
                def save_note_to_current():
                    """Save note to current interval in a separate thread"""
                    try:
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        try:
                            logger.info("[AddNote] Starting async save_note_to_current_interval...")
                            loop.run_until_complete(activity._save_note_to_current_interval(note_text, file_path))
                            logger.info("✓ Note saved to current interval successfully")
                        except Exception as e:
                            logger.error("[AddNote] Error in save_note_to_current_interval: %s", e, exc_info=True)
                        finally:
                            loop.close()
                    except Exception as e:
                        logger.error("[AddNote] Error in save_note_to_current thread: %s", e, exc_info=True)
                
                note_thread = threading.Thread(target=save_note_to_current, daemon=True, name="AddNoteHandler")
                note_thread.start()
                logger.info("[AddNote] Add note thread started")
                
                return  # Don't process further
            
            # Handle mark idle intervals as uploaded (before auto-stop)
            if msg_type == 'mark_idle_intervals_uploaded':
                logger.info("=== Received mark_idle_intervals_uploaded command ===")
                
                def mark_idle_intervals():
                    """Mark all intervals during idle popup period as uploaded in a separate thread"""
                    try:
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        try:
                            async def update_idle_intervals():
                                from ws_package.models.base import ActivityInterval, async_session
                                from sqlalchemy import select, update, and_
                                
                                try:
                                    async with async_session() as session:
                                        # CRITICAL FIX: Mark ALL intervals created during idle popup period
                                        # (from _idle_popup_triggered_at to now) as is_uploaded=True
                                        # This includes both idle intervals AND intervals with activity
                                        # that occurred while popup was active but not handled
                                        
                                        # Get the idle popup trigger time from activity controller
                                        idle_popup_triggered_at = None
                                        if hasattr(activity, '_idle_popup_triggered_at') and activity._idle_popup_triggered_at:
                                            idle_popup_triggered_at = activity._idle_popup_triggered_at
                                            logger.info("[AutoStop] Idle popup was triggered at: %s", idle_popup_triggered_at)
                                        
                                        if idle_popup_triggered_at:
                                            # Mark ALL intervals with start_time >= idle_popup_triggered_at
                                            # This includes intervals with activity that occurred during popup period
                                            stmt = (
                                                update(ActivityInterval)
                                                .where(
                                                    and_(
                                                        ActivityInterval.counter == activity.last_count + 1,
                                                        ActivityInterval.start_time >= idle_popup_triggered_at,
                                                        ActivityInterval.is_uploaded == False
                                                    )
                                                )
                                                .values(is_uploaded=True)
                                            )
                                            logger.info("[AutoStop] Marking ALL intervals from %s onwards as is_uploaded=True (includes intervals with activity)", idle_popup_triggered_at)
                                        else:
                                            # Fallback: If no trigger time, mark only idle intervals (backward compatibility)
                                            logger.warning("[AutoStop] No idle popup trigger time found - falling back to marking only idle intervals")
                                            stmt = (
                                                update(ActivityInterval)
                                                .where(ActivityInterval.is_idle == True)
                                                .where(ActivityInterval.is_uploaded == False)
                                                .values(is_uploaded=True)
                                            )
                                        
                                        result = await session.execute(stmt)
                                        await session.commit()
                                        
                                        updated_count = result.rowcount
                                        if idle_popup_triggered_at:
                                            logger.info("✓ Marked %s intervals (idle + with activity) from popup period as uploaded (is_uploaded=True)", updated_count)
                                        else:
                                            logger.info("✓ Marked %s idle intervals as uploaded (is_uploaded=True)", updated_count)
                                        
                                        return updated_count
                                except Exception as e:
                                    logger.error("Error updating idle intervals: %s", e, exc_info=True)
                                    return 0
                            
                            updated_count = loop.run_until_complete(update_idle_intervals())
                            logger.info("✓ mark_idle_intervals_uploaded completed: %s intervals updated", updated_count)
                        finally:
                            loop.close()
                    except Exception as e:
                        logger.error("Error in mark_idle_intervals thread: %s", e, exc_info=True)
                
               # Execute in a separate thread
                mark_thread = threading.Thread(target=mark_idle_intervals, daemon=True, name="MarkIdleIntervalsThread")
                mark_thread.start()
                logger.info("mark_idle_intervals thread started")
                
                return  # Don't process further
            
            # Handle update user entity command
            if msg_type == 'update_user_entity':
                logger.info("=== Received update_user_entity command ===")
                
                def update_user_entity():
                    """Update user entity in a separate thread"""
                    try:
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        try:
                            async def process_user_update():
                                from ws_package.entities.user_entity import UserEntity
                                from ws_package.controllers.user_controller import UserController
                                from ws_package.controllers.server_controller import ServerController
                                
                                try:
                                    # Extract user data from message
                                    user_data = data if data else {}
                                    if not user_data:
                                        logger.error("[UpdateUserEntity] No user data provided")
                                        return
                                    
                                    # Log current user entity before update
                                    old_user_entity = user_entity_global[0]
                                    if old_user_entity:
                                        logger.info("[UpdateUserEntity] CURRENT user entity: user_id=%s, org_id=%s, token=%s..., sd_token=%s", old_user_entity.user_id, old_user_entity.org_id, old_user_entity.token, old_user_entity.sd_token)
                                    else:
                                        logger.info("[UpdateUserEntity] No current user entity found")
                                    
                                    # Create UserEntity from the data
                                    user_entity = UserEntity(
                                        username=user_data.get('username', ''),
                                        name=user_data.get('name', ''),
                                        timezone=user_data.get('timezone', ''),
                                        token=user_data.get('token', ''),
                                        org_id=user_data.get('org_id', 0),
                                        user_id=user_data.get('user_id', 0),
                                        sd_token=user_data.get('sd_token', ''),
                                        bucket=user_data.get('bucket', 'aws'),
                                        timezone_preference=(
                                            0 if user_data.get('timezone_preference') in (0, '0', False) else 1
                                        ),
                                    )
                                    
                                    logger.info("[UpdateUserEntity] NEW user entity: user_id=%s, org_id=%s, username=%s, name=%s, token=%s..., sd_token=%s, bucket=%s", user_entity.user_id, user_entity.org_id, user_entity.username, user_entity.name, user_entity.token, user_entity.sd_token, user_entity.bucket)

                                    # Save user entity to database
                                    logger.info("[UpdateUserEntity] Saving user entity to database...")
                                    user_controller = UserController()
                                    saved_entity = await user_controller.user_service.save_user(user_entity)
                                    
                                    if saved_entity:
                                        logger.info("[UpdateUserEntity] User entity saved to database successfully")
                                        
                                        # Force process the write queue immediately to ensure it's saved
                                        from ws_package.services.db_writer_service import db_writer_service
                                        await db_writer_service.process_immediately()
                                        logger.info("[UpdateUserEntity] Write queue processed")
                                        
                                        # Update global user entity and activity.user_entity
                                        user_entity_global[0] = saved_entity
                                        if activity:
                                            activity.user_entity = saved_entity
                                            if saved_entity.timezone_preference == 1:
                                                had_timezone_stop = (
                                                    activity._timezone_mismatch
                                                    or activity._time_error_type == "timezone_error"
                                                )
                                                activity._timezone_mismatch = False
                                                if activity._time_error_type == "timezone_error":
                                                    activity._time_error_type = None
                                                if had_timezone_stop:
                                                    activity.force_stop = False
                                                time_monitor = getattr(
                                                    getattr(activity, "activity_manager", None),
                                                    "time_monitor",
                                                    None,
                                                )
                                                if time_monitor:
                                                    time_monitor.stop_monitoring()
                                                logger.info(
                                                    "[UpdateUserEntity] timezone_preference=1 — "
                                                    "timezone/time enforcement disabled"
                                                )
                                            logger.info("[UpdateUserEntity] Updated activity.user_entity: org_id=%s, token=%s..., sd_token=%s", activity.user_entity.org_id, activity.user_entity.token, activity.user_entity.sd_token)
                                        
                                        # Verify the update
                                        logger.info("[UpdateUserEntity] VERIFIED user_entity_global[0]: user_id=%s, org_id=%s, token=%s..., sd_token=%s", user_entity_global[0].user_id, user_entity_global[0].org_id, user_entity_global[0].token, user_entity_global[0].sd_token)
                                        
                                        # Update S3 bucket details if needed
                                        try:
                                            logger.info("[UpdateUserEntity] Updating S3 bucket details for new organization...")
                                            server = ServerController()
                                            await server.get_s3_bucket_details(saved_entity)
                                            logger.info("[UpdateUserEntity] S3 bucket details updated for new organization")
                                        except Exception as s3_error:
                                            logger.warning("[UpdateUserEntity] Error updating S3 bucket details: %s", s3_error, exc_info=True)
                                        
                                        logger.info("[UpdateUserEntity] User entity updated successfully: %s (Org: %s)", saved_entity.name, saved_entity.org_id)
                                    else:
                                        logger.error("[UpdateUserEntity] Failed to save user entity to database")
                                        
                                except Exception as e:
                                    logger.error("[UpdateUserEntity] Error updating user entity: %s", e, exc_info=True)
                            
                            loop.run_until_complete(process_user_update())
                        finally:
                            loop.close()
                    except Exception as e:
                        logger.error("[UpdateUserEntity] Error in update_user_entity thread: %s", e, exc_info=True)

                # Execute in a separate thread
                update_thread = threading.Thread(target=update_user_entity, daemon=True, name="UpdateUserEntityThread")
                update_thread.start()
                logger.info("[UpdateUserEntity] update_user_entity thread started")
                
                return  # Don't process further
            
            # Handle stop command - SIMPLE: any message with type='stop' will stop monitoring
            if msg_type == 'stop':
                # CRITICAL: Preserve break_id when stop is called during a break
                # Only break_stop should clear break_id, not regular stop
                if activity:
                    current_break_id = getattr(activity, 'break_id', 0) or 0
                    if current_break_id != 0:
                        logger.info("Stop command received during break (break_id=%s) - preserving break_id for next start", current_break_id)
                
                logger.info("=== Received stop command - stopping activity monitoring (Python process will stay alive) ===")
                
                # IMMEDIATELY stop all monitoring threads by setting their stop events
                # This must happen before calling activity.stop_event() to ensure threads stop quickly
                logger.info("Setting stop events for all monitoring threads...")
                threads_stopped = []
                # Create a snapshot to avoid RuntimeError if dictionary is modified during iteration
                thread_items = list(thread_manager.thread_controls.items())
                for name, info in thread_items:
                    if name not in ACTIVITY_MANAGED_THREAD_NAMES:
                        continue
                    if info.get("stop_event"):
                        info["stop_event"].set()
                        threads_stopped.append(name)
                        logger.info("Stop signal sent to thread: %s", name)
                    if info.get("async_stop_event"):
                        info["async_stop_event"].set()
                        logger.info("Async stop signal sent to thread: %s", name)
                
                logger.info("Stop signals sent to %s thread(s): %s", len(threads_stopped), threads_stopped)
                
                # Set the main monitoring event to stop the monitor_threads loop
                stop_monitoring_event.set()
                monitoring_started[0] = False  # Mark monitoring as stopped
                
                # Call activity.stop_event to trigger last_api_call
                # This must be done in a separate thread to avoid blocking the message loop
                def call_stop_event():
                    """Call stop_event in a separate thread with its own event loop"""
                    try:
                        if user_entity_global[0] and activity:
                            logger.info("Calling activity.stop_event() to trigger last_api_call...")
                            # Create a new event loop for this thread
                            loop = asyncio.new_event_loop()
                            asyncio.set_event_loop(loop)
                            try:
                                # Run the async stop_event which calls last_api_call
                                keep_idle_time = True
                                if data and isinstance(data, dict):
                                    keep_idle_time = data.get('keep_idle_time', True)
                                    # Handle string 'false'/'true' just in case
                                    if isinstance(keep_idle_time, str):
                                        keep_idle_time = keep_idle_time.lower() == 'true'
                                
                                logger.info("Calling activity.stop_event(keep_idle_time=%s)", keep_idle_time)
                                loop.run_until_complete(activity.stop_event(user_entity_global[0], keep_idle_time=keep_idle_time))
                                logger.info("activity.stop_event() completed successfully - last_api_call executed")
                                
                                # Send stop_completed message to Electron
                                try:
                                    electron_comm.send_to_electron("stop_completed", {
                                        "status": "success",
                                        "message": "Final data upload complete"
                                    })
                                    logger.info("✓ stop_completed signal sent to Electron via TCP")
                                except Exception as e:
                                    logger.warning("Failed to send stop_completed signal to Electron: %s", e)
                            except Exception as e:
                                logger.error("Error in activity.stop_event(): %s", e, exc_info=True)
                            finally:
                                loop.close()
                        else:
                            if not user_entity_global[0]:
                                logger.warning("Cannot call stop_event - user_entity not available")
                            if not activity:
                                logger.warning("Cannot call stop_event - activity object not available")
                    except Exception as e:
                        logger.error("Error in call_stop_event thread: %s", e, exc_info=True)
                
                # Start stop_event in a separate thread so it doesn't block message processing
                # Use non-daemon thread to ensure last_api_call completes before process exits
                from threading import Thread
                stop_thread = Thread(target=call_stop_event, daemon=False, name="StopEventThread")
                stop_thread.start()
                logger.info("Stop event thread started - last_api_call will execute in background (non-daemon to ensure completion)")
                
                logger.info("=== Monitoring stopped - Python process remains alive and ready for next start command ===")
                return

            # Handle start command - SIMPLE: any message with type='start' will start monitoring
            if msg_type == 'start':
                logger.info("=== Received start command - starting activity monitoring ===")
                logger.debug("[Timing][Python] Received start command at %s", time.time())

                # WebSocket is started AFTER MonitorThread is spawned and any prior workers are
                # stopped/cleared (see below). Starting it here caused the restart path to signal
                # stop on the brand-new websocket thread, disconnect during subscribe, and leave
                # monitoring_active false — especially visible after sleep/resume + timer start.

                # CRITICAL FIX: Small delay to allow break_start message to be processed if it's queued
                # This ensures break_start sets is_break=1 before we check it
                time.sleep(0.15)  # 150ms delay to allow break_start to process if queued
                
                # CRITICAL: Preserve break_id and is_break if break was started before this start command
                # break_id is the source of truth - if it's set, we're on a break
                if activity:
                    current_break_id = getattr(activity, 'break_id', 0) or 0
                    if current_break_id != 0:
                        # Break is active - preserve break status
                        logger.info("Start command received but break_id=%s is set - preserving break status", current_break_id)
                        logger.info("✓ Preserving break_id=%s - monitoring will save intervals with is_break=%s", current_break_id, current_break_id)
                        # Ensure is_break is set to 1 if break_id is set
                        if activity.is_break != 1:
                            logger.warning("Start command: break_id=%s but is_break=%s - setting is_break=1", current_break_id, activity.is_break)
                            activity.set_is_break(1)
                    else:
                        # Normal start, not a break - ensure break status is cleared
                        if activity.is_break != 0:
                            logger.info("Start command: break_id=0 but is_break=%s - resetting is_break to 0 (normal start)", activity.is_break)
                            activity.set_is_break(0)
                        else:
                            logger.info("Start command received with is_break=0 (normal start, not a break)")
                
                # FIRST: Extract project_id and task_id from start message data (if present)
                # This must be done BEFORE any early returns to ensure project/task IDs are always set
                project_id = (data.get('projectId') if data else None) or message.get('projectId')
                task_id = (data.get('taskId') if data else None) or message.get('taskId')
                if isinstance(data, str):
                    try:
                        import json
                        parsed_data = json.loads(data)
                        if isinstance(parsed_data, dict):
                            project_id = project_id or parsed_data.get('projectId')
                            task_id = task_id or parsed_data.get('taskId')
                    except Exception:
                        pass

                # Coerce to strings for runtime type-checkers / validation
                # (prevents "expected str, got int" when Electron sends numbers)
                project_id = str(project_id) if project_id is not None else None
                task_id = str(task_id) if task_id is not None else None
                
                # Also check if action is set_project_task (for backward compatibility)
                if action == 'set_project_task' or project_id is not None or task_id is not None:
                    logger.info("Start message contains project/task IDs: project_id=%s, task_id=%s", project_id, task_id)
                    _handle_set_project_task_from_electron(project_id, task_id)
                
                try:
                    is_timer_session_start = action == 'start_activity_monitoring'
                    is_project_task_only = action == 'set_project_task'

                    # set_project_task normally only updates IDs; Electron should send
                    # start_activity_monitoring next. After stop/reconnect, Electron often
                    # sends only set_project_task — start monitoring in that case.
                    if is_project_task_only:
                        monitoring_inactive = (
                            stop_monitoring_event.is_set() or not monitoring_started[0]
                        )
                        if monitoring_inactive:
                            logger.info(
                                "Received set_project_task while monitoring is stopped — "
                                "starting monitoring (start_activity_monitoring not received; "
                                "common after stop or IPC reconnect)"
                            )
                            is_timer_session_start = True
                        else:
                            logger.info(
                                "Received set_project_task only - project/task updated; "
                                "waiting for start_activity_monitoring"
                            )
                            return

                    # Check if user is authenticated before timer-session start
                    if not user_entity_global[0]:
                        logger.warning("Cannot start monitoring - user not authenticated yet")
                        for _ in range(50):  # Wait up to 5 seconds for AuthThread
                            if user_entity_global[0]:
                                break
                            time.sleep(0.1)

                        if not user_entity_global[0]:
                            retry_authentication_once("start_command")

                        if not user_entity_global[0]:
                            logger.error("Cannot start monitoring - authentication not complete")
                            return

                    was_stopped_at_entry = stop_monitoring_event.is_set()
                    forced_stop_for_new_session = False

                    # set_project_task only: do not restart monitoring when already active
                    if (
                        is_project_task_only
                        and not was_stopped_at_entry
                        and monitoring_started[0]
                    ):
                        logger.info(
                            "Monitoring already active - ignoring start command (project/task IDs only)"
                        )
                        return

                    # User started timer but stop IPC was missed (common after sleep) — force-stop then new session
                    if (
                        (is_timer_session_start or getattr(activity, '_bump_counter_on_next_monitoring_start', False))
                        and not was_stopped_at_entry
                        and monitoring_started[0]
                    ):
                        forced_stop_for_new_session = True
                        logger.warning(
                            "Monitoring still active on start (action=%s) — forcing stop before new session "
                            "(missed stop after sleep?)",
                            action,
                        )
                        thread_items = list(thread_manager.thread_controls.items())
                        for name, info in thread_items:
                            if name not in ACTIVITY_MANAGED_THREAD_NAMES:
                                continue
                            if info.get("stop_event"):
                                info["stop_event"].set()
                            if info.get("async_stop_event"):
                                info["async_stop_event"].set()
                        stop_monitoring_event.set()
                        monitoring_started[0] = False

                    # Only skip counter bump if break_stop thread is actively finishing NOW (counter already bumped there)
                    break_stop_thread_alive_at_entry = (
                        break_stop_thread_ref[0] is not None and break_stop_thread_ref[0].is_alive()
                    )

                    if break_stop_thread_ref[0] is not None and break_stop_thread_ref[0].is_alive():
                        logger.info("Waiting for break_stop_event thread to complete before starting new threads (prevents duplicate intervals)...")
                        max_wait_time = 10  # Maximum 10 seconds to wait
                        wait_interval = 0.2  # Check every 200ms
                        waited_time = 0
                        while break_stop_thread_ref[0].is_alive() and waited_time < max_wait_time:
                            time.sleep(wait_interval)
                            waited_time += wait_interval
                        
                        if break_stop_thread_ref[0].is_alive():
                            logger.warning("break_stop_thread still running after %ss timeout - proceeding anyway to avoid blocking", max_wait_time)
                        else:
                            logger.info("✓ break_stop_thread completed after %ss - safe to start new threads", waited_time)
                        
                        # Clear the reference after waiting
                        break_stop_thread_ref[0] = None

                    # Ensure previous MonitorThread finished cleanup; otherwise its shutdown loop can
                    # set stop_event on thread_controls that already belong to the new session (Windows
                    # after sleep: WebSocket auth completes then "not connected" / subscribe failures).
                    prev_monitor = monitor_thread_ref[0]
                    if prev_monitor is not None and prev_monitor.is_alive():
                        logger.info("Joining previous MonitorThread before starting new monitoring session...")
                        prev_monitor.join(timeout=25)
                        if prev_monitor.is_alive():
                            logger.warning(
                                "Previous MonitorThread still alive after 25s join — proceeding; WebSocket race still possible"
                            )
                    
                    # If monitoring was stopped or never started, start it
                    # Increment counter if this is a restart after stop (new start/stop cycle)
                    # NOTE: Counter is already incremented in break_stop_event when break finishes, so skip increment here if break just finished
                    is_break_start = activity and activity.is_break == 1
                    break_id = getattr(activity, 'break_id', 0) or 0 if activity else 0
                    break_stop_incremented_this_start = (
                        break_stop_thread_alive_at_entry and break_id == 0 and not is_break_start
                    )
                    wake_requires_bump = bool(
                        activity and getattr(activity, '_bump_counter_on_next_monitoring_start', False)
                    )

                    should_bump_counter = False
                    if activity and not is_break_start and not break_stop_incremented_this_start:
                        # Every monitoring start gets a new counter (stop→sleep→start, wake, missed stop)
                        should_bump_counter = True
                        if _activity_should_skip_counter_bump_dedup(activity):
                            should_bump_counter = False
                            logger.info(
                                "[SessionCounter] Skipping duplicate bump within dedup window "
                                "(same start wave; counter=%s, intervals use %s)",
                                activity.last_count,
                                activity.last_count + 1,
                            )

                    if break_stop_incremented_this_start:
                        logger.info(
                            "=== BREAK FINISHED - RESTART === Counter already incremented in break_stop "
                            "(using counter=%s, will save intervals with counter=%s)",
                            activity.last_count,
                            activity.last_count + 1,
                        )
                    elif should_bump_counter:
                        if wake_requires_bump:
                            reason = "wake_then_start"
                        elif forced_stop_for_new_session:
                            reason = "forced_new_session_missed_stop"
                        elif is_timer_session_start:
                            reason = "timer_start_new_session"
                        elif is_project_task_only:
                            reason = "timer_start_set_project_task"
                        else:
                            reason = "timer_start"
                        _activity_increment_session_counter(activity, reason=reason)
                    elif is_break_start:
                        logger.info(
                            "=== BREAK START === Using counter: %s (will save intervals with counter=%s, is_break=1)",
                            activity.last_count,
                            activity.last_count + 1,
                        )
                    else:
                        logger.info(
                            "=== SESSION START (no counter bump) === Using counter: %s "
                            "(will save intervals with counter=%s)",
                            activity.last_count,
                            activity.last_count + 1,
                        )
                    
                    logger.info("Starting monitoring threads (Python process already running - fast start)...")
                    stop_monitoring_event.clear()
                    monitoring_started[0] = True
                    
                    # CRITICAL: Clear force_stop on explicit user start so a prior idle/WS
                    # stop cannot leave status stuck False and break the next Start.
                    if activity:
                        activity.force_stop = False

                    # CRITICAL: Set status to True immediately when monitoring starts (unless force_stop is set)
                    # This ensures intervals are saved right away, even before status thread updates it
                    # This is especially important after break stops when monitoring resumes
                    # Only set to True if force_stop is not True (tracking is not explicitly disabled)
                    # CRITICAL FIX FOR WINDOWS: Check for break_id to ensure break monitoring continues
                    # When on break, we set status=True to start monitoring, and break logic will ensure
                    # monitoring continues even if status thread sets status=False later
                    if activity and not getattr(activity, 'force_stop', False):
                        current_break_id = getattr(activity, 'break_id', 0) or 0
                        if current_break_id != 0:
                            # On break - set status to True to ensure monitoring starts immediately
                            # The break logic in activity_controller (line 1562) will continue monitoring
                            # even if status becomes False later, as long as break_id != 0
                            activity.status = True
                            logger.info("[Windows Break Fix] Set status=True when monitoring starts during break (break_id=%s) - break logic will ensure monitoring continues even if status becomes False", current_break_id)
                        else:
                            # Normal start, not on break
                            activity.status = True  
                            logger.info("Set status=True immediately when monitoring starts (will be updated by status thread)")
                    
                    # Start monitoring in a new thread
                    if user_entity_global[0]:
                        from threading import Thread
                        
                        # CRITICAL FIX: Properly stop all existing threads before starting new ones
                        # This prevents duplicate threads from running simultaneously after break stops
                        # IMPORTANT: Ensure all queue items are saved BEFORE stopping threads
                        
                        # ALWAYS check for running threads, even if thread_controls is empty
                        # This handles cases where threads are still running but not in thread_controls
                        threads_to_stop = []
                        if thread_manager.thread_controls:
                            threads_to_stop = [
                                (name, info)
                                for name, info in thread_manager.thread_controls.items()
                                if name in ACTIVITY_MANAGED_THREAD_NAMES
                            ]
                        
                        # Also check for any threads that might be running but not in thread_controls
                        # by checking all active threads
                        active_threads = threading.enumerate()
                        device_threads = [t for t in active_threads if t.name == "device" and t.is_alive()]
                        if device_threads and not any(name == "device" for name, _ in threads_to_stop):
                            logger.warning("Found %s device thread(s) running but not in thread_controls - will force stop", len(device_threads))
                        
                        if threads_to_stop or device_threads:
                            logger.info("Preparing to stop existing threads before restart to prevent duplicates...")
                            
                            # STEP 1: Flush all pending queue items BEFORE stopping threads
                            # This ensures no data is lost when threads are stopped
                            try:
                                logger.info("Flushing all pending queue items before stopping threads...")
                                # Access db_writer_service to flush queue
                                from ws_package.services.db_writer_service import db_writer_service
                                from ws_package.utils.db_write_queue import db_write_queue
                                
                                # Check queue status
                                initial_queue_size = db_write_queue.get_total_queue_size()
                                if initial_queue_size > 0:
                                    logger.info("Found %s pending queue items - processing before stopping threads...", initial_queue_size)
                                    # Run async flush in a new event loop since we're in a sync context
                                    loop = asyncio.new_event_loop()
                                    asyncio.set_event_loop(loop)
                                    try:
                                        processed_count = loop.run_until_complete(
                                            db_writer_service.process_immediately(ignore_stop_event=True)
                                        )
                                        logger.info("✓ Flushed %s queue items before stopping threads", processed_count)
                                        # Wait a bit for commit to be visible
                                        loop.run_until_complete(asyncio.sleep(0.5))
                                    except Exception as flush_error:
                                        logger.error("Error flushing queue before stopping threads: %s", flush_error, exc_info=True)
                                    finally:
                                        loop.close()
                                else:
                                    logger.info("No pending queue items - threads can be stopped safely")
                            except Exception as e:
                                logger.error("Error checking/flushing queue before stopping threads: %s", e, exc_info=True)
                                # Continue with thread stopping even if flush fails
                            
                            # STEP 2: Stop activity-managed threads only (keep db_writer alive)
                            logger.info("Stopping existing activity monitoring threads...")
                            for name, info in threads_to_stop:
                                if info.get("stop_event"):
                                    info["stop_event"].set()
                                    logger.info("Stop signal sent to existing thread: %s", name)
                                if info.get("async_stop_event"):
                                    info["async_stop_event"].set()
                                    logger.info("Async stop signal sent to existing thread: %s", name)
                            
                            # CRITICAL: Also stop any device threads found outside thread_controls
                            for device_thread in device_threads:
                                logger.warning("Found orphaned device thread: %s (id=%s) - cannot stop gracefully (no stop_event)", device_thread.name, device_thread.ident)
                            
                            # Wait longer for device threads to finish current interval creation
                            logger.info("Waiting for device threads to process stop signal and finish current operations...")
                            time.sleep(2.0)
                            
                            # STEP 3: Wait for activity threads to finish gracefully
                            logger.info("Waiting for activity threads to finish gracefully...")
                            for name, info in threads_to_stop:
                                if info.get("thread") and info["thread"].is_alive():
                                    logger.info("Waiting for thread '%s' to stop...", name)
                                    timeout = 5 if name == "device" else 3
                                    info["thread"].join(timeout=timeout)
                                    if info["thread"].is_alive():
                                        logger.warning("Thread '%s' did not stop within %ss timeout - proceeding anyway", name, timeout)
                                    else:
                                        logger.info("✓ Thread '%s' stopped successfully", name)
                            
                            # STEP 4: Verify all device threads are actually stopped
                            logger.info("Verifying all device threads are stopped...")
                            active_threads_after = threading.enumerate()
                            remaining_device_threads = [t for t in active_threads_after if t.name == "device" and t.is_alive()]
                            if remaining_device_threads:
                                logger.error("⚠ WARNING: %s device thread(s) still running after stop attempt!", len(remaining_device_threads))
                                for t in remaining_device_threads:
                                    logger.error("  - Thread: %s (id=%s, daemon=%s)", t.name, t.ident, t.daemon)
                                # Wait a bit more and check again
                                time.sleep(1.0)
                                active_threads_final = threading.enumerate()
                                remaining_device_threads_final = [t for t in active_threads_final if t.name == "device" and t.is_alive()]
                                if remaining_device_threads_final:
                                    logger.error("⚠ CRITICAL: %s device thread(s) still running - duplicates may occur!", len(remaining_device_threads_final))
                            else:
                                logger.info("✓ All device threads verified as stopped")
                            
                            # STEP 5: Remove only activity thread references (keep db_writer/websocket)
                            logger.info("Clearing activity thread references after stop")
                            for name in ACTIVITY_MANAGED_THREAD_NAMES:
                                thread_manager.thread_controls.pop(name, None)
                            ensure_db_writer_thread_running()
                            logger.info("✓ Activity threads stopped - ready to start new monitoring threads")
                        else:
                            logger.info("No existing threads to stop - starting fresh")
                        
                        monitor_thread = Thread(
                            target=monitor_threads,
                            args=(activity, user_entity_global[0], stop_monitoring_event),
                            daemon=True,
                            name="MonitorThread"
                        )
                        logger.debug("[Timing][Python] Starting MonitorThread...")
                        monitor_thread.start()
                        monitor_thread_ref[0] = monitor_thread
                        logger.debug("[Timing][Python] MonitorThread started")

                        # Start WebSocket only after prior thread_controls were cleared and MonitorThread
                        # is running — avoids stop-all-threads killing a websocket just created above.
                        ue_ws = user_entity_global[0]
                        if ue_ws is not None:
                            try:
                                ws_info_ws = thread_manager.thread_controls.get("websocket")
                            except Exception:
                                ws_info_ws = None
                            need_ws = True
                            if ws_info_ws:
                                ws_th = ws_info_ws.get("thread")
                                if ws_th and ws_th.is_alive():
                                    need_ws = False
                            # if need_ws:
                            #     try:
                            #         logger.info("[WebSocket] Starting WebSocket service after workers up (start command)")
                            #         start_websocket_service(ue_ws)
                            #     except Exception as e:
                            #         logger.error("[WebSocket] Failed to start WebSocket service on start command: %s", e, exc_info=True)
                            # else:
                                logger.debug("[WebSocket] Existing WebSocket thread is alive; reusing connection for this session")
                        else:
                            logger.warning("[WebSocket] Cannot start WebSocket service: user_entity_global[0] is None")

                        logger.info("✓ Monitoring started successfully - Counter: %s", activity.last_count)
                        timing_log(f"Monitoring threads started via start command (fast restart) - Counter: {activity.last_count}")
                    else:
                        logger.error("Cannot start monitoring - user_entity not available")
                        monitoring_started[0] = False
                        stop_monitoring_event.set()
                        
                except Exception as e:
                    logger.error("Error starting monitoring: %s", e, exc_info=True)
                    monitoring_started[0] = False
                    stop_monitoring_event.set()  # Reset on error
                
                return

            # Handle set_project_task (can be either msg_type='set_project_task' or action='set_project_task')
            if msg_type == 'set_project_task' or action == 'set_project_task':
                project_id = data.get('projectId') if data else None
                task_id = data.get('taskId') if data else None
                logger.info("Setting project/task: project_id=%s, task_id=%s", project_id, task_id)
                _handle_set_project_task_from_electron(project_id, task_id)
                return
            
            # Handle get_monitoring_status - check if monitoring is actually running
            if msg_type == 'get_monitoring_status' or action == 'get_monitoring_status':
                try:
                    # Check if monitoring is actually active
                    is_monitoring_active = not stop_monitoring_event.is_set() and monitoring_started[0]
                    
                    # Check if key threads are running
                    thread_status = {}
                    if thread_manager.thread_controls:
                        for name, info in thread_manager.thread_controls.items():
                            thread = info.get("thread")
                            if thread:
                                thread_status[name] = {
                                    "alive": thread.is_alive(),
                                    "name": thread.name if hasattr(thread, 'name') else name
                                }
                    
                    # Check if activity controller has status=True (monitoring enabled)
                    activity_status = False
                    if activity:
                        activity_status = getattr(activity, 'status', False)
                    
                    # Count active threads
                    active_thread_count = sum(1 for status in thread_status.values() if status.get("alive", False))
                    
                    status_data = {
                        "monitoring_active": is_monitoring_active,
                        "monitoring_started": monitoring_started[0],
                        "stop_event_set": stop_monitoring_event.is_set(),
                        "activity_status": activity_status,
                        "active_thread_count": active_thread_count,
                        "thread_status": thread_status,
                        "user_authenticated": user_entity_global[0] is not None
                    }
                    
                    logger.info("Monitoring status check: %s", status_data)
                    
                    # Send status back to Electron
                    try:
                        electron_comm.send_to_electron("monitoring_status", status_data)
                        logger.info("Monitoring status sent to Electron")
                    except Exception as e:
                        logger.warning("Failed to send monitoring status to Electron: %s", e)
                    
                    return
                except Exception as e:
                    logger.error("Error checking monitoring status: %s", e, exc_info=True)
                    # Send error status
                    try:
                        electron_comm.send_to_electron("monitoring_status", {
                            "monitoring_active": False,
                            "error": str(e)
                        })
                    except Exception:
                        pass
                    return

            # Handle break_start - when user selects a break
            if msg_type == 'break_start' or action == 'start_break':
                # Handle nested data structure if data is a string (double-encoded JSON)
                if isinstance(data, str):
                    try:
                        data = json.loads(data)
                        logger.info("Parsed nested JSON data from break_start message")
                    except (json.JSONDecodeError, TypeError):
                        logger.warning("Could not parse data as JSON: %s", data)
                        data = {}
                
                break_id = data.get('breakId') if data else None
                break_name = data.get('breakName') if data else None
                start_time = data.get('startTime') if data else None
                project_id = data.get('projectId') if data else None
                task_id = data.get('taskId') if data else None
                
                logger.info("=== Received break start command ===")
                logger.info("Break ID: %s, Break Name: %s", break_id, break_name)
                logger.info("Start Time: %s, Project ID: %s, Task ID: %s", start_time, project_id, task_id)
                logger.info("Raw data type: %s, data content: %s", type(data), data)
                
                # CRITICAL: Verify break_id is set
                if break_id is None or break_id == 0:
                    logger.error("CRITICAL: break_id is None or 0! Cannot start break. Data: %s, Message: %s", data, message)
                    logger.error("Full message structure: type=%s, action=%s, data=%s", msg_type, action, data)
                    return  # Can't start break without break_id
                
                logger.debug("[Timing][Python] Received break_start command at %s", time.time())
                
                # CRITICAL: Do NOT set break_id/is_break/break_start_time here - causes race where last interval (e.g. 15:35:00-15:35:43) gets saved with break_id. Set break state only AFTER last_api_call in the thread.
                if activity:
                    def _apply_break_state():
                        """Set break state on activity (call after last_api_call so last interval is saved as regular)."""
                        from datetime import datetime as dt, timedelta
                        activity.set_is_break(1)
                        if hasattr(activity, 'break_id'):
                            activity.break_id = break_id
                        if hasattr(activity, 'break_project_id'):
                            activity.break_project_id = str(project_id) if project_id else activity.current_project_id
                            activity.break_task_id = str(task_id) if task_id else activity.current_task_id
                        work_stop = getattr(activity, '_last_work_stop_time', None)
                        if work_stop is not None:
                            work_stop = work_stop.replace(microsecond=0)
                            activity.break_start_time = work_stop + timedelta(seconds=1)
                            activity.break_start_time = activity.break_start_time.replace(microsecond=0)
                            logger.info(
                                "Break start=%s (work stop=%s + 1s; Electron click time ignored for ordering)",
                                activity.break_start_time,
                                work_stop,
                            )
                        elif hasattr(activity, '_break_start_after_work_stop'):
                            activity.break_start_time = activity._break_start_after_work_stop()
                        else:
                            activity.break_start_time = dt.now().replace(microsecond=0) + timedelta(seconds=1)
                        logger.info("Break state applied after last_api_call: break_id=%s, break_start_time=%s", break_id, activity.break_start_time)
                    
                    # Run last_api_call first (break_id still 0 so last interval saved as regular), then apply break state, then break_start_event
                    def call_break_start_event():
                        """Run last_api_call then set break state then break_start_event in a separate thread."""
                        try:
                            if activity and user_entity_global[0]:
                                loop = asyncio.new_event_loop()
                                asyncio.set_event_loop(loop)
                                try:
                                    logger.info("Break start: running last_api_call first (last interval saved as regular, break_id=0)...")
                                    loop.run_until_complete(activity.stop_event(user_entity_global[0], keep_idle_time=True))
                                    logger.info("Break start: last_api_call done, applying break_id and break state...")
                                    _apply_break_state()
                                    logger.info("Break start: running break_start_event...")
                                    loop.run_until_complete(activity.break_start_event())
                                    logger.info("activity.break_start_event() completed successfully")
                                except Exception as e:
                                    logger.error("Error in break_start_event thread: %s", e, exc_info=True)
                                finally:
                                    loop.close()
                            elif not user_entity_global[0]:
                                logger.warning("Cannot call break_start_event - user_entity not available")
                            else:
                                logger.warning("Cannot call break_start_event - activity object not available")
                        except Exception as e:
                            logger.error("Error in call_break_start_event thread: %s", e, exc_info=True)
                    
                    from threading import Thread
                    break_start_thread = Thread(target=call_break_start_event, daemon=False, name="BreakStartEventThread")
                    break_start_thread.start()
                    logger.info("Break start thread: last_api_call (regular) -> apply break_id -> break_start_event")
                else:
                    logger.warning("Cannot set break status - activity controller not initialized yet")
                
                logger.info("=== Break start command processed ===")
                return
            
            # Handle break_stop - when user stops a break
            if msg_type == 'break_stop' or action == 'stop_break':
                logger.info("=== Received break stop command ===")
                if activity:
                    # CRITICAL: Preserve break_id before calling break_stop_event
                    # break_id is needed to update dummy entries and process intervals
                    preserved_break_id = getattr(activity, 'break_id', None)
                    logger.info("Preserved break_id=%s before calling break_stop_event", preserved_break_id)
                    
                    # Call break_stop_event BEFORE resetting is_break
                    # This ensures break_id is still available for processing
                    def call_break_stop_event():
                        """Call break_stop_event in a separate thread with its own event loop"""
                        try:
                            if activity:
                                logger.info("Calling activity.break_stop_event()...")
                                # Create a new event loop for this thread
                                loop = asyncio.new_event_loop()
                                asyncio.set_event_loop(loop)
                                try:
                                    # Call break_stop_event (break_id is still preserved)
                                    loop.run_until_complete(activity.break_stop_event())
                                    logger.info("activity.break_stop_event() completed successfully")
                                    
                                    # CRITICAL: Increment counter when break stops
                                    # This ensures counter is incremented immediately when break finishes
                                    old_counter = activity.last_count
                                    new_counter = activity.last_count + 1
                                    activity.last_count = new_counter
                                    logger.info("=== BREAK STOP - COUNTER INCREMENTED === Counter incremented from %s to %s (will save intervals with counter=%s)", old_counter, new_counter, new_counter + 1)
                                    
                                    # CRITICAL: Clear held idle intervals when counter changes (new session starts)
                                    # Held intervals from previous counter should not be carried over
                                    if hasattr(activity, '_held_idle_intervals') and activity._held_idle_intervals:
                                        held_count = len(activity._held_idle_intervals)
                                        activity._held_idle_intervals = []
                                        logger.info("=== BREAK STOP - CLEARED HELD IDLE INTERVALS === Cleared %s held idle interval(s) from previous counter (counter changed from %s to %s)", held_count, old_counter, new_counter)
                                    
                                    # CRITICAL: Clear idle popup state when counter changes (new session starts)
                                    # This ensures idle popup state doesn't carry over to new session
                                    if hasattr(activity, '_idle_popup_active'):
                                        activity._idle_popup_active = False
                                        activity._idle_popup_triggered_at = None
                                        activity._api_pause_active = False
                                        activity._last_idle_resolution_time = None  # CRITICAL: Clear last idle resolution time so intervals after break can be marked as is_idle
                                        logger.info("=== BREAK STOP - CLEARED IDLE POPUP STATE === Cleared idle popup state for new session (counter=%s)", new_counter)
                                    
                                    # New session starts at break end (not after async API work) so activity/log
                                    # "start" matches interval boundaries — avoid interval.from < payload.start.
                                    import datetime as dt
                                    break_stop_ts = getattr(activity, '_break_stop_time', None)
                                    if break_stop_ts is not None:
                                        activity.current_session_start_time = break_stop_ts.replace(microsecond=0)
                                    else:
                                        activity.current_session_start_time = dt.datetime.now().replace(microsecond=0)
                                    logger.info(
                                        "=== BREAK STOP - SESSION START TIME SET === current_session_start_time=%s (payload start for today will use this; break_stop_time=%s)",
                                        activity.current_session_start_time,
                                        break_stop_ts,
                                    )

                                    # Register an immediate open work session (start=<break stop time>, stop=null, intervals=[]).
                                    # This ensures the server gets the new session boundary right after break stop.
                                    try:
                                        open_session_user = getattr(activity, "user_entity", None) or user_entity_global[0]
                                        if open_session_user is not None:
                                            loop.run_until_complete(
                                                activity.push_open_session_activity_log(
                                                    open_session_user,
                                                    start_time=activity.current_session_start_time,
                                                    break_id=None,
                                                )
                                            )
                                            logger.info(
                                                "=== BREAK STOP - OPEN WORK SESSION POSTED === start=%s",
                                                activity.current_session_start_time,
                                            )
                                        else:
                                            logger.warning(
                                                "=== BREAK STOP - OPEN WORK SESSION SKIPPED === user_entity unavailable"
                                            )
                                    except Exception as open_session_err:
                                        logger.error(
                                            "=== BREAK STOP - OPEN WORK SESSION FAILED === %s",
                                            open_session_err,
                                            exc_info=True,
                                        )
                                    
                                    # After break_stop_event completes, reset break status
                                    activity.set_is_break(0)
                                    logger.info("ActivityController.is_break set to 0 (not on break) - after break_stop_event")
                                    
                                    # CRITICAL: Clear break_id and break_start_time after break_stop_event completes
                                    # This ensures new intervals created after break stops will have is_break=0
                                    if hasattr(activity, 'break_id'):
                                        cleared_break_id = activity.break_id
                                        activity.break_id = 0
                                        logger.info("ActivityController.break_id cleared (was %s) - new intervals will have is_break=0", cleared_break_id)
                                    if hasattr(activity, 'break_start_time'):
                                        activity.break_start_time = None
                                        logger.info("ActivityController.break_start_time cleared - new intervals will not be marked as break intervals")
                                    # Clear break_project_id and break_task_id when break stops
                                    if hasattr(activity, 'break_project_id'):
                                        activity.break_project_id = None
                                        logger.info("ActivityController.break_project_id cleared")
                                    if hasattr(activity, 'break_task_id'):
                                        activity.break_task_id = None
                                        logger.info("ActivityController.break_task_id cleared")
                                except Exception as e:
                                    logger.error("Error in break_stop_event: %s", e, exc_info=True)
                                    # Still reset is_break even if break_stop_event fails
                                    activity.set_is_break(0)
                                    # Also clear break_id and break_start_time even if break_stop_event fails
                                    if hasattr(activity, 'break_id'):
                                        cleared_break_id = activity.break_id
                                        activity.break_id = 0
                                        logger.info("ActivityController.break_id cleared after error (was %s) - new intervals will have is_break=0", cleared_break_id)
                                    if hasattr(activity, 'break_start_time'):
                                        activity.break_start_time = None
                                        logger.info("ActivityController.break_start_time cleared after error")
                                    # Clear break_project_id and break_task_id even if break_stop_event fails
                                    if hasattr(activity, 'break_project_id'):
                                        activity.break_project_id = None
                                        logger.info("ActivityController.break_project_id cleared after error")
                                    if hasattr(activity, 'break_task_id'):
                                        activity.break_task_id = None
                                        logger.info("ActivityController.break_task_id cleared after error")
                                finally:
                                    # Notify Electron so it can update UI without fixed 10s wait (event-driven)
                                    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("break_stop_completed", {"status": "success"})
                                            logger.info("break_stop_completed sent to Electron")
                                    except Exception as send_e:
                                        logger.warning("Failed to send break_stop_completed to Electron: %s", send_e)
                                    loop.close()
                            else:
                                logger.warning("Cannot call break_stop_event - activity object not available")
                        except Exception as e:
                            logger.error("Error in call_break_stop_event thread: %s", e, exc_info=True)
                    
                    # Start break_stop_event in a separate thread so it doesn't block message processing
                    from threading import Thread
                    break_stop_thread = Thread(target=call_break_stop_event, daemon=False, name="BreakStopEventThread")
                    break_stop_thread_ref[0] = break_stop_thread  # Store reference to track completion
                    break_stop_thread.start()
                    logger.info("Break stop event thread started - break_stop_event will execute in background")
                else:
                    logger.warning("Cannot reset break status - activity controller not initialized yet")
                logger.info("=== Break stop command processed ===")
                return

            if msg_type == 'ping':
                try:
                    ts = message.get('timestamp')
                    electron_comm.send_to_electron('pong', {'timestamp': ts})
                except Exception as pe:
                    logger.debug('ping reply failed: %s', pe)
                return

            # Unhandled message type — log so we know what to add
            logger.warning("Unhandled message type: type=%s, action=%s", msg_type, action)

        def apply_authenticated_user(user_entity):
            """Run post-auth setup (S3, caches) after UserController.authenticate() succeeds."""
            from ws_package.controllers.server_controller import ServerController

            user_entity_global[0] = user_entity
            activity.user_entity = user_entity
            pref = getattr(user_entity, "timezone_preference", 1)
            logger.info(
                "[Auth] timezone_preference=%s (0=checks ON, 1=checks OFF)",
                pref,
            )
            if int(pref) == 1:
                had_timezone_stop = (
                    activity._timezone_mismatch
                    or activity._time_error_type == "timezone_error"
                )
                activity._timezone_mismatch = False
                if activity._time_error_type == "timezone_error":
                    activity._time_error_type = None
                if had_timezone_stop:
                    activity.force_stop = False
                time_monitor = getattr(
                    getattr(activity, "activity_manager", None),
                    "time_monitor",
                    None,
                )
                if time_monitor:
                    try:
                        time_monitor.stop_monitoring()
                    except Exception:
                        pass
                logger.info(
                    "[Auth] timezone_preference=1 — timezone/time enforcement disabled"
                )
            else:
                logger.info(
                    "[Auth] timezone_preference=0 — timezone/time checks ENABLED"
                )
            auth_error[0] = None

            server = ServerController()
            asyncio.run(server.ensure_fresh_s3_bucket(user_entity))
            try:
                from ws_package.services.db_writer_service import db_writer_service
                from ws_package.utils.s3 import clear_s3_credential_cache
                clear_s3_credential_cache(user_entity.org_id)
                asyncio.run(
                    db_writer_service.process_immediately(ignore_stop_event=True)
                )
            except Exception as _s3_flush_err:
                logger.warning(
                    "Could not flush S3 config to database after auth: %s",
                    _s3_flush_err,
                )
            try:
                from ws_package.utils.s3 import prefetch_s3_credentials
                asyncio.run(prefetch_s3_credentials(user_entity.org_id))
            except Exception as _s3_pf_err:
                logger.warning("S3 credential pre-fetch failed (non-fatal): %s", _s3_pf_err)
            # Enable hourly log upload as soon as auth+S3 are ready (do not wait for
            # monitoring start). Silent mode auto-starts immediately; enabling here
            # avoids the first upload racing before credentials are usable and then
            # waiting a full hour before retrying.
            try:
                from ws_package.log.writer import enable_hourly_log_upload
                from ws_package.configs import config as _cfg

                s3_logs_path = _cfg("s3_logs_path", "Sync") or "logs/"
                enable_hourly_log_upload(user_entity, user_entity.org_id, s3_logs_path)
                logger.info(
                    "Hourly log upload enabled after auth (org_id=%s)",
                    user_entity.org_id,
                )
            except Exception as _log_up_err:
                logger.warning(
                    "Could not enable hourly log upload after auth: %s",
                    _log_up_err,
                )
            try:
                from ws_package.services.screenshot_service import ScreenshotService as _SS
                _ss_retry = _SS()
                _ss_retry.org_id = user_entity.org_id
                _bucket = getattr(user_entity, "bucket", "aws")
                asyncio.run(_ss_retry.retry_pending_uploads(_bucket))
            except Exception as _retry_err:
                logger.warning("Pending screenshot retry failed (non-fatal): %s", _retry_err)

            if platform.system() == "Windows":
                try:
                    ws_info = thread_manager.thread_controls.get("websocket")
                    ws_thread = ws_info.get("thread") if ws_info else None
                    # if ws_thread and ws_thread.is_alive():
                    #     logger.debug("[WebSocket] WebSocket service already running after authentication")
                    # else:
                    #     logger.info("[WebSocket] Starting WebSocket service after authentication")
                    #     start_websocket_service(user_entity)
                except Exception as ws_error:
                    logger.error(
                        "[WebSocket] Failed to start WebSocket service after authentication: %s",
                        ws_error,
                        exc_info=True,
                    )

            logger.info("User authenticated: %s", user_entity.username)
            timing_log(f"User authenticated: {user_entity.username}")

        def retry_authentication_once(reason: str) -> bool:
            """Re-read Electron userdetail once (e.g. login finished after agent boot)."""
            if user_entity_global[0]:
                return True
            if auth_retry_used[0]:
                logger.debug("[Auth] Skipping retry (%s) — already attempted this session", reason)
                return False

            auth_retry_used[0] = True
            logger.info("[Auth] Retrying authentication (%s)...", reason)
            try:
                user_entity = authenticate_user()
                if not user_entity:
                    logger.warning(
                        "[Auth] Retry authentication failed — no user in Electron or Python DB"
                    )
                    return False

                apply_authenticated_user(user_entity)
                if silent_build:
                    fetch_and_apply_org_settings(user_entity, activity)
                logger.info("[Auth] Retry authentication succeeded for %s", user_entity.username)
                return True
            except Exception as e:
                logger.error("[Auth] Retry authentication error: %s", e, exc_info=True)
                return False

        def authenticate_and_setup():
            """
            Run in AuthThread: read user credentials from the Electron SQLite DB,
            build UserEntity, save to Python DB, fetch org config from API.

            On success  → sets auth_complete event; main thread drains deferred queue.
            On failure  → sets auth_error[0] + auth_complete; main thread falls back
                          to last cached user from Python DB and notifies Electron.

            The deferred message queue is drained EXCLUSIVELY by the main thread
            after auth_complete fires — never inside this function — to prevent
            process_message() being called from two threads simultaneously.
            """
            try:
                timing_log("Background authentication started")

                user_entity = authenticate_user()

                if user_entity:
                    apply_authenticated_user(user_entity)
                    if silent_build:
                        fetch_and_apply_org_settings(user_entity, activity)
                        logger.info(
                            "[Silent] Authentication complete — will auto-start monitoring"
                        )
                    else:
                        logger.info("Authentication complete - waiting for start command from Electron")
                        timing_log("Authentication complete - monitoring will start on user request")
                    auth_complete.set()
                    timing_log("Background authentication completed")
                    # Deferred messages are processed exclusively by the main
                    # thread after auth_complete fires.  Processing them here
                    # as well would race with the main thread and call
                    # process_message() from two threads simultaneously.
                    
                else:
                    logger.warning("Authentication failed - no user found")
                    auth_error[0] = (
                        "No user_token_file.txt or stealth_login failed (silent)"
                        if silent_build
                        else "No user found in Electron database"
                    )
                    auth_complete.set()
                    if not silent_build:
                        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("auth_failed", {"reason": "No user found in Electron database"})
                        except Exception as _notify_err:
                            logger.warning("Could not notify Electron of auth failure: %s", _notify_err)
                    
            except Exception as e:
                logger.error("Error in background authentication: %s", e, exc_info=True)
                auth_error[0] = str(e)
                auth_complete.set()

        # Start auth thread
        auth_thread = Thread(target=authenticate_and_setup, daemon=True, name="AuthThread")
        auth_thread.start()

        try:
            # Wait for authentication to complete (with timeout)
            logger.info("Waiting for background authentication to complete...")
            auth_complete.wait(timeout=300)  # 5 minute timeout
            
            if auth_error[0]:
                logger.error("Authentication failed: %s", auth_error[0])
                if silent_build:
                    logger.info("[Silent] Auth failed — fallback to last Python DB user if any")
                else:
                    logger.info("Falling back to last user (but NOT starting monitoring - waiting for start command)...")
                # Don't start monitoring automatically - just set up user data
                try:
                    user_entity = asyncio.run(get_last_user())
                    user_org = asyncio.run(get_last_org())
                    if user_entity:
                        user_entity_global[0] = user_entity
                        # Refresh S3 + enable log upload so silent auto-start can upload logs
                        # even when stealth_login failed and we reused the last DB user.
                        try:
                            apply_authenticated_user(user_entity)
                        except Exception as _fallback_auth_err:
                            logger.warning(
                                "[Silent] Fallback apply_authenticated_user failed: %s",
                                _fallback_auth_err,
                            )
                    if user_org:
                        logger.info("#### >> %s", user_org.user_timezone) 
                        activity.org_id = user_org.org_id
                        activity.screenshot_interval = user_org.ssfrequencystatus
                        activity.interval = change(user_org.timeInterval)
                        activity.appandurlstatus = user_org.appandurlstatus
                        activity.blur_intensity = user_org.screenshotblurstatus
                        activity.org_api_timezone = user_org.user_timezone
                        activity.notification_status = user_org.notification_status if user_org.notification_status is not None else activity.notification_status
                    logger.info("Fallback user setup complete - monitoring will start on user request")
                except Exception as e:
                    logger.error("Error in fallback user setup: %s", e, exc_info=True)
            elif not auth_complete.is_set():
                logger.warning("Authentication timeout - proceeding with fallback (but NOT starting monitoring)")
                try:
                    user_entity = asyncio.run(get_last_user())
                    user_org = asyncio.run(get_last_org())
                    if user_entity:
                        user_entity_global[0] = user_entity
                        if silent_build:
                            try:
                                apply_authenticated_user(user_entity)
                            except Exception as _fallback_auth_err:
                                logger.warning(
                                    "[Silent] Timeout fallback apply_authenticated_user failed: %s",
                                    _fallback_auth_err,
                                )
                    if user_org:
                        logger.info("#### >> %s", user_org.user_timezone)
                        activity.org_id = user_org.org_id
                        activity.screenshot_interval = user_org.ssfrequencystatus
                        activity.interval = change(user_org.timeInterval)
                        activity.appandurlstatus = user_org.appandurlstatus
                        activity.blur_intensity = user_org.screenshotblurstatus
                        activity.org_api_timezone = user_org.user_timezone
                        activity.notification_status = user_org.notification_status if user_org.notification_status is not None else activity.notification_status
                    logger.info("Fallback user setup complete - monitoring will start on user request")
                except Exception as e:
                    logger.error("Error in fallback user setup: %s", e, exc_info=True)
            else:
                if silent_build:
                    logger.info("[Silent] Authentication completed — auto-start will run next")
                else:
                    logger.info("Authentication completed successfully - Python process ready, waiting for start command")

            # Drain deferred messages regardless of auth outcome.
            # Previously this only ran in the success branch, so a 'start'
            # command queued during auth failure/timeout was silently dropped.
            # Also, auth-success used to drain the queue inside the auth
            # thread AND here, causing a race; now the auth thread no longer
            # drains it so this is the single drain point.
            deferred_processed = 0
            while not deferred_queue.empty():
                try:
                    deferred_msg = deferred_queue.get_nowait()
                    deferred_processed += 1
                    logger.info("Processing deferred message #%s: type=%s, action=%s", deferred_processed, deferred_msg.get('type'), deferred_msg.get('action'))
                    process_message(deferred_msg)
                except queue.Empty:
                    break
                except Exception as e:
                    logger.error("Error processing deferred message: %s", e, exc_info=True)
            if deferred_processed > 0:
                logger.info("Processed %s deferred message(s)", deferred_processed)

            def _auto_start_silent_monitoring():
                if not silent_build:
                    return
                if not user_entity_global[0]:
                    logger.error("[Silent] Cannot auto-start — user not authenticated")
                    return
                if get_tracker_status() != 'start':
                    logger.info("[Silent] tracker_status=stop — skipping auto-start")
                    return
                if monitoring_started[0] and not stop_monitoring_event.is_set():
                    logger.info("[Silent] Monitoring already active — skip auto-start")
                    return
                if activity:
                    activity.set_project_task_ids(SILENT_DEFAULT_PROJECT_ID, SILENT_DEFAULT_TASK_ID)
                logger.info(
                    "[Silent] Auto-starting monitoring (project=%s task=%s)",
                    SILENT_DEFAULT_PROJECT_ID,
                    SILENT_DEFAULT_TASK_ID,
                )
                process_message({
                    "type": "start",
                    "action": "start_activity_monitoring",
                    "projectId": SILENT_DEFAULT_PROJECT_ID,
                    "taskId": SILENT_DEFAULT_TASK_ID,
                    "data": {
                        "projectId": SILENT_DEFAULT_PROJECT_ID,
                        "taskId": SILENT_DEFAULT_TASK_ID,
                    },
                })

            _auto_start_silent_monitoring()

            # Main message loop - MUST run continuously to handle start/stop commands.
            # This runs unconditionally regardless of whether deferred messages existed.
            if silent_build:
                logger.info("=== [Silent] Main loop running (admin stop via WebSocket / tracker_status) ===")
            else:
                logger.info("=== Main message loop started - ready to receive start/stop commands ===")
            message_count = 0

            # Parent-process watchdog — exits Python when Electron is gone.
            # On Windows 11 after a long sleep Electron can be killed by the OS
            # while the Python process keeps running as an orphan forever.
            # We record Electron's PID (our parent) at loop start and check it
            # every 60 s.  If the parent is dead AND the IPC connection has been
            # disconnected for > ORPHAN_TIMEOUT_SEC, we exit cleanly.
            _electron_parent_pid = os.getppid()
            _last_connected_time = time.time()
            ORPHAN_TIMEOUT_SEC = 300  # 5 minutes of confirmed disconnection → exit
            _watchdog_tick = 0
            _ipc_was_connected = electron_comm.is_connected()

            while True:  # Run forever - process stays alive to handle commands
                try:
                    if silent_build:
                        # No IPC in silent mode — monitoring runs via auto-start + WebSocket admin stop
                        time.sleep(1.0)
                        continue

                    message = electron_comm.get_message(timeout=0.1)
                    ipc_connected = electron_comm.is_connected()
                    if ipc_connected and not _ipc_was_connected:
                        _last_connected_time = time.time()
                        _electron_parent_pid = os.getppid()
                        logger.info(
                            "IPC reconnected — main message loop active (parent pid=%s)",
                            _electron_parent_pid,
                        )
                    _ipc_was_connected = ipc_connected

                    if message:
                        message_count += 1
                        _last_connected_time = time.time()  # reset watchdog on any message
                        logger.info("=== Message #%s received in main loop: type=%s, action=%s ===", message_count, message.get('type'), message.get('action'))
                        process_message(message)
                    else:
                        # Update last-connected timestamp while IPC is alive
                        if ipc_connected:
                            _last_connected_time = time.time()

                        # Parent-process watchdog: check every ~60 s (visible + Electron only)
                        if not silent_build:
                            _watchdog_tick += 1
                            if _watchdog_tick >= 600:  # 600 × 0.1 s = 60 s
                                _watchdog_tick = 0
                                try:
                                    import psutil
                                    parent_alive = psutil.pid_exists(_electron_parent_pid)
                                except Exception:
                                    parent_alive = True  # assume alive if psutil unavailable
                                disconnected_sec = time.time() - _last_connected_time
                                # Do not sys.exit() — that stops only MainThread while pipe/websocket
                                # threads keep running and queue start/stop forever (see logs after stop).
                                if (
                                    not ipc_connected
                                    and not parent_alive
                                    and disconnected_sec > ORPHAN_TIMEOUT_SEC
                                ):
                                    logger.warning(
                                        "Parent process (Electron PID %s) is gone and IPC has been "
                                        "disconnected for %.0f s — terminating orphan Python process.",
                                        _electron_parent_pid,
                                        disconnected_sec,
                                    )
                                    stop_monitoring_event.set()
                                    os._exit(ExitCode.OK)

                        time.sleep(0.01)

                except KeyboardInterrupt:
                    logger.info("KeyboardInterrupt detected. Signaling threads to stop and exiting.")
                    stop_monitoring_event.set()
                    break
                except Exception as e:
                    logger.error("Error in main message loop: %s", e, exc_info=True)
                    time.sleep(1)
        except Exception as e:
            logger.exception("Fatal error in main: %s", e)
            # Attempt graceful shutdown
            try:
                stop_monitoring_event.set()
                stop_electron_communication()
            except Exception:
                pass
            # Clean up HardwareEventValidator event taps
            try:
                from ws_package.utils.hardware_event_validator import cleanup_hardware_validator
                cleanup_hardware_validator()
            except Exception:
                pass
            fallback_user(activity, stop_monitoring_event)

# ----------------- RUN -----------------
if __name__ == "__main__":
    try:
        main()
    except SystemExit:
        raise
    except Exception as _e:
        logger.critical("[E000] Unhandled exception — process terminating: %s", _e, exc_info=True)
        sys.exit(ExitCode.STARTUP_FAILURE)