# ws_package/services/activity_percentage_service.py

import datetime
import threading
from typing import Optional, Tuple, Dict
from ws_package.entities.activity_entity import ActivityPercentageEntity
from ws_package.repositories.activity_percentage_repo import activity_percentage_manager
from ws_package.utils.db_write_queue import db_write_queue, WriteOperationType
from ws_package.log.writer import LogWriter

logger = LogWriter.file(__name__)


class ActivityPercentageService:
    def __init__(self):
        # In-memory cache: stores latest calculated activity percentage per slot
        # Key: (start_time, stop_time) tuple, Value: ActivityPercentageEntity
        self._slot_cache: Dict[tuple, ActivityPercentageEntity] = {}
        self._cache_lock = threading.Lock()
    
    async def calculate_and_store_rolling_average(self, keyboard_percentage: float, mouse_percentage: float, average_percentage: float, interval_minutes: int = 10) -> Optional[ActivityPercentageEntity]:
        """
        Calculate rolling average and store in database.
        
        Args:
            keyboard_percentage: Current minute's keyboard activity percentage
            mouse_percentage: Current minute's mouse activity percentage  
            average_percentage: Current minute's average activity percentage
            interval_minutes: Interval duration in minutes (5, 10, or 15). Defaults to 10.
            
        Returns:
            ActivityPercentageEntity: The stored record with rolling averages
        """
        try:
            # Use a single write lock for the entire operation to prevent deadlocks
            # This ensures atomic read-then-write without lock contention
            latest_record = await activity_percentage_manager.get_latest_activity_percentage()
            current_time = datetime.datetime.now()
            
            if latest_record is None:
                # First record - no previous data to average with
                # Align start time to slot boundary for consistency with new slot logic
                minute = current_time.minute
                rounded_minute = (minute // interval_minutes) * interval_minutes
                record_start = current_time.replace(minute=rounded_minute, second=0, microsecond=0)
                
                rolling_keyboard = keyboard_percentage
                rolling_mouse = mouse_percentage
                rolling_average = average_percentage
                minute_count = 1
                
                # Log first record with both slot boundary and actual start time
                if record_start != current_time:
                    logger.info("First activity percentage record created - "
                              f"Slot start (aligned to {interval_minutes}-min boundary): {record_start}, "
                              f"Actual tracking started: {current_time}, "
                              f"Keyboard: {rolling_keyboard:.2f}%, "
                              f"Mouse: {rolling_mouse:.2f}%, "
                              f"Average: {rolling_average:.2f}%, "
                              f"Minute: {minute_count}/{interval_minutes}")
                else:
                    logger.info("First activity percentage record created - "
                              f"Slot start: {record_start}, "
                              f"Keyboard: {rolling_keyboard:.2f}%, "
                              f"Mouse: {rolling_mouse:.2f}%, "
                              f"Average: {rolling_average:.2f}%, "
                              f"Minute: {minute_count}/{interval_minutes}")
            else:
                # Check if we're still in the same interval slot
                # Calculate expected slot end time based on latest record's start time
                expected_slot_end = latest_record.start + datetime.timedelta(minutes=interval_minutes)
                
                # Check if current time is still within the same interval slot
                if current_time < expected_slot_end:
                    # Still in same slot - increment minute but cap at interval duration
                    minute_count = min(latest_record.minute + 1, interval_minutes)
                    record_start = latest_record.start
                    
                    # Calculate rolling average with previous data
                    # Rolling average formula: (previous_total + current_value) / total_minutes
                    # Special case: If minute=0 (initial zero record), just use current values (no averaging needed)
                    if latest_record.minute == 0:
                        # Initial zero record - first actual data, so just use current values
                        rolling_keyboard = keyboard_percentage
                        rolling_mouse = mouse_percentage
                        rolling_average = min(keyboard_percentage + mouse_percentage, 100.0)
                    else:
                        # Normal rolling average calculation
                        rolling_keyboard = ((latest_record.keyboard * latest_record.minute) + keyboard_percentage) / minute_count
                        rolling_mouse = ((latest_record.mouse * latest_record.minute) + mouse_percentage) / minute_count
                        # Calculate rolling average as sum of rolling keyboard and mouse, capped at 100%
                        # Note: "average" field contains sum, not actual average
                        rolling_average = min(rolling_keyboard + rolling_mouse, 100.0)
                    
                    logger.info("Rolling average calculation (same slot): "
                              f"Keyboard: {rolling_keyboard:.2f}% (was {latest_record.keyboard:.2f}%), "
                              f"Mouse: {rolling_mouse:.2f}% (was {latest_record.mouse:.2f}%), "
                              f"Average: {rolling_average:.2f}% (was {latest_record.average:.2f}%), "
                              f"Minute: {minute_count}/{interval_minutes}")
                else:
                    # New slot started - reset to 1 and start fresh
                    # Align start time to slot boundary (same logic as getCurrentSlot)
                    minute = current_time.minute
                    rounded_minute = (minute // interval_minutes) * interval_minutes
                    record_start = current_time.replace(minute=rounded_minute, second=0, microsecond=0)
                    
                    minute_count = 1
                    rolling_keyboard = keyboard_percentage
                    rolling_mouse = mouse_percentage
                    # Calculate average as sum of keyboard and mouse, capped at 100%
                    # Note: "average" field contains sum, not actual average
                    rolling_average = min(keyboard_percentage + mouse_percentage, 100.0)
                    
                    # Log new slot with both slot boundary and actual detection time
                    if record_start != current_time:
                        logger.info("New interval slot started - resetting rolling average: "
                                  f"Slot start (aligned to {interval_minutes}-min boundary): {record_start}, "
                                  f"Detected at: {current_time}, "
                                  f"Keyboard: {rolling_keyboard:.2f}%, "
                                  f"Mouse: {rolling_mouse:.2f}%, "
                                  f"Average: {rolling_average:.2f}%, "
                                  f"Minute: {minute_count}/{interval_minutes}")
                    else:
                        logger.info("New interval slot started - resetting rolling average: "
                                  f"Slot start: {record_start}, "
                                  f"Keyboard: {rolling_keyboard:.2f}%, "
                                  f"Mouse: {rolling_mouse:.2f}%, "
                                  f"Average: {rolling_average:.2f}%, "
                                  f"Minute: {minute_count}/{interval_minutes}")
            
            # Calculate slot end time to ensure the record covers the full interval
            # This is critical for the overlap query to work correctly when retrieving activity data
            slot_end_time = record_start + datetime.timedelta(minutes=interval_minutes)
            
            # Create new activity percentage entity
            activity_percentage_entity = ActivityPercentageEntity(
                start=record_start,
                stop=slot_end_time,  # Set stop to slot end time to cover full interval for overlap queries
                keyboard=round(rolling_keyboard, 2),
                mouse=round(rolling_mouse, 2),
                average=round(rolling_average, 2),
                minute=minute_count
            )
            
            # Update in-memory cache with latest calculated values
            # This ensures we have the data even if database write hasn't completed yet
            with self._cache_lock:
                cache_key = (record_start, slot_end_time)
                self._slot_cache[cache_key] = activity_percentage_entity
                # Keep only last 5 slots in cache to prevent memory growth
                if len(self._slot_cache) > 5:
                    # Remove oldest entry (first key)
                    oldest_key = next(iter(self._slot_cache))
                    del self._slot_cache[oldest_key]
            
            # Use universal queue - UPDATE_ACTIVITY_PERCENTAGE will update latest record
            # If no record exists, it will create a new one (handled by db_writer_service)
            # Note: enqueue is synchronous (thread-safe), not async
            success = db_write_queue.enqueue(
                operation_type=WriteOperationType.UPDATE_ACTIVITY_PERCENTAGE,
                table_name="activity_percentage",
                data=activity_percentage_entity.model_dump(),
                record_id=None  # None means update latest record
            )
            
            if success:
                logger.info("Enqueued rolling average for processing: "
                          f"Keyboard: {activity_percentage_entity.keyboard}%, "
                          f"Mouse: {activity_percentage_entity.mouse}%, "
                          f"Average: {activity_percentage_entity.average}%, "
                          f"Minute: {activity_percentage_entity.minute}")
                return activity_percentage_entity
            else:
                logger.error("Failed to enqueue activity percentage record - queue may be full")
                return None
                
        except Exception as e:
            logger.error("Error calculating rolling average: %s", e)
            return None

    async def get_current_rolling_averages(self) -> Optional[Tuple[float, float, float, int]]:
        """
        Get the current rolling averages from the latest record.
        
        Returns:
            Tuple of (keyboard_avg, mouse_avg, average_avg, minute_count) or None
        """
        try:
            latest_record = await activity_percentage_manager.get_latest_activity_percentage()
            
            if latest_record:
                return (
                    latest_record.keyboard,
                    latest_record.mouse, 
                    latest_record.average,
                    latest_record.minute
                )
            else:
                return None
                
        except Exception as e:
            logger.error("Error getting current rolling averages: %s", e)
            return None

    async def reset_rolling_averages(self) -> bool:
        """
        Reset rolling averages by clearing all records.
        Use this if you want to start fresh.
        
        Returns:
            bool: True if successful, False otherwise
        """
        try:
            # Note: This would require implementing a delete method in the repository
            # For now, we'll just log that this functionality would be here
            logger.info("Reset rolling averages functionality would be implemented here")
            return True
        except Exception as e:
            logger.error("Error resetting rolling averages: %s", e)
            return False

    async def get_activity_percentage_history(self, hours: int = 24) -> list:
        """
        Get activity percentage history for the last N hours.
        
        Args:
            hours: Number of hours to look back
            
        Returns:
            List of ActivityPercentageEntity records
        """
        try:
            end_time = datetime.datetime.now()
            start_time = end_time - datetime.timedelta(hours=hours)
            
            records = await activity_percentage_manager.get_activity_percentage_by_date_range(
                start_date=start_time,
                end_date=end_time
            )
            
            return records
            
        except Exception as e:
            logger.error("Error getting activity percentage history: %s", e)
            return []

    def get_cached_activity_percentage(self, start_time: datetime.datetime, end_time: datetime.datetime) -> Optional[ActivityPercentageEntity]:
        """
        Get cached activity percentage if it overlaps with the requested time range.
        Used as fallback when database query returns None.
        
        Args:
            start_time: Start of the requested time range
            end_time: End of the requested time range
            
        Returns:
            ActivityPercentageEntity if found in cache, None otherwise
        """
        with self._cache_lock:
            for (cache_start, cache_stop), cached_entity in self._slot_cache.items():
                # Check if cached record overlaps with requested range
                # Overlap condition: start < end_time AND stop > start_time
                if cache_start < end_time and cache_stop > start_time:
                    logger.debug(
                        "Found cached activity percentage for range %s to %s: Keyboard: %s%%, Mouse: %s%%, Average: %s%%",
                        start_time, end_time,
                        cached_entity.keyboard, cached_entity.mouse, cached_entity.average)
                    return cached_entity
            return None


activity_percentage_service = ActivityPercentageService()
