"""
Queue processor v2 - uses encrypted user databases instead of service.db
Supports both direct execution and queue modes.
"""

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

from loguru import logger

from ...database.encrypted_db import db_manager
from ...database.models import (
    QueuedResearch,
    ResearchHistory,
    UserActiveResearch,
)
from ...database.queue_service import UserQueueService
from ...database.session_context import get_user_db_session
from ...database.session_passwords import session_password_store
from ...notifications.queue_helpers import (
    send_research_completed_notification_from_session,
    send_research_failed_notification_from_session,
)
from ..routes.globals import active_research, termination_flags
from ..services.research_service import (
    run_research_process,
    start_research_process,
)

# Retry configuration constants for notification database queries
MAX_RESEARCH_LOOKUP_RETRIES = 3
INITIAL_RESEARCH_LOOKUP_DELAY = 0.5  # seconds
RETRY_BACKOFF_MULTIPLIER = 2


class QueueProcessorV2:
    """
    Processes queued researches using encrypted user databases.
    This replaces the service.db approach.
    """

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

        Args:
            check_interval: How often to check for work (seconds)
        """
        self.check_interval = check_interval
        self.running = False
        self.thread = None

        # Per-user settings will be retrieved from each user's database
        # when processing their queue using SettingsManager
        logger.info(
            "Queue processor v2 initialized - will use per-user settings from SettingsManager"
        )

        # Track which users we should check
        self._users_to_check: Set[str] = set()
        self._users_lock = threading.Lock()

        # Track pending operations from background threads
        self.pending_operations = {}
        self._pending_operations_lock = threading.Lock()

    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 v2 started")

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

    def notify_user_activity(self, username: str, session_id: str):
        """
        Notify that a user has activity and their queue should be checked.

        Args:
            username: The username
            session_id: The Flask session ID (for password access)
        """
        with self._users_lock:
            self._users_to_check.add(f"{username}:{session_id}")
            logger.debug(f"User {username} added to queue check list")

    def notify_research_queued(self, username: str, research_id: str, **kwargs):
        """
        Notify that a research was queued.
        In direct mode, this immediately starts the research if slots are available.
        In queue mode, it adds to the queue.

        Args:
            username: The username
            research_id: The research ID
            **kwargs: Additional parameters for direct execution (query, mode, etc.)
        """
        # Check user's queue_mode setting when we have database access
        if kwargs:
            session_id = kwargs.get("session_id")
            if session_id:
                # Check if we can start it directly
                password = session_password_store.get_session_password(
                    username, session_id
                )
                if password:
                    try:
                        # Open database and check settings + active count
                        engine = db_manager.open_user_database(
                            username, password
                        )
                        if engine:
                            with get_user_db_session(username) as db_session:
                                # Get user's settings using SettingsManager
                                from ...settings.manager import SettingsManager

                                settings_manager = SettingsManager(db_session)

                                # Get user's queue_mode setting (env > DB > default)
                                queue_mode = settings_manager.get_setting(
                                    "app.queue_mode", "direct"
                                )

                                # Get user's max concurrent setting (env > DB > default)
                                max_concurrent = settings_manager.get_setting(
                                    "app.max_concurrent_researches", 3
                                )

                                logger.debug(
                                    f"User {username} settings: queue_mode={queue_mode}, "
                                    f"max_concurrent={max_concurrent}"
                                )

                                # Only try direct execution if user has queue_mode="direct"
                                if queue_mode == "direct":
                                    # Count active researches
                                    active_count = (
                                        db_session.query(UserActiveResearch)
                                        .filter_by(
                                            username=username,
                                            status="in_progress",
                                        )
                                        .count()
                                    )

                                    if active_count < max_concurrent:
                                        # We have slots - start directly!
                                        logger.info(
                                            f"Direct mode: Starting research {research_id} immediately "
                                            f"(active: {active_count}/{max_concurrent})"
                                        )

                                        # Start the research directly
                                        self._start_research_directly(
                                            username,
                                            research_id,
                                            password,
                                            **kwargs,
                                        )
                                        return
                                    else:
                                        logger.info(
                                            f"Direct mode: Max concurrent reached ({active_count}/"
                                            f"{max_concurrent}), queueing {research_id}"
                                        )
                                else:
                                    logger.info(
                                        f"User {username} has queue_mode={queue_mode}, "
                                        f"queueing research {research_id}"
                                    )
                    except Exception:
                        logger.exception(
                            f"Error in direct execution for {username}"
                        )

        # Fall back to queue mode (or if direct mode failed)
        try:
            with get_user_db_session(username) as session:
                queue_service = UserQueueService(session)
                queue_service.add_task_metadata(
                    task_id=research_id,
                    task_type="research",
                    priority=0,
                )
                logger.info(
                    f"Research {research_id} queued for user {username}"
                )
        except Exception:
            logger.exception(f"Failed to update queue status for {username}")

    def _start_research_directly(
        self, username: str, research_id: str, password: str, **kwargs
    ):
        """
        Start a research directly without queueing.

        Args:
            username: The username
            research_id: The research ID
            password: The user's password
            **kwargs: Research parameters (query, mode, settings, etc.)
        """
        query = kwargs.get("query")
        mode = kwargs.get("mode")
        settings_snapshot = kwargs.get("settings_snapshot", {})

        # Create active research record
        try:
            with get_user_db_session(username) as db_session:
                active_record = UserActiveResearch(
                    username=username,
                    research_id=research_id,
                    status="in_progress",
                    thread_id="pending",
                    settings_snapshot=settings_snapshot,
                )
                db_session.add(active_record)
                db_session.commit()

                # Update task status if it exists
                queue_service = UserQueueService(db_session)
                queue_service.update_task_status(research_id, "processing")
        except Exception:
            logger.exception(
                f"Failed to create active research record for {research_id}"
            )
            return

        # Extract parameters from kwargs
        model_provider = kwargs.get("model_provider")
        model = kwargs.get("model")
        custom_endpoint = kwargs.get("custom_endpoint")
        search_engine = kwargs.get("search_engine")

        # Start the research process
        try:
            research_thread = start_research_process(
                research_id,
                query,
                mode,
                active_research,
                termination_flags,
                run_research_process,
                username=username,
                user_password=password,
                model_provider=model_provider,
                model=model,
                custom_endpoint=custom_endpoint,
                search_engine=search_engine,
                max_results=kwargs.get("max_results"),
                time_period=kwargs.get("time_period"),
                iterations=kwargs.get("iterations"),
                questions_per_iteration=kwargs.get("questions_per_iteration"),
                strategy=kwargs.get("strategy", "source-based"),
                settings_snapshot=settings_snapshot,
            )

            # Update thread ID
            try:
                with get_user_db_session(username) as db_session:
                    active_record = (
                        db_session.query(UserActiveResearch)
                        .filter_by(username=username, research_id=research_id)
                        .first()
                    )
                    if active_record:
                        active_record.thread_id = str(research_thread.ident)
                        db_session.commit()
            except Exception:
                logger.exception(
                    f"Failed to update thread ID for {research_id}"
                )

            logger.info(
                f"Direct execution: Started research {research_id} for user {username} "
                f"in thread {research_thread.ident}"
            )

        except Exception:
            logger.exception(f"Failed to start research {research_id} directly")
            # Clean up the active record
            try:
                with get_user_db_session(username) as db_session:
                    active_record = (
                        db_session.query(UserActiveResearch)
                        .filter_by(username=username, research_id=research_id)
                        .first()
                    )
                    if active_record:
                        db_session.delete(active_record)
                        db_session.commit()
            except Exception:
                pass

    def notify_research_completed(
        self, username: str, research_id: str, user_password: str = None
    ):
        """
        Notify that a research completed.
        Updates the user's queue status in their database.

        Args:
            username: The username
            research_id: The research ID
            user_password: User password for database access. Required for queue
                          updates and database lookups during notification sending.
                          Optional only because some callers may not have it
                          available, in which case only basic updates occur.
        """
        try:
            # get_user_db_session is already imported at module level (line 19)
            # It accepts optional password parameter and returns a context manager
            with get_user_db_session(username, user_password) as session:
                queue_service = UserQueueService(session)
                queue_service.update_task_status(research_id, "completed")
                logger.info(
                    f"Research {research_id} completed for user {username}"
                )

                # Send notification using helper from notification module
                send_research_completed_notification_from_session(
                    username=username,
                    research_id=research_id,
                    db_session=session,
                )

        except Exception:
            logger.exception(
                f"Failed to update completion status for {username}"
            )

    def notify_research_failed(
        self,
        username: str,
        research_id: str,
        error_message: str = None,
        user_password: str = None,
    ):
        """
        Notify that a research failed.
        Updates the user's queue status in their database and sends notification.

        Args:
            username: The username
            research_id: The research ID
            error_message: Optional error message
            user_password: User password for database access. Required for queue
                          updates and database lookups during notification sending.
                          Optional only because some callers may not have it
                          available, in which case only basic updates occur.
        """
        try:
            # get_user_db_session is already imported at module level (line 19)
            # It accepts optional password parameter and returns a context manager
            with get_user_db_session(username, user_password) as session:
                queue_service = UserQueueService(session)
                queue_service.update_task_status(
                    research_id, "failed", error_message=error_message
                )
                logger.info(
                    f"Research {research_id} failed for user {username}: "
                    f"{error_message}"
                )

                # Send notification using helper from notification module
                send_research_failed_notification_from_session(
                    username=username,
                    research_id=research_id,
                    error_message=error_message or "Unknown error",
                    db_session=session,
                )

        except Exception:
            logger.exception(f"Failed to update failure status for {username}")

    def _process_queue_loop(self):
        """Main loop that processes the queue."""
        while self.running:
            try:
                # Get list of users to check (don't clear immediately)
                with self._users_lock:
                    users_to_check = list(self._users_to_check)

                # Process each user's queue
                users_to_remove = []
                for user_session in users_to_check:
                    try:
                        username, session_id = user_session.split(":", 1)
                        # _process_user_queue returns True if queue is empty
                        queue_empty = self._process_user_queue(
                            username, session_id
                        )
                        if queue_empty:
                            users_to_remove.append(user_session)
                    except Exception:
                        logger.exception(
                            f"Error processing queue for {user_session}"
                        )
                        # Don't remove on error - the _process_user_queue method
                        # determines whether to keep checking based on error type

                # Only remove users whose queues are now empty
                with self._users_lock:
                    for user_session in users_to_remove:
                        self._users_to_check.discard(user_session)

            except Exception:
                logger.exception("Error in queue processor loop")

            time.sleep(self.check_interval)

    def _process_user_queue(self, username: str, session_id: str) -> bool:
        """
        Process the queue for a specific user.

        Args:
            username: The username
            session_id: The Flask session ID

        Returns:
            True if the queue is empty, False if there are still items
        """
        # Get the user's password from session store
        password = session_password_store.get_session_password(
            username, session_id
        )
        if not password:
            logger.debug(
                f"No password available for user {username}, skipping queue check"
            )
            return True  # Remove from checking - session expired

        # Open the user's encrypted database
        try:
            # First ensure the database is open
            engine = db_manager.open_user_database(username, password)
            if not engine:
                logger.error(f"Failed to open database for user {username}")
                return False  # Keep checking - could be temporary DB issue

            # Get a session and process the queue
            with get_user_db_session(username, password) as db_session:
                queue_service = UserQueueService(db_session)

                # Get user's settings using SettingsManager
                from ...settings.manager import SettingsManager

                settings_manager = SettingsManager(db_session)

                # Get user's max concurrent setting (env > DB > default)
                max_concurrent = settings_manager.get_setting(
                    "app.max_concurrent_researches", 3
                )

                # Get queue status
                queue_status = queue_service.get_queue_status() or {
                    "active_tasks": 0,
                    "queued_tasks": 0,
                }

                # Calculate available slots
                available_slots = max_concurrent - queue_status["active_tasks"]

                if available_slots <= 0:
                    # No slots available, but queue might not be empty
                    return False  # Keep checking

                if queue_status["queued_tasks"] == 0:
                    # Queue is empty
                    return True  # Remove from checking

                logger.info(
                    f"Processing queue for {username}: "
                    f"{queue_status['active_tasks']} active, "
                    f"{queue_status['queued_tasks']} queued, "
                    f"{available_slots} slots available"
                )

                # Process queued researches
                self._start_queued_researches(
                    db_session,
                    queue_service,
                    username,
                    password,
                    available_slots,
                )

                # Check if there are still items in queue
                updated_status = queue_service.get_queue_status() or {
                    "queued_tasks": 0
                }
                return updated_status["queued_tasks"] == 0

        except Exception:
            logger.exception(f"Error processing queue for user {username}")
            return False  # Keep checking - errors might be temporary

    def _start_queued_researches(
        self,
        db_session,
        queue_service: UserQueueService,
        username: str,
        password: str,
        available_slots: int,
    ):
        """Start queued researches up to available slots."""
        # Get queued researches
        queued = (
            db_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
                queued_research.is_processing = True
                db_session.commit()

                # Update task status
                queue_service.update_task_status(
                    queued_research.research_id, "processing"
                )

                # Start the research
                self._start_research(
                    db_session,
                    username,
                    password,
                    queued_research,
                )

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

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

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

                # Update task status
                queue_service.update_task_status(
                    queued_research.research_id,
                    "failed",
                    error_message="Failed to start research",
                )

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

        if not research:
            raise ValueError(
                f"Research {queued_research.research_id} not found"
            )

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

        # Create active research record
        active_record = UserActiveResearch(
            username=username,
            research_id=queued_research.research_id,
            status="in_progress",
            thread_id="pending",
            settings_snapshot=queued_research.settings_snapshot,
        )
        db_session.add(active_record)
        db_session.commit()

        # Extract settings
        settings_snapshot = queued_research.settings_snapshot or {}

        # Handle new vs legacy structure
        if (
            isinstance(settings_snapshot, dict)
            and "submission" in settings_snapshot
        ):
            submission_params = settings_snapshot.get("submission", {})
            complete_settings = settings_snapshot.get("settings_snapshot", {})
        else:
            submission_params = settings_snapshot
            complete_settings = {}

        # Start the research process with password
        research_thread = start_research_process(
            queued_research.research_id,
            queued_research.query,
            queued_research.mode,
            active_research,
            termination_flags,
            run_research_process,
            username=username,
            user_password=password,  # Pass password for metrics
            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,
        )

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

    def process_user_request(self, username: str, session_id: str) -> int:
        """
        Process queue for a user during their request.
        This is called from request context to check and start queued items.

        Returns:
            Number of researches started
        """
        try:
            # Add user to check list
            self.notify_user_activity(username, session_id)

            # Force immediate check (don't wait for loop)
            password = session_password_store.get_session_password(
                username, session_id
            )
            if password:
                # Open database and check queue
                engine = db_manager.open_user_database(username, password)
                if engine:
                    with get_user_db_session(username) as db_session:
                        queue_service = UserQueueService(db_session)
                        status = queue_service.get_queue_status()

                        if status and status["queued_tasks"] > 0:
                            logger.info(
                                f"User {username} has {status['queued_tasks']} "
                                f"queued tasks, triggering immediate processing"
                            )
                            # Process will happen in background thread
                            return status["queued_tasks"]

            return 0

        except Exception:
            logger.exception(f"Error in process_user_request for {username}")
            return 0

    def queue_progress_update(
        self, username: str, research_id: str, progress: float
    ):
        """
        Queue a progress update that needs database access.
        For compatibility with old processor during migration.

        Args:
            username: The username
            research_id: The research ID
            progress: The progress value (0-100)
        """
        # In processor_v2, we can update directly if we have database access
        # or queue it for later processing
        operation_id = str(uuid.uuid4())
        with self._pending_operations_lock:
            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_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.
        For compatibility with old processor during migration.

        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())
        with self._pending_operations_lock:
            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 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.
        For compatibility with old processor during migration.

        Args:
            username: Username to process operations for
            db_session: Active database session for the user

        Returns:
            Number of operations processed
        """
        # Find pending operations for this user (with lock)
        operations_to_process = []
        with self._pending_operations_lock:
            for op_id, op_data in list(self.pending_operations.items()):
                if op_data["username"] == username:
                    operations_to_process.append((op_id, op_data))
                    # Remove immediately to prevent duplicate processing
                    del self.pending_operations[op_id]

        if not operations_to_process:
            return 0

        processed_count = 0

        # Process operations outside the lock (to avoid holding lock during DB operations)
        for op_id, op_data in operations_to_process:
            try:
                operation_type = op_data.get("operation_type")

                if operation_type == "progress_update":
                    # Update progress in database
                    from ...database.models import ResearchHistory

                    research = (
                        db_session.query(ResearchHistory)
                        .filter_by(id=op_data["research_id"])
                        .first()
                    )
                    if research:
                        # Update the progress column directly
                        research.progress = op_data["progress"]
                        db_session.commit()
                        processed_count += 1

                elif operation_type == "error_update":
                    # Update error status in database
                    from ...database.models import ResearchHistory

                    research = (
                        db_session.query(ResearchHistory)
                        .filter_by(id=op_data["research_id"])
                        .first()
                    )
                    if research:
                        research.status = op_data["status"]
                        research.error_message = op_data["error_message"]
                        research.research_meta = op_data["metadata"]
                        research.completed_at = op_data["completed_at"]
                        if op_data.get("report_path"):
                            research.report_path = op_data["report_path"]
                        db_session.commit()
                        processed_count += 1

            except Exception:
                logger.exception(f"Error processing operation {op_id}")
                # Rollback to clear the failed transaction state
                try:
                    db_session.rollback()
                except Exception:
                    logger.warning(
                        f"Failed to rollback after error in operation {op_id}"
                    )

        return processed_count


# Global queue processor instance
queue_processor = QueueProcessorV2()
