"""
Universal In-Memory Database Write Queue
Allows all threads to enqueue write operations without blocking on database locks.
A dedicated writer thread processes the queue and writes to the database.
"""

import threading
import time
import uuid
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, field
from enum import Enum
from ws_package.log.writer import LogWriter

logger = LogWriter.file(__name__)


class WriteOperationType(Enum):
    """Types of database write operations"""
    # INSERT operations
    ACTIVITY_INTERVAL = "activity_interval"
    SCREENSHOT = "screenshot"
    APP_MONITOR = "app_monitor"
    USER = "user"
    ORG = "org"
    S3_DETAILS = "s3_details"
    FIXED_MODE = "fixed_mode"
    ACTIVITY_PERCENTAGE = "activity_percentage"
    NOTES = "notes"
    
    # UPDATE operations
    UPDATE_ACTIVITY_INTERVAL = "update_activity_interval"
    UPDATE_SCREENSHOT = "update_screenshot"
    UPDATE_USER = "update_user"
    UPDATE_ORG = "update_org"
    UPDATE_ACTIVITY_PERCENTAGE = "update_activity_percentage"
    BULK_UPDATE = "bulk_update"


@dataclass
class WriteQueueItem:
    """Represents a single write operation in the queue"""
    operation_type: WriteOperationType
    table_name: str
    data: Dict[str, Any]  # Entity data as dictionary
    timestamp: float = field(default_factory=time.time)
    item_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    retry_count: int = 0
    max_retries: int = 3
    record_id: Optional[int] = None  # For UPDATE operations (primary key)
    in_flight: bool = False  # True while a writer is actively processing this item
    
    def __post_init__(self):
        """Validate queue item after initialization"""
        if not isinstance(self.operation_type, WriteOperationType):
            raise ValueError(f"Invalid operation type: {self.operation_type}")


class DatabaseWriteQueue:
    """
    Thread-safe in-memory queue for database write operations.
    
    All application threads enqueue write operations here instead of writing directly
    to the database. A dedicated writer thread processes the queue.
    
    Features:
    - Thread-safe operations
    - Per-operation-type queues
    - Size limits to prevent memory issues
    - Statistics tracking
    """
    
    def __init__(self, max_queue_size: int = 10000):
        """
        Initialize the write queue.
        
        Args:
            max_queue_size: Default maximum items per queue type before blocking
        """
        # Per-operation-type queues
        self._queues: Dict[WriteOperationType, List[WriteQueueItem]] = {
            op_type: [] for op_type in WriteOperationType
        }
        
        # Thread-safe locks for each queue
        self._locks: Dict[WriteOperationType, threading.Lock] = {
            op_type: threading.Lock() for op_type in WriteOperationType
        }
        
        # Global lock for statistics
        self._stats_lock = threading.Lock()
        
        # Statistics
        self._stats = {
            'total_enqueued': 0,
            'total_processed': 0,
            'total_failed': 0,
            'queue_sizes': {op_type.value: 0 for op_type in WriteOperationType}
        }
        
        # FIXED: Per-operation-type queue limits to prevent memory pressure
        # Screenshots can have large file paths, so use lower limit
        self._max_queue_sizes: Dict[WriteOperationType, int] = {
            WriteOperationType.SCREENSHOT: 1000,  # Lower limit for screenshots (file paths)
            WriteOperationType.UPDATE_SCREENSHOT: 1000,  # Lower limit for screenshot updates
            # Other operations can use default limit
        }
        self._default_max_size = max_queue_size
        
        logger.debug("DatabaseWriteQueue initialized with max_size=%s", max_queue_size)
    
    def enqueue(
        self,
        operation_type: WriteOperationType,
        table_name: str,
        data: Dict[str, Any],
        max_retries: int = 3,
        record_id: Optional[int] = None
    ) -> bool:
        """
        Enqueue a write operation to the in-memory queue.
        
        This is a non-blocking operation that returns immediately.
        The actual database write happens asynchronously by the writer thread.
        
        Args:
            operation_type: Type of write operation
            table_name: Target database table name
            data: Entity data as dictionary (from model_dump() or similar)
            max_retries: Maximum retry attempts if write fails
            
        Returns:
            True if enqueued successfully, False if queue is full
            
        Example:
            >>> queue.enqueue(
            ...     WriteOperationType.ACTIVITY_INTERVAL,
            ...     "activity_interval",
            ...     activity_entity.model_dump()
            ... )
        """
        try:
            queue_item = WriteQueueItem(
                operation_type=operation_type,
                table_name=table_name,
                data=data,
                max_retries=max_retries,
                record_id=record_id
            )
            
            with self._locks[operation_type]:
                queue = self._queues[operation_type]
                
                # FIXED: Use per-operation-type queue limits
                max_size = self._max_queue_sizes.get(operation_type, self._default_max_size)
                
                # Check queue size limit
                if len(queue) >= max_size:
                    logger.warning(
                        f"Queue full for {operation_type.value}. "
                        f"Size: {len(queue)}, Max: {max_size}"
                    )
                    return False
                
                queue.append(queue_item)
                
                # Update statistics
                with self._stats_lock:
                    self._stats['total_enqueued'] += 1
                    self._stats['queue_sizes'][operation_type.value] = len(queue)
            
            logger.debug(
                f"Enqueued {operation_type.value} write operation. "
                f"Queue size: {len(self._queues[operation_type])}"
            )
            return True
            
        except Exception as e:
            logger.error("Error enqueueing write operation: %s", e, exc_info=True)
            return False
    
    def get_pending_items(
        self,
        operation_type: WriteOperationType,
        limit: int = 50
    ) -> List[WriteQueueItem]:
        """
        Get pending items from a specific queue.
        
        This is used by the writer thread to get items for processing.
        Items are NOT removed from the queue - use remove_processed() after successful write.
        
        Args:
            operation_type: Type of operation to get items for
            limit: Maximum number of items to return
            
        Returns:
            List of WriteQueueItem objects
        """
        with self._locks[operation_type]:
            queue = self._queues[operation_type]
            return queue[:limit].copy()  # Return copy to avoid modification

    def claim_pending_items(
        self,
        operation_type: WriteOperationType,
        limit: int = 50
    ) -> List[WriteQueueItem]:
        """
        Atomically claim pending items for processing.

        Unlike get_pending_items(), this marks returned items as in-flight so
        concurrent writer passes cannot process the same queue item twice.
        Claimed items must be finalized with remove_processed() or mark_failed().
        """
        claimed_items: List[WriteQueueItem] = []

        with self._locks[operation_type]:
            queue = self._queues[operation_type]
            for item in queue:
                if item.in_flight:
                    continue
                item.in_flight = True
                claimed_items.append(item)
                if len(claimed_items) >= limit:
                    break

        return claimed_items.copy()
    
    def remove_processed(
        self,
        operation_type: WriteOperationType,
        items: List[WriteQueueItem]
    ) -> int:
        """
        Remove successfully processed items from the queue.
        
        Args:
            operation_type: Type of operation
            items: List of items that were successfully written
            
        Returns:
            Number of items removed
        """
        if not items:
            return 0
        
        removed_count = 0
        with self._locks[operation_type]:
            queue = self._queues[operation_type]

            # Create set of item_ids for fast, collision-free lookup
            processed_ids = {item.item_id for item in items}

            # Remove items that match processed item_ids
            original_size = len(queue)
            queue[:] = [
                item for item in queue
                if item.item_id not in processed_ids
            ]
            removed_count = original_size - len(queue)
            
            # Update statistics
            with self._stats_lock:
                self._stats['total_processed'] += removed_count
                self._stats['queue_sizes'][operation_type.value] = len(queue)
        
        if removed_count > 0:
            logger.debug(
                f"Removed {removed_count} processed items from {operation_type.value} queue"
            )
        
        return removed_count
    
    def mark_failed(
        self,
        operation_type: WriteOperationType,
        items: List[WriteQueueItem]
    ) -> int:
        """
        Mark items as failed and increment retry count.
        Items exceeding max_retries are removed.
        
        Args:
            operation_type: Type of operation
            items: List of items that failed to write
            
        Returns:
            Number of items removed (exceeded max retries)
        """
        if not items:
            return 0
        
        removed_count = 0
        with self._locks[operation_type]:
            queue = self._queues[operation_type]
            
            # Create lookup by item_id for collision-free identification
            failed_ids = {item.item_id for item in items}

            # Two-pass approach: collect expired items first, then remove after iteration
            # (mutating a list while iterating it skips elements — silent data loss).
            expired = []
            for item in queue:
                if item.item_id in failed_ids:
                    item.retry_count += 1
                    if item.retry_count >= item.max_retries:
                        expired.append(item)
                    else:
                        item.in_flight = False

            for item in expired:
                queue.remove(item)
                removed_count += 1
                with self._stats_lock:
                    self._stats['total_failed'] += 1
                logger.warning(
                    "Removed %s item after %d failed attempts",
                    operation_type.value, item.retry_count,
                )
        
        return removed_count
    
    def get_queue_size(self, operation_type: WriteOperationType) -> int:
        """Get current queue size for a specific operation type"""
        with self._locks[operation_type]:
            return len(self._queues[operation_type])
    
    def get_total_queue_size(self) -> int:
        """Get total size across all queues"""
        total = 0
        for op_type in WriteOperationType:
            total += self.get_queue_size(op_type)
        return total
    
    def get_stats(self) -> Dict[str, Any]:
        """Get queue statistics"""
        with self._stats_lock:
            # Update current queue sizes
            for op_type in WriteOperationType:
                self._stats['queue_sizes'][op_type.value] = self.get_queue_size(op_type)
            
            return self._stats.copy()
    
    def clear_queue(self, operation_type: WriteOperationType) -> int:
        """
        Clear all items from a specific queue.
        Use with caution - only for emergency cleanup.
        
        Returns:
            Number of items cleared
        """
        with self._locks[operation_type]:
            queue = self._queues[operation_type]
            count = len(queue)
            queue.clear()
            
            with self._stats_lock:
                self._stats['queue_sizes'][operation_type.value] = 0
            
            logger.warning("Cleared %s items from %s queue", count, operation_type.value)
            return count


# Global singleton instance
db_write_queue = DatabaseWriteQueue()

