"""
Base class for all search strategies.
Defines the common interface and shared functionality for different search approaches.
"""

from abc import ABC, abstractmethod
from typing import Any, Callable, Optional

from loguru import logger

from ...utilities.type_utils import unwrap_setting


class BaseSearchStrategy(ABC):
    """Abstract base class for all search strategies."""

    def __init__(
        self,
        all_links_of_system=None,
        settings_snapshot=None,
        questions_by_iteration=None,
        search_original_query: bool = True,
    ):
        """Initialize the base strategy with common attributes.

        Args:
            all_links_of_system: List to store all discovered links
            settings_snapshot: Settings snapshot for configuration
            questions_by_iteration: Dictionary of questions by iteration
            search_original_query: Whether to include the original query in the first iteration
        """
        # Log strategy initialization
        strategy_name = self.__class__.__name__
        logger.info(f"Initializing strategy: {strategy_name}")
        logger.debug(
            f"Strategy {strategy_name} - search_original_query: {search_original_query}"
        )

        self.progress_callback: (
            Callable[[str, int | None, dict[str, Any]], None] | None
        ) = None
        # Create a new dict if None is provided (avoiding mutable default argument)
        self.questions_by_iteration = (
            questions_by_iteration if questions_by_iteration is not None else {}
        )
        # Create a new list if None is provided (avoiding mutable default argument)
        self.all_links_of_system = (
            all_links_of_system if all_links_of_system is not None else []
        )
        self.settings_snapshot = settings_snapshot or {}
        self.search_original_query = search_original_query

    def close(self):
        """Release any persistent resources held by this strategy.

        Override this method if your strategy creates persistent resources
        in __init__ (e.g. ThreadPoolExecutor, HTTP sessions).

        Currently, two strategies need this:
        - ConstraintParallelStrategy: holds search_executor +
          evaluation_executor
        - ConcurrentDualConfidenceStrategy: holds evaluation_executor

        The default source-based strategy uses context-managed ('with')
        executors that clean up automatically, so no override is needed.

        Note: This is called by AdvancedSearchSystem.close() which is
        called in run_research_process()'s finally block. Strategies
        should also clean up in their own finally blocks (as they already
        do) — this close() is a second line of defense.
        """
        pass

    def get_setting(self, key: str, default=None):
        """Get a setting value from the snapshot."""
        if key in self.settings_snapshot:
            return unwrap_setting(self.settings_snapshot[key])
        return default

    def set_progress_callback(
        self, callback: Callable[[str, int | None, dict[str, Any]], None]
    ) -> None:
        """Set a callback function to receive progress updates."""
        self.progress_callback = callback

    def check_termination(self) -> None:
        """Check if the research has been cancelled.

        Reuses the existing ``progress_callback`` rather than a dedicated
        termination-check callable.  This avoids duplicating the ~15 wiring
        points across the codebase (search_system, research_service,
        research_functions, and every nested strategy) where
        ``set_progress_callback`` is already propagated.  A dedicated
        ``set_termination_check`` method could be introduced later if the
        callback-based approach becomes a maintenance burden.

        The callback in ``research_service.py`` recognises the
        ``"termination_check"`` phase and returns immediately after the
        flag check -- no UI logging or socket emission occurs.
        """
        if self.progress_callback:
            logger.debug("Checking termination status")
            self.progress_callback(
                "Checking termination status",
                None,
                {"phase": "termination_check"},
            )

    def _update_progress(
        self,
        message: str,
        progress_percent: Optional[int] = None,
        metadata: Optional[dict[str, Any]] = None,
    ) -> None:
        """Send a progress update via the callback if available."""
        if self.progress_callback:
            self.progress_callback(message, progress_percent, metadata or {})

    @abstractmethod
    def analyze_topic(self, query: str) -> dict[str, Any]:
        """
        Analyze a topic using the strategy's specific approach.

        Args:
            query: The research query to analyze

        Returns:
            Dict containing:
            - findings: List of research findings
            - iterations: Number of iterations completed
            - questions: Questions generated by iteration
            - formatted_findings: Formatted output
            - current_knowledge: Accumulated knowledge
            - error: Optional error message
        """
        pass

    def _validate_search_engine(self) -> bool:
        """
        Validate that the search engine is available and configured.

        Returns:
            bool: True if search engine is available, False otherwise
        """
        if not hasattr(self, "search") or self.search is None:
            error_msg = "Error: No search engine available. Please check your configuration."
            self._update_progress(
                error_msg,
                100,
                {
                    "phase": "error",
                    "error": "No search engine available",
                    "status": "failed",
                },
            )
            return False
        return True

    def _handle_search_error(
        self, error: Exception, question: str, progress_base: int
    ) -> list:
        """
        Handle errors during search execution.

        Args:
            error: The exception that occurred
            question: The question being searched
            progress_base: The current progress percentage

        Returns:
            List: Empty list to continue processing
        """
        error_msg = f"Error during search: {error!s}"
        logger.error(f"SEARCH ERROR: {error_msg}")
        self._update_progress(
            error_msg,
            progress_base + 2,
            {"phase": "search_error", "error": str(error)},
        )
        return []

    def _handle_analysis_error(
        self, error: Exception, question: str, progress_base: int
    ) -> None:
        """
        Handle errors during result analysis.

        Args:
            error: The exception that occurred
            question: The question being analyzed
            progress_base: The current progress percentage
        """
        error_msg = f"Error analyzing results: {error!s}"
        logger.info(f"ANALYSIS ERROR: {error_msg}")
        self._update_progress(
            error_msg,
            progress_base + 10,
            {"phase": "analysis_error", "error": str(error)},
        )

    def _emit_question_generation_progress(
        self,
        iteration: int,
        progress_percent: int,
        source_count: int = 0,
        query: str = "",
    ) -> None:
        """
        Emit progress update for question generation phase.

        Args:
            iteration: Current iteration number
            progress_percent: Current progress percentage
            source_count: Number of sources found (for iteration > 1)
            query: The original research query (for iteration 1)
        """
        if iteration == 1:
            self._update_progress(
                f"Generating initial search questions:\n• {query}",
                progress_percent,
                {
                    "phase": "question_generation",
                    "type": "milestone",
                    "iteration": iteration,
                },
            )
        else:
            # Show previous questions and source count
            prev_questions = self.questions_by_iteration.get(iteration - 1, [])
            prev_display = "\n".join(f"• {q}" for q in prev_questions[:3])
            if len(prev_questions) > 3:
                prev_display += f"\n... and {len(prev_questions) - 3} more"
            self._update_progress(
                f"Generating questions from {source_count} sources:\n{prev_display}",
                progress_percent,
                {
                    "phase": "question_generation",
                    "type": "milestone",
                    "iteration": iteration,
                    "source_count": source_count,
                },
            )

    def _create_error_response(self, error: str) -> dict[str, Any]:
        """Build a standardized error result.

        Shared by ``mcp_strategy`` and ``focused_iteration_strategy``.
        Both ``questions`` and ``questions_by_iteration`` keys are
        included because the two consumers read different keys
        historically — keeping both avoids breaking either caller.
        """
        return {
            "findings": [],
            "iterations": 0,
            "questions": {},
            "questions_by_iteration": {},
            "formatted_findings": f"Error: {error}",
            "current_knowledge": "",
            "error": error,
        }

    def _format_citations(
        self, content: str, search_results: list[dict[str, Any]]
    ) -> str:
        """Append a Markdown ``## Sources`` bibliography to ``content``.

        Shared by ``mcp_strategy`` (uses ``self.all_search_results``)
        and ``langgraph_agent_strategy`` (uses ``self.collector.results``).
        Returns ``content`` unchanged when no links can be extracted.
        """
        if not search_results:
            return content
        try:
            from ...utilities.search_utilities import (
                extract_links_from_search_results,
                format_links_to_markdown,
            )

            all_links = extract_links_from_search_results(search_results)
            if not all_links:
                return content
            sources_markdown = format_links_to_markdown(all_links)
            if not sources_markdown:
                return content
            return f"{content}\n\n## Sources\n\n{sources_markdown}"
        except Exception:
            logger.exception("Failed to format source links")
            return content

    def _emit_searching_progress(
        self,
        iteration: int,
        questions: list[str],
        progress_percent: int,
    ) -> None:
        """
        Emit progress update for searching phase with questions as milestone.

        Args:
            iteration: Current iteration number
            questions: List of questions being searched
            progress_percent: Current progress percentage
        """
        questions_display = "\n".join(f"• {q}" for q in questions[:5])
        if len(questions) > 5:
            questions_display += f"\n... and {len(questions) - 5} more"
        self._update_progress(
            f"Searching iteration {iteration}:\n{questions_display}",
            progress_percent,
            {
                "phase": "parallel_search",
                "type": "milestone",
                "iteration": iteration,
                "questions": questions,
            },
        )
