"""Service for managing the research queue"""

import threading
import time
import uuid
from typing import Any, Dict, Optional

from loguru import logger

from ...database.models import (
    QueuedResearch,
    ResearchHistory,
    UserActiveResearch,
)
from ...database.models.memory_queue import get_memory_queue_tracker
from ..routes.globals import active_research, termination_flags
from ..services.research_service import (
    run_research_process,
    start_research_process,
)


class QueueProcessor:
    """Processes queued researches when slots become available"""

    def __init__(self, check_interval=5):
        """
        Initialize the queue processor

        Args:
            check_interval: How often to check for available slots (seconds)
        """
        self.check_interval = check_interval
        self.running = False
        self.thread = None
        self.max_concurrent_per_user = 3
        self.pending_operations = {}  # Store operations that need DB access

    def start(self):
        """Start the queue processor thread"""
        if self.running:
            logger.warning("Queue processor already running")
            return

        self.running = True
        self.thread = threading.Thread(
            target=self._process_queue_loop, daemon=True
        )
        self.thread.start()
        logger.info("Queue processor started")

    def stop(self):
        """Stop the queue processor thread"""
        self.running = False
        if self.thread:
            self.thread.join(timeout=10)
        logger.info("Queue processor stopped")

    def _process_queue_loop(self):
        """Main loop that processes the queue"""
        while self.running:
            try:
                self._check_and_process_queue()
            except Exception:
                logger.exception("Error in queue processor loop")

            time.sleep(self.check_interval)

    def _check_and_process_queue(self):
        """Check for available slots and process queued researches"""
        # Get memory tracker
        memory_tracker = get_memory_queue_tracker()

        # Get all users with queued tasks from memory tracker
        users = set()
        with memory_tracker.get_session() as session:
            from ...database.models.memory_queue import InMemoryTaskMetadata

            usernames = (
                session.query(InMemoryTaskMetadata.username)
                .filter(InMemoryTaskMetadata.status == "queued")
                .distinct()
                .all()
            )
            users = [(username,) for (username,) in usernames]

        for (username,) in users:
            try:
                self._process_user_queue(username)
            except Exception:
                logger.exception(f"Error processing queue for user {username}")

    def _process_user_queue(self, username: str):
        """Process the queue for a specific user using memory tracker"""
        # Get memory tracker
        memory_tracker = get_memory_queue_tracker()

        # Get queue status from memory tracker
        queue_status = memory_tracker.get_queue_status(username) or {
            "active_tasks": 0,
            "queued_tasks": 0,
        }

        # Calculate available slots
        available_slots = (
            self.max_concurrent_per_user - queue_status["active_tasks"]
        )

        if available_slots <= 0:
            return

        # Get pending tasks from memory tracker
        pending_tasks = memory_tracker.get_user_tasks(
            username, status="queued"
        )[:available_slots]

        # For tasks requiring DB access, we need to use a different approach
        if queue_status["queued_tasks"] > len(pending_tasks):
            # There are tasks that require DB access
            # These will be processed when the user makes a request (event-driven)
            self._register_pending_db_operation(username, available_slots)

    def _register_pending_db_operation(
        self, username: str, available_slots: int
    ):
        """Register that we need to access user's encrypted database"""
        operation_id = str(uuid.uuid4())
        self.pending_operations[operation_id] = {
            "username": username,
            "available_slots": available_slots,
            "timestamp": time.time(),
        }

        # This will be processed when user makes a request
        logger.debug(
            f"Registered pending operation {operation_id} for {username}"
        )

    def process_pending_operations_for_user(
        self, username: str, db_session
    ) -> int:
        """
        Process pending operations for a user when we have database access.
        Called from request context where encrypted database is accessible.

        Returns number of researches started.
        """
        started_count = 0

        # Find pending operations for this user
        operations_to_process = []
        for op_id, op_data in list(self.pending_operations.items()):
            if op_data["username"] == username:
                operations_to_process.append((op_id, op_data))

        if not operations_to_process:
            return 0

        for op_id, op_data in operations_to_process:
            try:
                operation_type = op_data.get("operation_type", "queue_process")

                if operation_type == "queue_process":
                    # Process queued researches with database access
                    count = self._process_queued_researches_with_db(
                        db_session, username, op_data["available_slots"]
                    )
                    started_count += count

                elif operation_type == "cleanup":
                    # Process cleanup operation
                    self._process_cleanup_operation(db_session, op_data)

                elif operation_type == "error_update":
                    # Process error update
                    self._process_error_update(db_session, op_data)

                elif operation_type == "progress_update":
                    # Process progress update
                    self._process_progress_update(db_session, op_data)

                elif operation_type == "token_metrics":
                    # Process token metrics
                    self._process_token_metrics(db_session, op_data)

                # Remove processed operation
                del self.pending_operations[op_id]

            except Exception:
                logger.exception(f"Error processing operation {op_id}")

        return started_count

    def _process_queued_researches_with_db(
        self, session, username: str, available_slots: int
    ) -> int:
        """Process queued researches when we have database access"""
        started_count = 0

        try:
            # Get queued researches ordered by position
            queued = (
                session.query(QueuedResearch)
                .filter_by(username=username, is_processing=False)
                .order_by(QueuedResearch.position)
                .limit(available_slots)
                .all()
            )

            for queued_research in queued:
                try:
                    # Mark as processing to avoid race conditions
                    queued_research.is_processing = True
                    session.commit()

                    # Start the research
                    self._start_queued_research(
                        session, username, queued_research
                    )

                    # Remove from queue
                    session.delete(queued_research)
                    session.commit()

                    # Update memory tracker
                    memory_tracker = get_memory_queue_tracker()
                    memory_tracker.update_task_status(
                        queued_research.research_id, "processing"
                    )

                    logger.info(
                        f"Started queued research {queued_research.research_id} for user {username}"
                    )
                    started_count += 1

                except Exception:
                    logger.exception(
                        f"Error starting queued research {queued_research.research_id}"
                    )
                    # Reset processing flag on error
                    queued_research.is_processing = False
                    session.commit()

                    # Update memory tracker
                    memory_tracker = get_memory_queue_tracker()
                    memory_tracker.update_task_status(
                        queued_research.research_id,
                        "failed",
                    )

        except Exception:
            logger.exception(
                f"Error accessing encrypted database for {username}"
            )

        return started_count

    def _start_queued_research(self, session, username, queued_research):
        """Start a queued research"""
        # Update research status
        research = (
            session.query(ResearchHistory)
            .filter_by(id=queued_research.research_id)
            .first()
        )

        if not research:
            logger.error(
                f"Research {queued_research.research_id} not found in history"
            )
            return

        research.status = "in_progress"
        session.commit()

        # Create active research record
        active_record = UserActiveResearch(
            username=username,
            research_id=queued_research.research_id,
            status="in_progress",
            thread_id="pending",  # Will be updated when thread starts
            settings_snapshot=queued_research.settings_snapshot,
        )
        session.add(active_record)
        session.commit()

        # Extract settings
        settings_snapshot = queued_research.settings_snapshot or {}

        # Extract submission parameters from the new structure
        submission_params = {}
        if isinstance(settings_snapshot, dict):
            # Check if it's the new structure with 'submission' key
            if "submission" in settings_snapshot:
                submission_params = settings_snapshot.get("submission", {})
                complete_settings = settings_snapshot.get(
                    "settings_snapshot", {}
                )
            else:
                # Legacy structure - use the whole object as submission params
                submission_params = settings_snapshot
                complete_settings = {}

        # Start the research process
        research_thread = start_research_process(
            queued_research.research_id,
            queued_research.query,
            queued_research.mode,
            active_research,
            termination_flags,
            run_research_process,
            username=username,
            model_provider=submission_params.get("model_provider"),
            model=submission_params.get("model"),
            custom_endpoint=submission_params.get("custom_endpoint"),
            search_engine=submission_params.get("search_engine"),
            max_results=submission_params.get("max_results"),
            time_period=submission_params.get("time_period"),
            iterations=submission_params.get("iterations"),
            questions_per_iteration=submission_params.get(
                "questions_per_iteration"
            ),
            strategy=submission_params.get("strategy", "source-based"),
            settings_snapshot=complete_settings,  # Pass complete settings
        )

        # Update thread ID
        active_record.thread_id = str(research_thread.ident)
        session.commit()

        logger.info(
            f"Research {queued_research.research_id} started from queue"
        )

    def notify_research_queued(self, username: str, research_id: str):
        """
        Notify the queue processor that a new research was queued.
        Updates the memory tracker.
        """
        memory_tracker = get_memory_queue_tracker()
        memory_tracker.add_task(
            task_id=research_id,
            username=username,
            task_type="research",
        )

    def notify_research_completed(self, username: str, research_id: str):
        """
        Notify the queue processor that a research completed.
        Updates the memory tracker.
        """
        memory_tracker = get_memory_queue_tracker()
        memory_tracker.update_task_status(research_id, "completed")

    def queue_cleanup_operation(
        self, username: str, research_id: str, operation_type: str = "cleanup"
    ):
        """
        Queue a cleanup operation that needs database access.
        This will be processed when the user makes their next request.

        Args:
            username: The username
            research_id: The research ID
            operation_type: Type of operation (cleanup, error_update, etc.)
        """
        operation_id = str(uuid.uuid4())
        self.pending_operations[operation_id] = {
            "username": username,
            "operation_type": operation_type,
            "research_id": research_id,
            "timestamp": time.time(),
        }
        logger.info(
            f"Queued {operation_type} operation for research {research_id}"
        )

    def queue_error_update(
        self,
        username: str,
        research_id: str,
        status: str,
        error_message: str,
        metadata: Dict[str, Any],
        completed_at: str,
        report_path: Optional[str] = None,
    ):
        """
        Queue an error status update that needs database access.

        Args:
            username: The username
            research_id: The research ID
            status: The status to set (failed, suspended, etc.)
            error_message: The error message
            metadata: Research metadata
            completed_at: Completion timestamp
            report_path: Optional path to error report
        """
        operation_id = str(uuid.uuid4())
        self.pending_operations[operation_id] = {
            "username": username,
            "operation_type": "error_update",
            "research_id": research_id,
            "status": status,
            "error_message": error_message,
            "metadata": metadata,
            "completed_at": completed_at,
            "report_path": report_path,
            "timestamp": time.time(),
        }
        logger.info(
            f"Queued error update for research {research_id} with status {status}"
        )

    def queue_progress_update(
        self, username: str, research_id: str, progress: float
    ):
        """
        Queue a progress update that needs database access.

        Args:
            username: The username
            research_id: The research ID
            progress: The progress value (0-100)
        """
        operation_id = str(uuid.uuid4())
        self.pending_operations[operation_id] = {
            "username": username,
            "operation_type": "progress_update",
            "research_id": research_id,
            "progress": progress,
            "timestamp": time.time(),
        }
        logger.debug(
            f"Queued progress update for research {research_id}: {progress}%"
        )

    def queue_token_metrics(
        self,
        username: str,
        research_id: Optional[int],
        token_data: Dict[str, Any],
    ):
        """
        Queue token metrics that need database access.

        Args:
            username: The username
            research_id: The research ID (optional)
            token_data: Dictionary containing all token metric data
        """
        operation_id = str(uuid.uuid4())
        self.pending_operations[operation_id] = {
            "username": username,
            "operation_type": "token_metrics",
            "research_id": research_id,
            "token_data": token_data,
            "timestamp": time.time(),
        }
        logger.debug(
            f"Queued token metrics for research {research_id} - "
            f"tokens: {token_data.get('prompt_tokens', 0)} prompt, "
            f"{token_data.get('completion_tokens', 0)} completion"
        )

    def _process_cleanup_operation(self, db_session, op_data: Dict[str, Any]):
        """
        Process a cleanup operation with database access.

        Args:
            db_session: The database session
            op_data: The operation data
        """
        research_id = op_data["research_id"]
        username = op_data["username"]

        try:
            # Get the current status from the database
            research = (
                db_session.query(ResearchHistory)
                .filter(ResearchHistory.id == research_id)
                .first()
            )

            if not research:
                logger.error(
                    f"Research with ID {research_id} not found during cleanup"
                )
                return

            # Clean up UserActiveResearch record
            from ...database.models import UserActiveResearch

            active_record = (
                db_session.query(UserActiveResearch)
                .filter_by(username=username, research_id=research_id)
                .first()
            )

            if active_record:
                logger.info(
                    f"Cleaning up active research {research_id} for user {username} "
                    f"(was started at {active_record.started_at})"
                )
                db_session.delete(active_record)
                db_session.commit()
                logger.info(
                    f"Cleaned up active research record for user {username}"
                )
            else:
                logger.warning(
                    f"No active research record found to clean up for {research_id} / {username}"
                )

            # Notify service database that research completed
            self.notify_research_completed(username, research_id)

        except Exception:
            logger.exception(
                f"Error processing cleanup operation for research {research_id}"
            )
            db_session.rollback()

    def _process_error_update(self, db_session, op_data: Dict[str, Any]):
        """
        Process an error update operation with database access.

        Args:
            db_session: The database session
            op_data: The operation data
        """
        research_id = op_data["research_id"]

        try:
            research = (
                db_session.query(ResearchHistory)
                .filter_by(id=research_id)
                .first()
            )

            if not research:
                logger.error(
                    f"Research with ID {research_id} not found for error update"
                )
                return

            # Calculate duration
            from ..models.database import calculate_duration

            duration_seconds = calculate_duration(research.created_at)

            # Update the ResearchHistory object
            research.status = op_data["status"]
            research.completed_at = op_data["completed_at"]
            research.duration_seconds = duration_seconds
            research.research_meta = op_data["metadata"]

            # Add error report path if available
            if op_data.get("report_path"):
                research.report_path = op_data["report_path"]

            db_session.commit()

            logger.info(
                f"Updated research {research_id} with status '{op_data['status']}' "
                f"and error: {op_data['error_message']}"
            )

            # Update memory tracker
            memory_tracker = get_memory_queue_tracker()
            memory_tracker.update_task_status(
                research_id,
                op_data["status"],
            )

        except Exception:
            logger.exception(
                f"Error processing error update for research {research_id}"
            )
            db_session.rollback()

    def _process_progress_update(self, db_session, op_data: Dict[str, Any]):
        """
        Process a progress update operation with database access.

        Args:
            db_session: The database session
            op_data: The operation data
        """
        research_id = op_data["research_id"]
        progress = op_data["progress"]

        try:
            research = (
                db_session.query(ResearchHistory)
                .filter_by(id=research_id)
                .first()
            )

            if not research:
                logger.error(
                    f"Research with ID {research_id} not found for progress update"
                )
                return

            # Update progress
            research.progress = progress
            db_session.commit()

            logger.debug(
                f"Updated research {research_id} progress to {progress}%"
            )

        except Exception:
            logger.exception(
                f"Error processing progress update for research {research_id}"
            )
            db_session.rollback()

    def _process_token_metrics(self, db_session, op_data: Dict[str, Any]):
        """
        Process token metrics operation with database access.

        Args:
            db_session: The database session
            op_data: The operation data containing token metrics
        """
        research_id = op_data.get("research_id")
        token_data = op_data.get("token_data", {})

        try:
            from ...database.models import ModelUsage, TokenUsage

            # Extract token data
            prompt_tokens = token_data.get("prompt_tokens", 0)
            completion_tokens = token_data.get("completion_tokens", 0)
            total_tokens = prompt_tokens + completion_tokens

            # Create TokenUsage record
            token_usage = TokenUsage(
                research_id=research_id,
                model_name=token_data.get("model_name"),
                provider=token_data.get("provider"),
                prompt_tokens=prompt_tokens,
                completion_tokens=completion_tokens,
                total_tokens=total_tokens,
                # Research context
                research_query=token_data.get("research_query"),
                research_mode=token_data.get("research_mode"),
                research_phase=token_data.get("research_phase"),
                search_iteration=token_data.get("search_iteration"),
                # Performance metrics
                response_time_ms=token_data.get("response_time_ms"),
                success_status=token_data.get("success_status", "success"),
                error_type=token_data.get("error_type"),
                # Search engine context
                search_engines_planned=token_data.get("search_engines_planned"),
                search_engine_selected=token_data.get("search_engine_selected"),
                # Call stack tracking
                calling_file=token_data.get("calling_file"),
                calling_function=token_data.get("calling_function"),
                call_stack=token_data.get("call_stack"),
            )
            db_session.add(token_usage)

            # Update or create ModelUsage statistics
            if research_id and token_data.get("model_name"):
                model_usage = (
                    db_session.query(ModelUsage)
                    .filter_by(
                        research_id=research_id,
                        model_name=token_data.get("model_name"),
                    )
                    .first()
                )

                if model_usage:
                    model_usage.prompt_tokens += prompt_tokens
                    model_usage.completion_tokens += completion_tokens
                    model_usage.total_tokens += total_tokens
                    model_usage.calls += 1
                else:
                    model_usage = ModelUsage(
                        research_id=research_id,
                        model_name=token_data.get("model_name"),
                        provider=token_data.get("provider"),
                        prompt_tokens=prompt_tokens,
                        completion_tokens=completion_tokens,
                        total_tokens=total_tokens,
                        calls=1,
                    )
                    db_session.add(model_usage)

            db_session.commit()

            logger.info(
                f"Saved token metrics for research {research_id}: "
                f"{prompt_tokens} prompt, {completion_tokens} completion tokens"
            )

        except Exception:
            logger.exception(
                f"Error processing token metrics for research {research_id}"
            )
            db_session.rollback()


# Global queue processor instance
queue_processor = QueueProcessor()
