"""
Screenshot controller module.

Orchestrates interval-based and on-demand screenshot capture, compression,
S3 upload, and local database persistence for the Workstatus desktop agent.

Key classes:
    ScreenshotController: Main controller. Call start_screenshot_activity() to
        begin the capture loop and on_demand_screenshot() for manual captures.

Thread-safety: Designed to run in a single asyncio task. The processed_intervals
    set and last_count counter are not protected by locks; do not share an instance
    across concurrent coroutines.
"""

import datetime
import os
import re
import sys
import platform
from pathlib import Path
from typing import List, Tuple, Optional, Dict
import base64
import asyncio
import random

from sqlalchemy import select, and_
from ws_package.services.screenshot_service import ScreenshotService
from ws_package.entities.screenshot_entity import ScreenshotEntity 
from ws_package.models.base import Screenshots, ActivityInterval, async_session
from ws_package.configs import config
from ws_package.log.writer import LogWriter
from ws_package.utils.db_lock_manager import db_lock_manager
from ws_package.utils.system import get_os_type, precise_sleep
from io import BytesIO

try:
    from PIL import Image
    HAS_PILLOW = True
except Exception:
    HAS_PILLOW = False

# Conditional import for ScreenShot based on OS
try:
    if platform.system() == "Windows":
        from ws_package.monitoring.screenshot_windows import ScreenShot
    else:
        from ws_package.monitoring.screenshots import ScreenShot
except ImportError:
    raise RuntimeError("Error in importing Screenshot module")

logger = LogWriter.file(__name__)

class ScreenshotController:
    def __init__(self, org_id, save_dir, img_format, img_prefix, img_quality, blur_intensity, last_count, s3_screenshot_path, screenshot_interval, interval, limit_start, limit_end, no_of_days):
        self.org_id = org_id
        self.save_dir = save_dir
        self.img_format = img_format
        self.img_prefix = img_prefix
        self.img_quality = img_quality
        self.blur_intensity = blur_intensity
        self.last_count = last_count
        self.s3_screenshot_path = s3_screenshot_path
        self.screenshot_interval = screenshot_interval  # Frequency multiplier (1, 2, or 3)
        self.interval = interval  # Interval in seconds (60, 300, 600, 900 = 1, 5, 10, 15 min)
        self.limit_start = limit_start
        self.limit_end = limit_end
        self.no_of_days = no_of_days
        self.screenshot_service = ScreenshotService()
        # Tracking-enabled flag — kept in sync by ActivityController.update_interval().
        # Using self.status (not the argument passed to __start_screenshot_activity) lets
        # the loop react to dynamic enable/disable without restarting the thread.
        self.status: bool = True
        # Track processed intervals to ensure no interval is missed
        self.processed_intervals = set()  # Store (hour, minute_interval_start) tuples
        # Removed individual db_write_lock - now using centralized db_lock_manager
        self.MAX_SS_SIZE_KB=80
        # Track on-demand screenshots by counter for interval association
        self._on_demand_screenshots_by_counter: Dict[int, List[datetime.datetime]] = {}  # counter -> [timestamps]


    def get_os_type(self):
        return get_os_type()

    def _notify_electron_error(self, code, message: str, recoverable: bool = True) -> None:
        """Send a tracking_error notification to Electron so the user sees the failure."""
        try:
            from ws_package.utils.electron_comm import get_electron_comm
            ec = get_electron_comm()
            if ec and ec.is_connected():
                ec.send_to_electron("tracking_error", {
                    "code": str(code),
                    "message": message,
                    "recoverable": recoverable,
                    "source": "screenshot",
                })
        except Exception as _notify_err:
            logger.debug("[SCREENSHOT] Could not send error notification to Electron: %s", _notify_err)

    def _is_system_sleeping(self) -> bool:
        """
        Check if system is currently sleeping.
        Returns True if system is sleeping, False otherwise.
        """
        try:
            from ws_package.utils.system_wake_detector import get_system_wake_detector
            import time
            import platform
            
            wake_detector = get_system_wake_detector()
            if wake_detector:
                stats = wake_detector.get_stats()
                last_sleep_time = stats.get('last_sleep_time')
                last_wake_time = stats.get('last_wake_time')
                
                # If we have a sleep time and no wake time after it, system is sleeping
                if last_sleep_time:
                    # Convert datetime to timestamp if needed
                    if isinstance(last_sleep_time, datetime.datetime):
                        sleep_timestamp = last_sleep_time.timestamp()
                    else:
                        sleep_timestamp = last_sleep_time
                    
                    current_timestamp = time.time()
                    time_since_sleep = current_timestamp - sleep_timestamp
                    
                    # Check if system is actually responsive using non-blocking OS calls.
                    # macOS previously used a blocking osascript subprocess (up to 1s)
                    # which could block the asyncio event loop. Replaced with non-blocking
                    # alternatives on all platforms.
                    system_is_responsive = False
                    try:
                        if platform.system() == "Darwin":
                            # macOS: os.getloadavg() is a non-blocking syscall — if it
                            # succeeds, the kernel is responsive and the system is awake.
                            os.getloadavg()
                            system_is_responsive = True
                        elif platform.system() == "Windows":
                            # Windows: GetForegroundWindow() is non-blocking
                            try:
                                import win32gui
                                hwnd = win32gui.GetForegroundWindow()
                                if hwnd:
                                    system_is_responsive = True
                            except ImportError:
                                pass
                        else:
                            # Linux: getloadavg() is always available
                            os.getloadavg()
                            system_is_responsive = True
                    except Exception as query_err:
                        logger.debug("Could not check system responsiveness: %s", query_err)
                    
                    # If system is responsive, it's definitely awake
                    if system_is_responsive:
                        logger.debug("[SCREENSHOT] System is responsive (can query frontmost app) - system is awake")
                        return False
                    
                    # FIX: If sleep was detected more than 3 minutes ago and no new sleep,
                    # assume system is awake (wake detection may have missed it)
                    # Reduced from 10 minutes to 3 minutes for faster recovery
                    if time_since_sleep > 180:  # 3 minutes
                        logger.debug("[SCREENSHOT] Sleep detected %s minutes ago - assuming awake", time_since_sleep/60)
                        return False
                    
                    # Otherwise, check if wake time is after sleep time
                    if last_wake_time is None:
                        return True
                    
                    # Convert wake time to timestamp if needed
                    if isinstance(last_wake_time, datetime.datetime):
                        wake_timestamp = last_wake_time.timestamp()
                    else:
                        wake_timestamp = last_wake_time
                    
                    if wake_timestamp < sleep_timestamp:
                        return True
            return False
        except Exception as e:
            logger.debug("Could not check system sleep state: %s", e)
            return False

    def format_ss_name(self, original_name):
        from ws_package.utils.screenshot_naming import parse_screenshot_datetime

        basename = os.path.basename(str(original_name).replace("\\", "/"))
        dt = parse_screenshot_datetime(basename)
        return f"{dt.year}/{dt.month:02d}/{dt.day:02d}"

    def _compress_screenshot_safe(self, filepath: str, max_kb = 200) -> bool:
        """
        Compress screenshot in-place to stay under max_kb.
        SAFE: if anything fails, original file remains untouched.
        Returns True if compressed (or already under limit), False otherwise.
        """
        if not HAS_PILLOW or not os.path.exists(filepath):
            return False

        try:
            original_size = os.path.getsize(filepath) / 1024
            if original_size <= max_kb:
                return True  # already OK

            img = Image.open(filepath)

            if img.mode == "RGBA":
                img = img.convert("RGB")

            # progressive compression (borrowed from ss.py)
            for quality in [70, 60, 50, 40, 30, 20, 10]:
                buffer = BytesIO()
                img.save(
                    buffer,
                    format="JPEG",
                    quality=quality,
                    optimize=True,
                    progressive=True,
                    subsampling="4:2:0",
                )

                size_kb = len(buffer.getvalue()) / 1024
                if size_kb <= max_kb:
                    with open(filepath, "wb") as f:
                        f.write(buffer.getvalue())
                    return True

            # extreme fallback: resize once
            w, h = img.size
            img = img.resize((w // 2, h // 2), Image.Resampling.LANCZOS)
            img.save(filepath, format="JPEG", quality=30, optimize=True)

            return (os.path.getsize(filepath) / 1024) <= max_kb

        except Exception:
            # ABSOLUTE fallback → do nothing
            return False
        
    def unix_format_ss_name(self, original_name):
        return os.path.basename(str(original_name).replace("\\", "/"))

    def unix_format_ss_name_on_demand(self, original_name):
        basename = os.path.basename(str(original_name).replace("\\", "/"))
        stem, ex = os.path.splitext(basename)
        return f"{stem}_on_demand{ex}"

    async def on_demand_screenshot(self, user_entity, stop_event=None):
        """Capture an on-demand screenshot immediately and upload to S3 with _on_demand suffix."""
        logger.info("[ON-DEMAND SCREENSHOT] Initiating on-demand screenshot capture")
        try:
            ss_arr = []
            with ScreenShot(
                self.save_dir,
                self.img_format,
                self.img_prefix,
                self.img_quality,
                self.blur_intensity,
                stop_event=stop_event
            ) as screenshots:
                screenshot_count = 0
                for screenshot in screenshots:
                    if stop_event and stop_event.is_set():
                        logger.warning("[ONDEMAND] Stop event triggered during capture")
                        break
                    if screenshot[0] is not None:
                        screenshot_count += 1
                        screenshot_file = screenshot[1]
                        screenshot_filename = screenshot[2]

                        logger.info("[ONDEMAND] Captured image - File: %s, Size: %s bytes",
                                    screenshot_filename, os.stat(screenshot_file).st_size)

                        try:
                            self._compress_screenshot_safe(screenshot_file, self.MAX_SS_SIZE_KB)
                        except Exception as compress_err:
                            logger.debug("Screenshot compression failed (non-fatal): %s", compress_err)
                        s3_path = (
                            f"{user_entity.org_id}/{user_entity.user_id}/{self.s3_screenshot_path}"
                            f"{self.format_ss_name(screenshot[2])}/{self.get_os_type()}/"
                            f"{self.unix_format_ss_name_on_demand(screenshot[2])}"
                        )

                        try:
                            upload_start = datetime.datetime.now()
                            response = await self.screenshot_service.sync_on_demand(screenshot[1], s3_path, user_entity)
                            upload_duration = (datetime.datetime.now() - upload_start).total_seconds()

                            logger.info("[ONDEMAND] S3 Upload completed - Duration: %ss", upload_duration)
                            s3_upload_link = response or "upload_failed"
                            is_uploaded = self.is_link(s3_upload_link)
                            if is_uploaded:
                                ss_arr.append(s3_upload_link)
                            else:
                                logger.warning("[ONDEMAND] S3 Upload Failed - Response: %s", s3_upload_link)

                        except Exception as e:
                            error_msg = str(e)
                            if "Failed to get S3 configuration details" in error_msg:
                                logger.warning("[ONDEMAND] S3 configuration not available: %s", error_msg)
                            else:
                                logger.error("[ONDEMAND] ERROR - Upload failed: %s", e, exc_info=True)

            return (True, ss_arr)
        except Exception as e:
            logger.error("[ONDEMAND] ERROR - Capture failed: %s", e, exc_info=True)
            return (False, None)

    async def start_screenshot_activity(self, user_entity, limit_start, limit_end, no_of_days, screenshot_interval, interval, blur_intensity, status, stop_event=None):
        self.blur_intensity = blur_intensity
        self.limit_start = limit_start
        self.limit_end = limit_end
        self.no_of_days = no_of_days
        self.screenshot_interval = screenshot_interval
        self.interval = interval
        # Store in self.status so the loop can read the current value dynamically.
        # ActivityController.update_interval() also updates self.status directly,
        # allowing tracking to be enabled/disabled without restarting the thread.
        self.status = bool(status)
        await self.__start_screenshot_activity(user_entity, stop_event)
    

    def ss_datetime(self):
        now = datetime.datetime.now()
        if now.minute % 10 in (4, 9):
            logger.info("Make 1 Minute back")
            return now - datetime.timedelta(minutes=1)
        return now


    def _get_interval_size_minutes(self) -> int:
        """Get interval size in minutes from interval (in seconds)"""
        return self.interval // 60
    
    def _get_screenshots_per_interval(self) -> int:
        """Determine number of screenshots per interval based on interval size and frequency"""
        interval_minutes = self._get_interval_size_minutes()
        
        # For 1 and 5 minute intervals, always 1 screenshot
        if interval_minutes in [1, 5]:
            return 1
        
        # For 10 and 15 minute intervals, use screenshot_interval (frequency: 1, 2, or 3)
        if interval_minutes in [10, 15]:
            return max(1, min(3, self.screenshot_interval))  # Clamp between 1 and 3
        
        # Default to 1 for any other interval
        return 1
    
    def _calculate_interval_boundaries(self, current_time: datetime.datetime) -> Tuple[datetime.datetime, datetime.datetime]:
        """Calculate the current interval boundaries based on interval size"""
        interval_minutes = self._get_interval_size_minutes()
        
        # Get current minute and hour
        current_minute = current_time.minute
        
        # Calculate which interval we're in based on interval size
        if interval_minutes == 1:
            # 1-minute intervals: 00:00:00-00:00:59, 00:01:00-00:01:59, etc.
            interval_start_minute = current_minute
        elif interval_minutes == 5:
            # 5-minute intervals: 00:00:00-00:04:59, 00:05:00-00:09:59, etc.
            interval_start_minute = (current_minute // 5) * 5
        elif interval_minutes == 10:
            # 10-minute intervals: 00:00:00-00:09:59, 00:10:00-00:19:59, etc.
            interval_start_minute = (current_minute // 10) * 10
        elif interval_minutes == 15:
            # 15-minute intervals: 00:00:00-00:14:59, 00:15:00-00:29:59, etc.
            interval_start_minute = (current_minute // 15) * 15
        else:
            # Fallback: use the interval as-is
            interval_start_minute = (current_minute // interval_minutes) * interval_minutes
        
        # Calculate interval start time
        interval_start = current_time.replace(
            minute=interval_start_minute,
            second=0,
            microsecond=0
        )
        
        # Calculate interval end time (59 seconds before next interval starts)
        interval_end = interval_start + datetime.timedelta(minutes=interval_minutes, seconds=-1)
        
        return interval_start, interval_end
    
    def _generate_random_capture_times(
        self, 
        interval_start: datetime.datetime, 
        interval_end: datetime.datetime, 
        count: int
    ) -> List[datetime.datetime]:
        """Generate random capture times within an interval"""
        if count <= 0:
            return []
        
        # Calculate total seconds in interval
        total_seconds = int((interval_end - interval_start).total_seconds()) + 1
        
        if total_seconds < 10:
            # If interval is too short, capture at start
            return [interval_start]
        
        capture_times = []
        
        if count == 1:
            # Single screenshot: random time in middle 80% of interval (avoid first/last 10%)
            safe_start_offset = int(total_seconds * 0.1)
            safe_end_offset = int(total_seconds * 0.9)
            random_offset = random.randint(safe_start_offset, safe_end_offset)
            capture_time = interval_start + datetime.timedelta(seconds=random_offset)
            capture_times.append(capture_time)
        else:
            # Multiple screenshots: distribute evenly with random jitter
            # Ensure minimum spacing between captures (at least 10% of interval between them)
            min_spacing = max(10, total_seconds // (count + 2))
            base_spacing = total_seconds // (count + 1)
            
            for i in range(count):
                # Calculate base position with spacing
                base_offset = (i + 1) * base_spacing
                
                # Add random jitter: ±15% of spacing, but ensure minimum spacing
                jitter_range = max(1, int(base_spacing * 0.15))
                jitter = random.randint(-jitter_range, jitter_range)
                
                capture_time = interval_start + datetime.timedelta(seconds=base_offset + jitter)
                
                # Ensure within interval bounds and maintain minimum spacing from previous
                capture_time = max(interval_start, min(capture_time, interval_end))
                
                # Ensure minimum spacing from previous capture
                if capture_times:
                    min_time = capture_times[-1] + datetime.timedelta(seconds=min_spacing)
                    capture_time = max(capture_time, min_time)
                
                capture_times.append(min(capture_time, interval_end))
        
        # Sort and return
        return sorted(capture_times)
    
    def _get_interval_key(self, interval_start: datetime.datetime) -> Tuple[int, int]:
        """Get a unique key for an interval to track if it's been processed"""
        interval_minutes = self._get_interval_size_minutes()
        return (interval_start.hour, interval_start.minute // interval_minutes * interval_minutes)
    
    def _should_capture_for_interval(self, interval_start: datetime.datetime) -> bool:
        """Check if we should capture screenshots for this interval"""
        # Check if within tracking time limits
        interval_start_time = interval_start.time()
        if not (self.limit_start <= interval_start_time <= self.limit_end):
            return False
        
        # Check if within tracking days
        if self.no_of_days is None:
            return False
        weekday = self.get_weekday(str(interval_start))
        if weekday not in self.no_of_days:
            return False
        
        # Check if screenshot_interval is enabled (guard against None from API)
        if not self.screenshot_interval or self.screenshot_interval <= 0:
            return False
        
        return True
    
    async def _capture_screenshot(self, user_entity, capture_time: datetime.datetime, stop_event=None):
        """Capture a single screenshot at the specified time"""
        actual_capture_time = datetime.datetime.now()
        interval_minutes = self._get_interval_size_minutes()
        
        if stop_event and stop_event.is_set():
            logger.warning("[SCREENSHOT] Cancelled - Stop event triggered before capture at %s", capture_time.strftime('%H:%M:%S'))
            return False
        
        # Check if system is sleeping before capturing
        if self._is_system_sleeping():
            logger.info("[SCREENSHOT] System is sleeping - skipping screenshot capture at %s", capture_time.strftime('%H:%M:%S'))
            return False
        
        logger.info(
            "[SCREENSHOT] Starting capture - Scheduled: %s, Actual: %s, Interval: %s min, Delay: %.2fs",
            capture_time.strftime('%Y-%m-%d %H:%M:%S'),
            actual_capture_time.strftime('%Y-%m-%d %H:%M:%S'),
            interval_minutes,
            (actual_capture_time - capture_time).total_seconds())
        
        try:
            # Pass ssfrequencystatus=1 because we schedule multiple capture times per interval
            # Each scheduled time should capture 1 screenshot per monitor
            # The total number of screenshots per monitor is controlled by the number of scheduled times
            with ScreenShot(
                self.save_dir,
                self.img_format,
                self.img_prefix,
                self.img_quality,
                self.blur_intensity,
                ssfrequencystatus=1,  # Always 1 per capture time since we schedule multiple times
                stop_event=stop_event
            ) as screenshots:
                screenshot_count = 0
                for screenshot in screenshots:
                    if stop_event and stop_event.is_set():
                        logger.warning("[SCREENSHOT] Stop event triggered during capture")
                        break
                    if screenshot[0] is not None:
                        screenshot_count += 1
                        screenshot_file = screenshot[1]
                        screenshot_filename = screenshot[2]
                        
                        logger.info("[SCREENSHOT] Captured image - File: %s, Size: %s bytes, Time: %s",
                                   screenshot_filename, os.stat(screenshot_file).st_size,
                                   actual_capture_time.strftime('%H:%M:%S'))
                        
                        try:
                            self._compress_screenshot_safe(screenshot_file, self.MAX_SS_SIZE_KB)
                        except Exception:
                            pass

                        s3_path = f"{user_entity.org_id}/{user_entity.user_id}/{self.s3_screenshot_path}{self.format_ss_name(screenshot[2])}/{self.get_os_type()}/{self.unix_format_ss_name(screenshot[2])}"

                        try:
                            upload_start = datetime.datetime.now()
                            response = await self.screenshot_service.sync(screenshot[1], s3_path, user_entity)
                            upload_end = datetime.datetime.now()
                            upload_duration = (upload_end - upload_start).total_seconds()
                            
                            s3_upload_link = response or "upload_failed"
                            is_uploaded = self.is_link(s3_upload_link)

                            if is_uploaded:
                                logger.info("[SCREENSHOT] S3 Upload Success - Duration: %.2fs, Link: %s...",
                                          upload_duration, s3_upload_link[:50])
                                screenshot_entity = ScreenshotEntity(
                                    file_name=screenshot[1],
                                    blur=True,
                                    s3_upload_link=s3_upload_link,
                                    file_size=str(os.stat(screenshot[1]).st_size),
                                    is_uploaded=False, 
                                    file_location=screenshot[1],
                                    is_s3_uploaded=True,
                                    counter=self.last_count + 1,
                                    timestamp=capture_time
                                )
                            else:
                                from ws_package.utils.error_codes import ErrorCode
                                logger.warning("%s — Duration: %.2fs, Response: %s",
                                               ErrorCode.SS_UPLOAD_FAILED, upload_duration, s3_upload_link)
                                self._notify_electron_error(ErrorCode.SS_UPLOAD_FAILED,
                                    "Screenshot uploaded locally but S3 sync failed. "
                                    "Check internet connection and S3 configuration. "
                                    "The screenshot is saved locally and will retry.",
                                    recoverable=True)
                                screenshot_entity = ScreenshotEntity(
                                    file_name=screenshot[1],
                                    blur=True,
                                    s3_upload_link=s3_upload_link,
                                    file_size=str(os.stat(screenshot[1]).st_size),
                                    is_uploaded=True,
                                    file_location=screenshot[1],
                                    is_s3_uploaded=False,
                                    counter=self.last_count+1,
                                    timestamp=capture_time
                                )

                            save_start = datetime.datetime.now()
                            saved_screenshot = await self.screenshot_service.save(screenshot_entity)
                            save_end = datetime.datetime.now()
                            save_duration = (save_end - save_start).total_seconds()
                            
                            if saved_screenshot is None:
                                logger.error("[SCREENSHOT] Database Save Failed - File: %s, Duration: %.2fs",
                                           screenshot_filename, save_duration)
                            else:
                                logger.info(
                                    "[SCREENSHOT] COMPLETE - Captured: %s, File: %s, Interval: %smin, Total Time: %.2fs",
                                    capture_time.strftime('%H:%M:%S'), screenshot_filename,
                                    interval_minutes, (save_end - actual_capture_time).total_seconds())

                        except Exception as e:
                            error_msg = str(e)
                            if "Failed to get S3 configuration details" in error_msg:
                                logger.warning("[SCREENSHOT] S3 configuration not available - saving locally only (org_id may not have S3 configured): %s", error_msg)
                            else:
                                logger.error("[SCREENSHOT] ERROR - Upload/Save failed at %s: %s", capture_time.strftime('%H:%M:%S'), e, exc_info=True)
                
                if screenshot_count == 0:
                    from ws_package.utils.error_codes import ErrorCode
                    logger.warning("%s — no images returned at %s (screen recording permission may be denied)",
                                   ErrorCode.SS_CAPTURE_FAILED, capture_time.strftime('%H:%M:%S'))
                    self._notify_electron_error(ErrorCode.SS_CAPTURE_FAILED,
                        "Screenshot capture returned no images. "
                        "On macOS grant Screen Recording in System Settings → Privacy & Security. "
                        "On Windows ensure Workstatus is not blocked by UAC or antivirus.",
                        recoverable=False)
                else:
                    # Notify Electron once per capture (not per screenshot) to avoid duplicate notifications
                    try:
                        from ws_package.utils.electron_comm import get_electron_comm
                        electron_comm = get_electron_comm()
                        if electron_comm and electron_comm.is_connected():
                            electron_comm.send_to_electron("screenshot_captured", {})
                    except Exception as send_err:
                        logger.debug("[SCREENSHOT] Could not notify Electron: %s", send_err)
            
            return True
        except Exception as e:
            logger.error("[SCREENSHOT] ERROR - Capture failed at %s: %s", capture_time.strftime('%Y-%m-%d %H:%M:%S'), e, exc_info=True)
            return False

    async def capture_stop_screenshot(self, user_entity, counter: int, capture_time: datetime.datetime) -> bool:
        """
        Capture a single screenshot on stop button click: capture -> upload to AWS -> save to DB.
        The screenshot is stored with the given counter and timestamp so it is included
        in the last activity interval when intervals are sent (get_screenshots by time range).
        """
        if not self.screenshot_interval or self.screenshot_interval <= 0:
            logger.info("[SCREENSHOT] Stop screenshot skipped - screenshot capture disabled (screenshot_interval=%s)", self.screenshot_interval)
            return False
        try:
            logger.info("[SCREENSHOT] Stop screenshot capture started at %s", capture_time.strftime('%Y-%m-%d %H:%M:%S'))
            with ScreenShot(
                self.save_dir,
                self.img_format,
                self.img_prefix,
                self.img_quality,
                self.blur_intensity,
                stop_event=None
            ) as screenshots:
                for screenshot in screenshots:
                    if screenshot[0] is None:
                        continue
                    screenshot_file = screenshot[1]
                    screenshot_filename = screenshot[2]
                    try:
                        self._compress_screenshot_safe(screenshot_file, self.MAX_SS_SIZE_KB)
                    except Exception:
                        pass
                    s3_path = f"{user_entity.org_id}/{user_entity.user_id}/{self.s3_screenshot_path}{self.format_ss_name(screenshot[2])}/{self.get_os_type()}/{self.unix_format_ss_name(screenshot[2])}"
                    try:
                        response = await self.screenshot_service.sync(screenshot_file, s3_path, user_entity)
                        s3_upload_link = response or "upload_failed"
                        is_uploaded = self.is_link(s3_upload_link)
                        screenshot_entity = ScreenshotEntity(
                            file_name=screenshot_file,
                            blur=True,
                            s3_upload_link=s3_upload_link,
                            file_size=str(os.stat(screenshot_file).st_size),
                            is_uploaded=not is_uploaded,
                            file_location=screenshot_file,
                            is_s3_uploaded=is_uploaded,
                            counter=counter,
                            timestamp=capture_time
                        )
                        saved = await self.screenshot_service.save(screenshot_entity)
                        if saved is not None and saved is not False:
                            logger.info("[SCREENSHOT] Stop screenshot captured, uploaded and saved - counter=%s, time=%s", counter, capture_time.strftime('%H:%M:%S'))
                            return True
                        logger.warning("[SCREENSHOT] Stop screenshot save returned None/False")
                    except Exception as e:
                        logger.error("[SCREENSHOT] Stop screenshot upload/save failed: %s", e, exc_info=True)
                    break
            logger.warning("[SCREENSHOT] Stop screenshot - no frame captured")
            return False
        except Exception as e:
            logger.error("[SCREENSHOT] Stop screenshot capture failed: %s", e, exc_info=True)
            return False

    async def save_screenshot_from_remote_url(
        self,
        user_entity,
        image_url: str,
        triggered_by: Optional[int] = None,
    ) -> bool:
        """
        Save a screenshot record from a remote image URL (e.g. from app.control.tracking
        save_screenshot event). Does not capture locally; persists the URL to DB only.
        """
        if not image_url or not image_url.strip():
            logger.warning("[SCREENSHOT] save_screenshot_from_remote_url: empty image_url")
            return False
        try:
            counter = self.last_count + 1
            screenshot_entity = ScreenshotEntity(
                file_name="remote_screenshot",
                blur=True,
                s3_upload_link=image_url.strip(),
                file_location="",
                file_size="0",
                is_uploaded=False,
                is_s3_uploaded=True,
                counter=counter,
            )
            self.screenshot_service.org_id = self.org_id
            saved = await self.screenshot_service.save(screenshot_entity)
            if saved is not None:
                logger.info("[SCREENSHOT] Saved remote screenshot to DB: url_len=%s, counter=%s, triggered_by=%s", len(image_url), counter, triggered_by)
                return True
            logger.warning("[SCREENSHOT] save_screenshot_from_remote_url: save returned None")
            return False
        except Exception as e:
            logger.error("[SCREENSHOT] save_screenshot_from_remote_url failed: %s", e, exc_info=True)
            return False

    async def __start_screenshot_activity(self, user_entity, stop_event=None):
        """Start screenshot activity with interval-based random capture times.

        Reads self.status on every loop iteration so that dynamic enable/disable
        (via ActivityController.update_interval → self.screenshot_controller.status)
        takes effect immediately without restarting the thread.
        """
        self.screenshot_service.org_id = self.org_id

        interval_minutes = self._get_interval_size_minutes()
        screenshots_per_interval = self._get_screenshots_per_interval()

        logger.info(
            "[SCREENSHOT ACTIVITY] STARTED - OS: %s, Interval: %s min, Screenshots per interval: %s, "
            "Frequency multiplier (ssfrequencystatus): %s, Tracking hours: %s-%s, Days: %s, Status: %s",
            platform.system(), interval_minutes, screenshots_per_interval, self.screenshot_interval,
            self.limit_start.strftime('%H:%M:%S'), self.limit_end.strftime('%H:%M:%S'),
            self.no_of_days, self.status)
        
        # Clear processed intervals at start of each hour to prevent memory buildup
        last_hour_clear = datetime.datetime.now().hour
        
        while not stop_event.is_set():
            try:
                current_time = datetime.datetime.now()
                
                # Clear old interval tracking every hour
                if current_time.hour != last_hour_clear:
                    self.processed_intervals.clear()
                    last_hour_clear = current_time.hour
                
                # Check if system is sleeping - skip capture during sleep
                if self._is_system_sleeping():
                    logger.info("[SCREENSHOT] System is sleeping - skipping screenshot capture and interval planning")
                    await precise_sleep(60, stop_event)  # Check every minute
                    continue
                
                # Check if we should capture screenshots.
                # Read self.status (not a frozen arg) so dynamic enable/disable takes effect.
                # Guard screenshot_interval against None (API may omit ssfrequencystatus;
                # None<=0 raises TypeError, crashing the loop every 10 s silently).
                if not self.status or not self.screenshot_interval or self.screenshot_interval <= 0:
                    logger.info(
                        "Screenshot capture disabled (self.status=%s, screenshot_interval=%s)",
                        self.status, self.screenshot_interval,
                    )
                    await precise_sleep(60, stop_event)  # Check every minute
                    continue
                
                # Calculate current interval boundaries
                interval_start, interval_end = self._calculate_interval_boundaries(current_time)
                interval_key = self._get_interval_key(interval_start)
                
                # Check if we should process this interval
                if not self._should_capture_for_interval(interval_start):
                    # Outside tracking hours/days, wait until next potential interval
                    next_check = min(60, self.interval)  # Check every minute or at interval boundary
                    await precise_sleep(next_check, stop_event)
                    continue
                
                # Check if this interval has already been processed
                if interval_key in self.processed_intervals:
                    # Interval already processed, wait until next interval
                    wait_seconds = max(1, (interval_end - current_time).total_seconds() + 1)
                    await precise_sleep(min(wait_seconds, 60), stop_event)
                    continue
                
                # Determine how many screenshots for this interval
                screenshots_per_interval = self._get_screenshots_per_interval()
                
                # Generate random capture times for this interval
                capture_times = self._generate_random_capture_times(
                    interval_start, 
                    interval_end, 
                    screenshots_per_interval
                )
                
                # Filter capture times to only include those in the future
                # This handles cases where timer starts mid-interval
                current_time_check = datetime.datetime.now()
                future_capture_times = [ct for ct in capture_times if ct > current_time_check]
                
                if not future_capture_times:
                    # All capture times have passed (started mid-interval)
                    # Generate new capture time(s) between current time and interval end
                    if current_time_check < interval_end:
                        # Calculate remaining time in interval
                        remaining_seconds = int((interval_end - current_time_check).total_seconds())
                        
                        # Minimum time needed: at least 10 seconds for a single screenshot
                        min_time_needed = 10
                        
                        if remaining_seconds >= min_time_needed:
                            # Determine how many screenshots we can fit in remaining time
                            # For multiple screenshots, need at least 30 seconds per screenshot with spacing
                            min_seconds_per_screenshot = 30
                            max_screenshots_in_remaining = max(1, remaining_seconds // min_seconds_per_screenshot)
                            
                            # Use the smaller of: requested screenshots or what we can fit
                            actual_screenshots = min(screenshots_per_interval, max_screenshots_in_remaining)
                            
                            if actual_screenshots == 1:
                                # Single screenshot: random time in middle 80% of remaining time
                                safe_start_offset = max(1, int(remaining_seconds * 0.1))
                                safe_end_offset = int(remaining_seconds * 0.9)
                                
                                if safe_end_offset > safe_start_offset:
                                    random_offset = random.randint(safe_start_offset, safe_end_offset)
                                    new_capture_time = current_time_check + datetime.timedelta(seconds=random_offset)
                                    future_capture_times = [new_capture_time]
                                    logger.info("[INTERVAL] Mid-interval start detected - Generated new capture time: "
                                              f"{new_capture_time.strftime('%H:%M:%S')} "
                                              f"(remaining: {remaining_seconds}s, offset: {random_offset}s)")
                                else:
                                    # Very little time left, capture near the end but not at the very end
                                    random_offset = max(1, remaining_seconds - 5)
                                    new_capture_time = current_time_check + datetime.timedelta(seconds=random_offset)
                                    future_capture_times = [new_capture_time]
                                    logger.info("[INTERVAL] Mid-interval start with minimal time - Generated capture time: "
                                              f"{new_capture_time.strftime('%H:%M:%S')} (remaining: {remaining_seconds}s)")
                            else:
                                # Multiple screenshots: generate times in remaining portion
                                # Use the same logic as _generate_random_capture_times but for remaining time
                                min_spacing = max(10, remaining_seconds // (actual_screenshots + 2))
                                base_spacing = remaining_seconds // (actual_screenshots + 1)
                                
                                for i in range(actual_screenshots):
                                    base_offset = (i + 1) * base_spacing
                                    jitter_range = max(1, int(base_spacing * 0.15))
                                    jitter = random.randint(-jitter_range, jitter_range)
                                    
                                    capture_time = current_time_check + datetime.timedelta(seconds=base_offset + jitter)
                                    capture_time = max(current_time_check, min(capture_time, interval_end))
                                    
                                    # Ensure minimum spacing from previous capture
                                    if future_capture_times:
                                        min_time = future_capture_times[-1] + datetime.timedelta(seconds=min_spacing)
                                        capture_time = max(capture_time, min_time)
                                    
                                    future_capture_times.append(min(capture_time, interval_end))
                                
                                # Sort to ensure chronological order
                                future_capture_times = sorted(future_capture_times)
                                logger.info(
                                    "[INTERVAL] Mid-interval start detected - Generated %s new capture time(s): %s "
                                    "(remaining: %ss, requested: %s, actual: %s)",
                                    len(future_capture_times),
                                    [t.strftime('%H:%M:%S') for t in future_capture_times],
                                    remaining_seconds, screenshots_per_interval, actual_screenshots)
                        else:
                            # Too little time left, skip this interval
                            logger.warning("[INTERVAL] SKIPPED - %s-%s insufficient time remaining (%ss < %ss)",
                                         interval_start.strftime('%H:%M:%S'), interval_end.strftime('%H:%M:%S'),
                                         remaining_seconds, min_time_needed)
                            self.processed_intervals.add(interval_key)
                            continue
                    else:
                        # Already past interval end, skip
                        logger.warning("[INTERVAL] SKIPPED - %s-%s already past interval end (current: %s)",
                                     interval_start.strftime('%H:%M:%S'), interval_end.strftime('%H:%M:%S'),
                                     current_time_check.strftime('%H:%M:%S'))
                        self.processed_intervals.add(interval_key)
                        continue
                
                logger.info(
                    "[INTERVAL] PLANNED - %s-%s (Size: %s min, Total screenshots: %s, Capture times: %s)",
                    interval_start.strftime('%H:%M:%S'), interval_end.strftime('%H:%M:%S'),
                    self._get_interval_size_minutes(), len(future_capture_times),
                    [t.strftime('%H:%M:%S') for t in future_capture_times])
                
                # Log next screenshot capture time
                if future_capture_times:
                    next_capture_time = future_capture_times[0]
                    current_time_for_log = datetime.datetime.now()
                    wait_seconds = (next_capture_time - current_time_for_log).total_seconds()
                    wait_minutes = wait_seconds / 60
                    logger.info("[SCREENSHOT] Next capture scheduled at %s (in %.1f minutes / %.0f seconds)",
                               next_capture_time.strftime('%H:%M:%S'), wait_minutes, wait_seconds)
                
                # Capture screenshots at scheduled times
                captured_count = 0
                for idx, capture_time in enumerate(future_capture_times):
                    if stop_event and stop_event.is_set():
                        logger.warning("[INTERVAL] Stop event triggered, cancelling remaining captures")
                        break
                    
                    # Wait until capture time
                    wait_until = (capture_time - datetime.datetime.now()).total_seconds()
                    if wait_until > 0:
                        logger.debug("[SCREENSHOT] Waiting %ss until %s", wait_until, capture_time.strftime('%H:%M:%S'))
                        await precise_sleep(wait_until, stop_event)
                    
                    # Check if we're still within the interval (might have passed)
                    current_check = datetime.datetime.now()
                    if current_check <= interval_end:
                        success = await self._capture_screenshot(user_entity, capture_time, stop_event)
                        if success:
                            captured_count += 1
                            
                            # Log next screenshot in this interval if there are more
                            remaining_in_interval = len(future_capture_times) - (idx + 1)
                            if remaining_in_interval > 0:
                                next_in_interval = future_capture_times[idx + 1]
                                wait_next = (next_in_interval - current_check).total_seconds()
                                logger.info("[SCREENSHOT] Next capture in this interval at %s (in %.1f minutes / %.0f seconds)",
                                           next_in_interval.strftime('%H:%M:%S'), wait_next / 60, wait_next)
                            else:
                                # Last screenshot in interval - log that we're waiting for next interval
                                next_interval_start = interval_start + datetime.timedelta(minutes=self._get_interval_size_minutes())
                                wait_until_next = (next_interval_start - current_check).total_seconds()
                                if wait_until_next > 0:
                                    logger.info(
                                        "[SCREENSHOT] Last capture in interval completed - Waiting for next interval at %s (in %.1f minutes / %.0f seconds)",
                                        next_interval_start.strftime('%H:%M:%S'), wait_until_next / 60, wait_until_next)
                        else:
                            # Capture failed - log next screenshot only if there are more and previous was successful
                            remaining_in_interval = len(future_capture_times) - (idx + 1)
                            if remaining_in_interval > 0:
                                next_in_interval = future_capture_times[idx + 1]
                                wait_next = (next_in_interval - current_check).total_seconds()
                                logger.warning("[SCREENSHOT] Capture failed - Next attempt in this interval at %s (in %.1f minutes / %.0f seconds)",
                                             next_in_interval.strftime('%H:%M:%S'), wait_next / 60, wait_next)
                    else:
                        logger.warning("[SCREENSHOT] SKIPPED - Capture time %s passed, current time: %s, interval end: %s",
                                     capture_time.strftime('%H:%M:%S'), current_check.strftime('%H:%M:%S'),
                                     interval_end.strftime('%H:%M:%S'))
                
                # Mark interval as processed
                self.processed_intervals.add(interval_key)
                
                # Enhanced completion log with failure summary
                failed_count = len(future_capture_times) - captured_count
                if captured_count == 0:
                    logger.warning("[INTERVAL] COMPLETED WITH FAILURES - %s-%s, All %s screenshot(s) failed",
                                  interval_start.strftime('%H:%M:%S'), interval_end.strftime('%H:%M:%S'),
                                  len(future_capture_times))
                elif failed_count > 0:
                    logger.warning(
                        "[INTERVAL] COMPLETED WITH PARTIAL SUCCESS - %s-%s, Successfully captured: %s/%s screenshot(s), Failed: %s",
                        interval_start.strftime('%H:%M:%S'), interval_end.strftime('%H:%M:%S'),
                        captured_count, len(future_capture_times), failed_count)
                else:
                    logger.info("[INTERVAL] COMPLETED - %s-%s, Successfully captured: %s/%s screenshot(s)",
                               interval_start.strftime('%H:%M:%S'), interval_end.strftime('%H:%M:%S'),
                               captured_count, len(future_capture_times))
                
                # Log next interval start time and wait until next interval
                next_interval_start = interval_start + datetime.timedelta(minutes=self._get_interval_size_minutes())
                current_time_after = datetime.datetime.now()
                wait_until_next_interval = (next_interval_start - current_time_after).total_seconds()
                if wait_until_next_interval > 0:
                    logger.info("[SCREENSHOT] Next interval starts at %s (in %.1f minutes / %.0f seconds)",
                               next_interval_start.strftime('%H:%M:%S'),
                               wait_until_next_interval / 60, wait_until_next_interval)
                
                # Wait until next interval or check period
                wait_until_next = (next_interval_start - datetime.datetime.now()).total_seconds()
                
                if wait_until_next > 0:
                    await precise_sleep(min(wait_until_next, 60), stop_event)
                else:
                    # Already past next interval start, check immediately
                    await precise_sleep(0.1, stop_event)
                    
            except Exception as e:
                logger.error("Error in screenshot activity loop: %s", e, exc_info=True)
                await precise_sleep(10, stop_event)  # Wait before retrying

    def is_link(self, url):
        """Check whether *url* is a valid HTTP/HTTPS URL.

        Args:
            url: String to test.

        Returns:
            True if the string looks like an http(s) URL, False otherwise.
        """
        try:
            if url is None:
                return False
            pattern = re.compile(r'^(https?://)[^\s/$.?#].[^\s]*$', re.IGNORECASE)
            return bool(pattern.match(url))
        except Exception as e:
            logger.error("Unexpected error checking URL in is_link: %s", e, exc_info=True)
            return False

    def forward_ss(self, ss):
        normalized = ss.replace("\\", "/")
        return os.path.basename(normalized)

    def extract_datetime_from_filename(self, filename):
        from ws_package.utils.screenshot_naming import parse_screenshot_datetime

        try:
            return parse_screenshot_datetime(filename)
        except ValueError:
            logger.debug("Could not extract datetime from screenshot filename: %s", filename)

    async def update_screenshots(self, screenurls: List[str], mycounter: int):
        # Handle None case
        if screenurls is None:
            logger.debug("No screenUrl provided for counter %s (expected if key missing)", mycounter)
            return
        
        # Normalize to list (API may return single string or list)
        if isinstance(screenurls, str):
            screenurls = [screenurls] if screenurls.strip() else []
        
        # Handle empty list as normal case (not an error) - some intervals have no screenshots
        if not screenurls or len(screenurls) == 0:
            logger.debug("No screenshots to update for counter %s (interval has no screenshots - normal)", mycounter)
            return
        
        # Flush DB write queue so activity_interval is_uploaded=True is committed before validation
        try:
            from ws_package.services.db_writer_service import db_writer_service
            await db_writer_service.process_immediately()
        except Exception as e:
            logger.debug("DB writer flush before screenshot update: %s", e)
        
        # Only validate if we have URLs
        if not await self.validate_screenshots(screenurls, mycounter):
            logger.error("Validation failed for screenshots (counter: %s, URLs count: %s)", mycounter, len(screenurls))
            return
        logger.info("Validation passed for screenshots")
        
        # First, get screenshot data from database (minimal lock scope)
        screenshots_to_update = []
        with db_lock_manager.read_lock():
            async with async_session() as session:
                screenshot_query = select(Screenshots).where(
                    Screenshots.s3_upload_link.in_(screenurls)
                )
                screenshots = await session.scalars(screenshot_query)
                screenshots_to_update = list(screenshots)
        
        # Perform file operations outside of database lock
        file_operations_data = []
        for screenshot in screenshots_to_update:
            screenshot.is_uploaded = True
            # Collect file operation data for later processing
            file_ops = {
                'screenshot': screenshot,
                'file_location': screenshot.file_location,
                'os_type': self.get_os_type()
            }
            file_operations_data.append(file_ops)
        
        # Now update database with minimal lock scope
        with db_lock_manager.write_lock():
            async with async_session() as session:
                for screenshot in screenshots_to_update:
                    session.add(screenshot)
                await session.commit()
        
        # Perform file operations after database commit (outside lock)
        for file_ops in file_operations_data:
            screenshot = file_ops['screenshot']
            file_location = file_ops['file_location']
            os_type = file_ops['os_type']
            
            try:
                # Use basename so we always delete by filename regardless of path format
                screenshot_filename = os.path.basename(file_location.replace("\\", os.sep))
                # Determine target_directory based on OS and frozen status
                if getattr(sys, 'frozen', False) and os_type == "Windows":
                    script_path = Path(sys.executable).resolve()
                    target_directoryw = script_path.parents[0]
                    self.delete_screenshot(screenshot_filename, f"{target_directoryw}\\screenshots")
                elif getattr(sys, 'frozen', False) and os_type == "Darwin":
                    app_support_dir = Path.home() / "Library/Application Support/Workstatus"
                    screenshots_dir = app_support_dir / "screenshots"
                    self.delete_screenshot(screenshot_filename, str(screenshots_dir))
                elif getattr(sys, 'frozen', False):
                    script_path = Path(sys.executable).resolve()
                    target_directory = script_path.parents[0]
                    target_directory_stealth = script_path.parents[1]
                    self.delete_screenshot(screenshot_filename, f"{target_directory_stealth}/screenshots")
                    self.delete_screenshot(screenshot_filename, f"{target_directory}/screenshots")
                else:
                    script_path = Path(__file__).resolve()
                    target_directory = f"{script_path.parents[3]}/workstatus"
                    self.delete_screenshot(screenshot_filename, f"{target_directory}/screenshots")
            except Exception as e:
                logger.exception("Error deleting screenshot: %s", e)
        
        logger.info("update ss is uploaded")

    def delete_screenshot(self, screenshot_name, screenshot_dir):
        """Delete a screenshot file from the local filesystem.

        Args:
            screenshot_name: Basename of the screenshot file.
            screenshot_dir: Directory containing the screenshot.

        Returns:
            True if the file was deleted, False otherwise.
        """
        logger.debug("Deleting screenshot %s from %s", screenshot_name, screenshot_dir)
        if not os.path.exists(screenshot_dir):
            logger.debug("Screenshot directory does not exist: %s", screenshot_dir)
            return False

        screenshot_path = os.path.join(screenshot_dir, screenshot_name)
        if not os.path.isfile(screenshot_path):
            logger.debug("Screenshot file not found for deletion: %s", screenshot_path)
            return False
        try:
            os.remove(screenshot_path)
            logger.debug("Deleted screenshot file: %s", screenshot_name)
            return True
        except (OSError, IOError) as e:
            logger.error("Failed to delete screenshot %s from %s: %s", screenshot_name, screenshot_dir, e, exc_info=True)
            return False

    async def validate_screenshots(
        self,
        screenurls: List[str],
        mycounter: int
    ) -> bool:
        # Empty list should be handled by caller, but if it reaches here, return False
        if not screenurls or len(screenurls) == 0:
            return False  # This should not happen if caller checks first, but handle gracefully
        
        # Use centralized read lock for database read operations
        with db_lock_manager.read_lock():
            async with async_session() as session:
                screenshot_query = select(Screenshots).where(
                    Screenshots.s3_upload_link.in_(screenurls)
                )
                screenshots = await session.scalars(screenshot_query)
                screenshot_list = list(screenshots)
                
                if not screenshot_list:
                    logger.warning("Validation failed: No screenshots found in database for URLs: %s", screenurls)
                    return False
                
                for screenshot in screenshot_list:
                    timestamp = screenshot.timestamp
                    counter = screenshot.counter
                    s3_link = screenshot.s3_upload_link
                    logger.info("Validating screenshot: %s - Timestamp: %s, Counter: %s", s3_link, timestamp, counter)
                
                    activity_query = select(ActivityInterval).where(
                        and_(
                            ActivityInterval.start_time <= timestamp,
                            ActivityInterval.end_time >= timestamp,
                            ActivityInterval.is_uploaded.is_(True)
                            # ActivityInterval.counter == counter  # Relaxed check: counter mismatch observed in valid data
                        )
                    )
                    activity_interval = await session.scalars(activity_query)
                    activity_result = activity_interval.first()
                    if not activity_result:
                        logger.error(
                            "Validation failed for screenshot %s — no matching uploaded activity interval found",
                            s3_link,
                        )
                        return False

            return True

    def get_weekday(self, date_str: str) -> str:
        weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        try:
            date_obj = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") 
        except ValueError:
            date_obj = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S.%f")
        except Exception as e:
            logger.error("Error in get_weekday: %s", e)
            return "Mon" 
        return weekdays[date_obj.weekday()]
