import hashlib
import threading
from datetime import datetime
from pathlib import Path

from loguru import logger

from ...config.llm_config import get_llm
from ...config.search_config import get_search
from ...metrics.search_tracker import set_search_context
from ...report_generator import IntegratedReportGenerator
from ...search_system import AdvancedSearchSystem
from ...utilities.log_utils import log_for_research
from ...utilities.search_utilities import extract_links_from_search_results
from ...utilities.db_utils import get_db_session
from ...utilities.threading_utils import thread_context, thread_with_app_context
from ..database.models import ResearchStrategy, ResearchHistory
from ..models.database import calculate_duration
from .socket_service import SocketIOService

# Output directory for research results
_PROJECT_ROOT = Path(__file__).parents[4]
OUTPUT_DIR = _PROJECT_ROOT / "research_outputs"


def save_research_strategy(research_id, strategy_name):
    """
    Save the strategy used for a research to the database.

    Args:
        research_id: The ID of the research
        strategy_name: The name of the strategy used
    """
    try:
        logger.debug(
            f"save_research_strategy called with research_id={research_id}, strategy_name={strategy_name}"
        )
        session = get_db_session()

        try:
            # Check if a strategy already exists for this research
            existing_strategy = (
                session.query(ResearchStrategy)
                .filter_by(research_id=research_id)
                .first()
            )

            if existing_strategy:
                # Update existing strategy
                existing_strategy.strategy_name = strategy_name
                logger.debug(
                    f"Updating existing strategy for research {research_id}"
                )
            else:
                # Create new strategy record
                new_strategy = ResearchStrategy(
                    research_id=research_id, strategy_name=strategy_name
                )
                session.add(new_strategy)
                logger.debug(
                    f"Creating new strategy record for research {research_id}"
                )

            session.commit()
            logger.info(
                f"Saved strategy '{strategy_name}' for research {research_id}"
            )
        finally:
            session.close()
    except Exception:
        logger.exception("Error saving research strategy")


def get_research_strategy(research_id):
    """
    Get the strategy used for a research.

    Args:
        research_id: The ID of the research

    Returns:
        str: The strategy name or None if not found
    """
    try:
        session = get_db_session()

        try:
            strategy = (
                session.query(ResearchStrategy)
                .filter_by(research_id=research_id)
                .first()
            )

            return strategy.strategy_name if strategy else None
        finally:
            session.close()
    except Exception:
        logger.exception("Error getting research strategy")
        return None


def start_research_process(
    research_id,
    query,
    mode,
    active_research,
    termination_flags,
    run_research_callback,
    **kwargs,
):
    """
    Start a research process in a background thread.

    Args:
        research_id: The ID of the research
        query: The research query
        mode: The research mode (quick/detailed)
        active_research: Dictionary of active research processes
        termination_flags: Dictionary of termination flags
        run_research_callback: The callback function to run the research
        **kwargs: Additional parameters to pass to the research process (model, search_engine, etc.)

    Returns:
        threading.Thread: The thread running the research
    """
    # Pass the app context to the thread.
    run_research_callback = thread_with_app_context(run_research_callback)

    # Start research process in a background thread
    thread = threading.Thread(
        target=run_research_callback,
        args=(
            thread_context(),
            research_id,
            query,
            mode,
            active_research,
            termination_flags,
        ),
        kwargs=kwargs,
    )
    thread.daemon = True
    thread.start()

    active_research[research_id] = {
        "thread": thread,
        "progress": 0,
        "status": "in_progress",
        "log": [],
        "settings": kwargs,  # Store settings for reference
    }

    return thread


def _generate_report_path(query: str) -> Path:
    """
    Generates a path for a new report file based on the query.

    Args:
        query: The query used for the report.

    Returns:
        The path that it generated.

    """
    # Generate a unique filename that does not contain
    # non-alphanumeric characters.
    query_hash = hashlib.md5(query.encode("utf-8")).hexdigest()[:10]
    return OUTPUT_DIR / (
        f"research_report_{query_hash}_{int(datetime.now().timestamp())}.md"
    )


@log_for_research
def run_research_process(
    research_id, query, mode, active_research, termination_flags, **kwargs
):
    """
    Run the research process in the background for a given research ID.

    Args:
        research_id: The ID of the research
        query: The research query
        mode: The research mode (quick/detailed)
        active_research: Dictionary of active research processes
        termination_flags: Dictionary of termination flags
        **kwargs: Additional parameters for the research (model_provider, model, search_engine, etc.)
    """
    try:
        # Check if this research has been terminated before we even start
        if research_id in termination_flags and termination_flags[research_id]:
            logger.info(
                f"Research {research_id} was terminated before starting"
            )
            cleanup_research_resources(
                research_id, active_research, termination_flags
            )
            return

        logger.info(
            "Starting research process for ID %s, query: %s", research_id, query
        )

        # Extract key parameters
        model_provider = kwargs.get("model_provider")
        model = kwargs.get("model")
        custom_endpoint = kwargs.get("custom_endpoint")
        search_engine = kwargs.get("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"
        )  # Default to source-based

        # Save the strategy to the database
        logger.debug(
            f"About to call save_research_strategy with research_id={research_id}, strategy={strategy}"
        )
        save_research_strategy(research_id, strategy)
        logger.debug("save_research_strategy call completed")

        # Log all parameters for debugging
        logger.info(
            "Research parameters: provider=%s, model=%s, search_engine=%s, "
            "max_results=%s, time_period=%s, iterations=%s, "
            "questions_per_iteration=%s, custom_endpoint=%s, strategy=%s",
            model_provider,
            model,
            search_engine,
            max_results,
            time_period,
            iterations,
            questions_per_iteration,
            custom_endpoint,
            strategy,
        )

        # Set up the AI Context Manager
        output_dir = OUTPUT_DIR / f"research_{research_id}"
        output_dir.mkdir(parents=True, exist_ok=True)

        # Create shared research context that can be updated during research
        shared_research_context = {
            "research_id": research_id,
            "research_query": query,
            "research_mode": mode,
            "research_phase": "init",
            "search_iteration": 0,
            "search_engines_planned": None,
            "search_engine_selected": search_engine,
        }

        # Set search context for search tracking
        set_search_context(shared_research_context)

        # Set up progress callback
        def progress_callback(message, progress_percent, metadata):
            # Frequent termination check
            if (
                research_id in termination_flags
                and termination_flags[research_id]
            ):
                handle_termination(
                    research_id, active_research, termination_flags
                )
                raise Exception("Research was terminated by user")

            logger.log("milestone", message)

            if "SEARCH_PLAN:" in message:
                engines = message.split("SEARCH_PLAN:")[1].strip()
                metadata["planned_engines"] = engines
                metadata["phase"] = "search_planning"  # Use existing phase
                # Update shared context for token tracking
                shared_research_context["search_engines_planned"] = engines
                shared_research_context["research_phase"] = "search_planning"

            if "ENGINE_SELECTED:" in message:
                engine = message.split("ENGINE_SELECTED:")[1].strip()
                metadata["selected_engine"] = engine
                metadata["phase"] = "search"  # Use existing 'search' phase
                # Update shared context for token tracking
                shared_research_context["search_engine_selected"] = engine
                shared_research_context["research_phase"] = "search"

            # Capture other research phases for better context tracking
            if metadata.get("phase"):
                shared_research_context["research_phase"] = metadata["phase"]

            # Update search iteration if available
            if "iteration" in metadata:
                shared_research_context["search_iteration"] = metadata[
                    "iteration"
                ]

            # Adjust progress based on research mode
            adjusted_progress = progress_percent
            if (
                mode == "detailed"
                and metadata.get("phase") == "output_generation"
            ):
                # For detailed mode, adjust the progress range for output generation
                adjusted_progress = min(80, progress_percent)
            elif (
                mode == "detailed"
                and metadata.get("phase") == "report_generation"
            ):
                # Scale the progress from 80% to 95% for the report generation phase
                if progress_percent is not None:
                    normalized = progress_percent / 100
                    adjusted_progress = 80 + (normalized * 15)
            elif (
                mode == "quick" and metadata.get("phase") == "output_generation"
            ):
                # For quick mode, ensure we're at least at 85% during output generation
                adjusted_progress = max(85, progress_percent)
                # Map any further progress within output_generation to 85-95% range
                if progress_percent is not None and progress_percent > 0:
                    normalized = progress_percent / 100
                    adjusted_progress = 85 + (normalized * 10)

            # Don't let progress go backwards
            if research_id in active_research and adjusted_progress is not None:
                current_progress = active_research[research_id].get(
                    "progress", 0
                )
                adjusted_progress = max(current_progress, adjusted_progress)

            # Update active research record
            if research_id in active_research:
                if adjusted_progress is not None:
                    active_research[research_id]["progress"] = adjusted_progress

                # Update progress in the research_history table (for backward compatibility)
                db_session = get_db_session()

                # Update the progress and log separately to avoid race conditions
                with db_session:
                    if adjusted_progress is not None:
                        research = (
                            db_session.query(ResearchHistory)
                            .filter(ResearchHistory.id == research_id)
                            .first()
                        )
                        if research:
                            research.progress = adjusted_progress
                            db_session.commit()

                # Emit a socket event
                try:
                    # Basic event data
                    event_data = {"progress": adjusted_progress}

                    SocketIOService().emit_to_subscribers(
                        "progress", research_id, event_data
                    )
                except Exception:
                    logger.exception("Socket emit error (non-critical)")

        # Function to check termination during long-running operations
        def check_termination():
            if (
                research_id in termination_flags
                and termination_flags[research_id]
            ):
                handle_termination(
                    research_id, active_research, termination_flags
                )
                raise Exception(
                    "Research was terminated by user during long-running operation"
                )
            return False  # Not terminated

        # Configure the system with the specified parameters
        use_llm = None
        if model or search_engine or model_provider:
            # Log that we're overriding system settings
            logger.info(
                f"Overriding system settings with: provider={model_provider}, model={model}, search_engine={search_engine}"
            )

        # Override LLM if model or model_provider specified
        if model or model_provider:
            try:
                # Phase 1 Enhancement: Build research context for token tracking
                research_context = {
                    "research_query": query,
                    "research_mode": mode,
                    "research_phase": "init",
                    "search_iteration": 0,
                }

                # Get LLM with the overridden settings
                # Explicitly create the model with parameters to avoid fallback issues
                use_llm = get_llm(
                    model_name=model,
                    provider=model_provider,
                    openai_endpoint_url=custom_endpoint,
                    research_id=research_id,
                    research_context=research_context,
                )

                logger.info(
                    "Successfully set LLM to: provider=%s, model=%s",
                    model_provider,
                    model,
                )
            except Exception:
                logger.exception(
                    "Error setting LLM provider=%s, model=%s",
                    model_provider,
                    model,
                )

        # Set the progress callback in the system
        system = AdvancedSearchSystem(llm=use_llm, strategy_name=strategy)
        system.set_progress_callback(progress_callback)

        # Override search engine if specified
        if search_engine:
            try:
                if iterations:
                    system.max_iterations = int(iterations)
                if questions_per_iteration:
                    system.questions_per_iteration = int(
                        questions_per_iteration
                    )

                # Create a new search object with these settings
                system.search = get_search(
                    search_tool=search_engine, llm_instance=system.model
                )

                logger.info(
                    "Successfully set search engine to: %s", search_engine
                )
            except Exception:
                logger.exception(
                    "Error setting search engine to %s", search_engine
                )

        # Run the search
        progress_callback("Starting research process", 5, {"phase": "init"})

        try:
            results = system.analyze_topic(query)
            if mode == "quick":
                progress_callback(
                    "Search complete, preparing to generate summary...",
                    85,
                    {"phase": "output_generation"},
                )
            else:
                progress_callback(
                    "Search complete, generating output",
                    80,
                    {"phase": "output_generation"},
                )
        except Exception as search_error:
            # Better handling of specific search errors
            error_message = str(search_error)
            error_type = "unknown"

            # Extract error details for common issues
            if "status code: 503" in error_message:
                error_message = "Ollama AI service is unavailable (HTTP 503). Please check that Ollama is running properly on your system."
                error_type = "ollama_unavailable"
            elif "status code: 404" in error_message:
                error_message = "Ollama model not found (HTTP 404). Please check that you have pulled the required model."
                error_type = "model_not_found"
            elif "status code:" in error_message:
                # Extract the status code for other HTTP errors
                status_code = error_message.split("status code:")[1].strip()
                error_message = f"API request failed with status code {status_code}. Please check your configuration."
                error_type = "api_error"
            elif "connection" in error_message.lower():
                error_message = "Connection error. Please check that your LLM service (Ollama/API) is running and accessible."
                error_type = "connection_error"

            # Raise with improved error message
            raise Exception(f"{error_message} (Error type: {error_type})")

        # Generate output based on mode
        if mode == "quick":
            # Quick Summary
            if results.get("findings") or results.get("formatted_findings"):
                raw_formatted_findings = results["formatted_findings"]

                # Check if formatted_findings contains an error message
                if isinstance(
                    raw_formatted_findings, str
                ) and raw_formatted_findings.startswith("Error:"):
                    logger.exception(
                        f"Detected error in formatted findings: {raw_formatted_findings[:100]}..."
                    )

                    # Determine error type for better user feedback
                    error_type = "unknown"
                    error_message = raw_formatted_findings.lower()

                    if (
                        "token limit" in error_message
                        or "context length" in error_message
                    ):
                        error_type = "token_limit"
                        # Log specific error type
                        logger.warning(
                            "Detected token limit error in synthesis"
                        )

                        # Update progress with specific error type
                        progress_callback(
                            "Synthesis hit token limits. Attempting fallback...",
                            87,
                            {
                                "phase": "synthesis_error",
                                "error_type": error_type,
                            },
                        )
                    elif (
                        "timeout" in error_message
                        or "timed out" in error_message
                    ):
                        error_type = "timeout"
                        logger.warning("Detected timeout error in synthesis")
                        progress_callback(
                            "Synthesis timed out. Attempting fallback...",
                            87,
                            {
                                "phase": "synthesis_error",
                                "error_type": error_type,
                            },
                        )
                    elif "rate limit" in error_message:
                        error_type = "rate_limit"
                        logger.warning("Detected rate limit error in synthesis")
                        progress_callback(
                            "LLM rate limit reached. Attempting fallback...",
                            87,
                            {
                                "phase": "synthesis_error",
                                "error_type": error_type,
                            },
                        )
                    elif (
                        "connection" in error_message
                        or "network" in error_message
                    ):
                        error_type = "connection"
                        logger.warning("Detected connection error in synthesis")
                        progress_callback(
                            "Connection issue with LLM. Attempting fallback...",
                            87,
                            {
                                "phase": "synthesis_error",
                                "error_type": error_type,
                            },
                        )
                    elif (
                        "llm error" in error_message
                        or "final answer synthesis fail" in error_message
                    ):
                        error_type = "llm_error"
                        logger.warning(
                            "Detected general LLM error in synthesis"
                        )
                        progress_callback(
                            "LLM error during synthesis. Attempting fallback...",
                            87,
                            {
                                "phase": "synthesis_error",
                                "error_type": error_type,
                            },
                        )
                    else:
                        # Generic error
                        logger.warning("Detected unknown error in synthesis")
                        progress_callback(
                            "Error during synthesis. Attempting fallback...",
                            87,
                            {
                                "phase": "synthesis_error",
                                "error_type": "unknown",
                            },
                        )

                    # Extract synthesized content from findings if available
                    synthesized_content = ""
                    for finding in results.get("findings", []):
                        if finding.get("phase") == "Final synthesis":
                            synthesized_content = finding.get("content", "")
                            break

                    # Use synthesized content as fallback
                    if (
                        synthesized_content
                        and not synthesized_content.startswith("Error:")
                    ):
                        logger.info(
                            "Using existing synthesized content as fallback"
                        )
                        raw_formatted_findings = synthesized_content

                    # Or use current_knowledge as another fallback
                    elif results.get("current_knowledge"):
                        logger.info("Using current_knowledge as fallback")
                        raw_formatted_findings = results["current_knowledge"]

                    # Or combine all finding contents as last resort
                    elif results.get("findings"):
                        logger.info("Combining all findings as fallback")
                        # First try to use any findings that are not errors
                        valid_findings = [
                            f"## {finding.get('phase', 'Finding')}\n\n{finding.get('content', '')}"
                            for finding in results.get("findings", [])
                            if finding.get("content")
                            and not finding.get("content", "").startswith(
                                "Error:"
                            )
                        ]

                        if valid_findings:
                            raw_formatted_findings = (
                                "# Research Results (Fallback Mode)\n\n"
                            )
                            raw_formatted_findings += "\n\n".join(
                                valid_findings
                            )
                            raw_formatted_findings += f"\n\n## Error Information\n{raw_formatted_findings}"
                        else:
                            # Last resort: use everything including errors
                            raw_formatted_findings = (
                                "# Research Results (Emergency Fallback)\n\n"
                            )
                            raw_formatted_findings += "The system encountered errors during final synthesis.\n\n"
                            raw_formatted_findings += "\n\n".join(
                                f"## {finding.get('phase', 'Finding')}\n\n{finding.get('content', '')}"
                                for finding in results.get("findings", [])
                                if finding.get("content")
                            )

                    progress_callback(
                        f"Using fallback synthesis due to {error_type} error",
                        88,
                        {
                            "phase": "synthesis_fallback",
                            "error_type": error_type,
                        },
                    )

                logger.info(
                    "Found formatted_findings of length: %s",
                    len(str(raw_formatted_findings)),
                )

                try:
                    # Get the synthesized content from the LLM directly
                    clean_markdown = raw_formatted_findings

                    # Extract all sources from findings to add them to the summary
                    all_links = []
                    for finding in results.get("findings", []):
                        search_results = finding.get("search_results", [])
                        if search_results:
                            try:
                                links = extract_links_from_search_results(
                                    search_results
                                )
                                all_links.extend(links)
                            except Exception:
                                logger.exception(
                                    "Error processing search results/links"
                                )

                    logger.info(
                        "Successfully converted to clean markdown of length: %s",
                        len(clean_markdown),
                    )

                    # First send a progress update for generating the summary
                    progress_callback(
                        "Generating clean summary from research data...",
                        90,
                        {"phase": "output_generation"},
                    )

                    # Save as markdown file
                    report_path = _generate_report_path(query)
                    output_dir = report_path.parent
                    output_dir.mkdir(parents=True, exist_ok=True)

                    # Send progress update for writing to file
                    progress_callback(
                        "Writing research report to file...",
                        95,
                        {"phase": "report_complete"},
                    )

                    logger.info("Writing report to: %s", report_path)
                    with report_path.open("w", encoding="utf-8") as f:
                        f.write("# Quick Research Summary\n\n")
                        f.write(f"Query: {query}\n\n")
                        f.write(clean_markdown)
                        f.write("\n\n## Research Metrics\n")
                        f.write(
                            f"- Search Iterations: {results['iterations']}\n"
                        )
                        f.write(
                            f"- Generated at: {datetime.utcnow().isoformat()}\n"
                        )

                    # Update database
                    metadata = {
                        "iterations": results["iterations"],
                        "generated_at": datetime.utcnow().isoformat(),
                    }

                    # Calculate duration in seconds - using UTC consistently
                    now = datetime.utcnow()
                    completed_at = now.isoformat()

                    logger.info(
                        "Updating database for research_id: %s", research_id
                    )

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

                        # Use the helper function for consistent duration calculation
                        duration_seconds = calculate_duration(
                            research.created_at, research.completed_at
                        )

                        research.status = "completed"
                        research.completed_at = completed_at
                        research.duration_seconds = duration_seconds
                        research.report_path = str(report_path)
                        research.research_meta = metadata

                        db_session.commit()

                    logger.info(
                        f"Database updated successfully for research_id: {research_id}"
                    )

                    # Send the final completion message
                    progress_callback(
                        "Research completed successfully",
                        100,
                        {"phase": "complete", "report_path": str(report_path)},
                    )

                    # Clean up resources
                    logger.info(
                        "Cleaning up resources for research_id: %s", research_id
                    )
                    cleanup_research_resources(
                        research_id, active_research, termination_flags
                    )
                    logger.info(
                        "Resources cleaned up for research_id: %s", research_id
                    )

                except Exception as inner_e:
                    logger.exception("Error during quick summary generation")
                    raise Exception(
                        f"Error generating quick summary: {str(inner_e)}"
                    )
            else:
                raise Exception(
                    "No research findings were generated. Please try again."
                )
        else:
            # Full Report
            progress_callback(
                "Generating detailed report...",
                85,
                {"phase": "report_generation"},
            )

            # Extract the search system from the results if available
            search_system = results.get("search_system", None)

            # Pass the existing search system to maintain citation indices
            report_generator = IntegratedReportGenerator(
                search_system=search_system
            )
            final_report = report_generator.generate_report(results, query)

            progress_callback(
                "Report generation complete", 95, {"phase": "report_complete"}
            )

            # Save as markdown file
            report_path = _generate_report_path(query)
            output_dir = report_path.parent
            output_dir.mkdir(parents=True, exist_ok=True)

            with report_path.open("w", encoding="utf-8") as f:
                f.write(final_report["content"])

            # Update database
            metadata = final_report["metadata"]
            metadata["iterations"] = results["iterations"]

            # Calculate duration in seconds - using UTC consistently
            now = datetime.utcnow()
            completed_at = now.isoformat()

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

                # Use the helper function for consistent duration calculation
                duration_seconds = calculate_duration(
                    research.created_at, research.completed_at
                )

                research.status = "completed"
                research.completed_at = completed_at
                research.duration_seconds = duration_seconds
                research.report_path = str(report_path)
                research.research_meta = metadata

                db_session.commit()

            progress_callback(
                "Research completed successfully",
                100,
                {"phase": "complete", "report_path": str(report_path)},
            )

            # Clean up resources
            cleanup_research_resources(
                research_id, active_research, termination_flags
            )

    except Exception as e:
        # Handle error
        error_message = f"Research failed: {str(e)}"
        logger.exception(error_message)

        try:
            # Check for common Ollama error patterns in the exception and provide more user-friendly errors
            user_friendly_error = str(e)
            error_context = {}

            if "Error type: ollama_unavailable" in user_friendly_error:
                user_friendly_error = "Ollama AI service is unavailable. Please check that Ollama is running properly on your system."
                error_context = {
                    "solution": "Start Ollama with 'ollama serve' or check if it's installed correctly."
                }
            elif "Error type: model_not_found" in user_friendly_error:
                user_friendly_error = "Required Ollama model not found. Please pull the model first."
                error_context = {
                    "solution": "Run 'ollama pull mistral' to download the required model."
                }
            elif "Error type: connection_error" in user_friendly_error:
                user_friendly_error = "Connection error with LLM service. Please check that your AI service is running."
                error_context = {
                    "solution": "Ensure Ollama or your API service is running and accessible."
                }
            elif "Error type: api_error" in user_friendly_error:
                # Keep the original error message as it's already improved
                error_context = {
                    "solution": "Check API configuration and credentials."
                }

            # Update metadata with more context about the error
            metadata = {"phase": "error", "error": user_friendly_error}
            if error_context:
                metadata.update(error_context)

            # If we still have an active research record, update its log
            if research_id in active_research:
                progress_callback(user_friendly_error, None, metadata)

            # If termination was requested, mark as suspended instead of failed
            status = (
                "suspended"
                if (
                    research_id in termination_flags
                    and termination_flags[research_id]
                )
                else "failed"
            )
            message = (
                "Research was terminated by user"
                if status == "suspended"
                else user_friendly_error
            )

            # Calculate duration up to termination point - using UTC consistently
            now = datetime.utcnow()
            completed_at = now.isoformat()

            # Get the start time from the database
            duration_seconds = None
            db_session = get_db_session()
            with db_session:
                research = (
                    db_session.query(ResearchHistory)
                    .filter_by(id=research_id)
                    .first()
                )
                assert research is not None, "Research not in database"

                duration_seconds = calculate_duration(research.created_at)

            db_session = get_db_session()
            with db_session:
                research = (
                    db_session.query(ResearchHistory)
                    .filter_by(id=research_id)
                    .first()
                )
                assert research is not None, "Research not in database"

                # Update the ResearchHistory object with the new status and completion time
                research.status = status
                research.completed_at = completed_at
                research.duration_seconds = duration_seconds
                research.metadata = metadata

                db_session.commit()

            try:
                SocketIOService().emit_to_subscribers(
                    "research_progress",
                    research_id,
                    {"status": status, "error": message},
                )
            except Exception:
                logger.exception("Failed to emit error via socket")

        except Exception:
            logger.exception("Error in error handler")

        # Clean up resources
        cleanup_research_resources(
            research_id, active_research, termination_flags
        )


def cleanup_research_resources(research_id, active_research, termination_flags):
    """
    Clean up resources for a completed research.

    Args:
        research_id: The ID of the research
        active_research: Dictionary of active research processes
        termination_flags: Dictionary of termination flags
    """
    logger.info("Cleaning up resources for research %s", research_id)

    # Get the current status from the database to determine the final status message
    current_status = "completed"  # Default
    try:
        db_session = get_db_session()
        with db_session:
            research = (
                db_session.query(ResearchHistory)
                .filter(ResearchHistory.id == research_id)
                .first()
            )
            if research:
                current_status = research.status
            else:
                logger.error("Research with ID %s not found", research_id)
    except Exception:
        logger.exception("Error retrieving research status during cleanup")

    # Remove from active research
    if research_id in active_research:
        del active_research[research_id]

    # Remove from termination flags
    if research_id in termination_flags:
        del termination_flags[research_id]

    # Send a final message to subscribers
    try:
        # Import here to avoid circular imports
        from ..routes.globals import get_globals

        globals_dict = get_globals()
        socket_subscriptions = globals_dict.get("socket_subscriptions", {})

        # Send a final message to any remaining subscribers with explicit status
        if (
            research_id in socket_subscriptions
            and socket_subscriptions[research_id]
        ):
            # Use the proper status message based on database status
            if current_status == "suspended" or current_status == "failed":
                final_message = {
                    "status": current_status,
                    "message": f"Research was {current_status}",
                    "progress": 0,  # For suspended research, show 0% not 100%
                }
            else:
                final_message = {
                    "status": "completed",
                    "message": "Research process has ended and resources have been cleaned up",
                    "progress": 100,
                }

            logger.info(
                "Sending final %s socket message for research %s",
                current_status,
                research_id,
            )

            SocketIOService().emit_to_subscribers(
                "research_progress", research_id, final_message
            )

    except Exception:
        logger.error("Error sending final cleanup message")


def handle_termination(research_id, active_research, termination_flags):
    """
    Handle the termination of a research process.

    Args:
        research_id: The ID of the research
        active_research: Dictionary of active research processes
        termination_flags: Dictionary of termination flags
    """
    now = datetime.utcnow()
    completed_at = now.isoformat()

    # Fetch the start time from the database using the ORM
    session = get_db_session()
    research = session.query(ResearchHistory).filter_by(id=research_id).first()

    if research:
        duration_seconds = calculate_duration(research.created_at)

        # Update the database with suspended status using the ORM
        research.status = "suspended"
        research.completed_at = completed_at
        research.duration_seconds = duration_seconds
        session.commit()
    else:
        logger.error(f"Research with ID {research_id} not found.")

    # Clean up resources
    cleanup_research_resources(research_id, active_research, termination_flags)


def cancel_research(research_id):
    """
    Cancel/terminate a research process using ORM.

    Args:
        research_id: The ID of the research to cancel

    Returns:
        bool: True if the research was found and cancelled, False otherwise
    """
    # Import globals from research routes
    from ..routes.globals import get_globals

    globals_dict = get_globals()
    active_research = globals_dict["active_research"]
    termination_flags = globals_dict["termination_flags"]

    # Set termination flag
    termination_flags[research_id] = True

    # Check if the research is active
    if research_id in active_research:
        # Call handle_termination to update database
        handle_termination(research_id, active_research, termination_flags)
        return True
    else:
        # Update database directly if not found in active_research
        session = get_db_session()
        research = (
            session.query(ResearchHistory).filter_by(id=research_id).first()
        )
        if not research:
            return False

        # If it exists but isn't in active_research, still update status
        research.status = "suspended"
        session.commit()
        return True
