"""
Base candidate explorer for inheritance-based exploration system.

This module provides the base interface and common functionality for
candidate exploration implementations.
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Any, Optional

from langchain_core.language_models import BaseChatModel
from loguru import logger

from ..candidates.base_candidate import Candidate
from ..constraints.base_constraint import Constraint
from ...utilities.json_utils import get_llm_response_text


class ExplorationStrategy(Enum):
    """Different exploration strategies."""

    BREADTH_FIRST = "breadth_first"  # Explore widely first
    DEPTH_FIRST = "depth_first"  # Deep dive into promising areas
    CONSTRAINT_GUIDED = "constraint_guided"  # Let constraints guide exploration
    DIVERSITY_FOCUSED = "diversity_focused"  # Maximize candidate diversity
    ADAPTIVE = "adaptive"  # Adapt based on findings


@dataclass
class ExplorationResult:
    """Result of candidate exploration."""

    candidates: list[Candidate]
    total_searched: int
    unique_candidates: int
    exploration_paths: list[str]
    metadata: dict[str, Any]
    elapsed_time: float
    strategy_used: ExplorationStrategy


class BaseCandidateExplorer(ABC):
    """
    Base class for candidate exploration implementations.

    This provides the common interface and shared functionality that
    all candidate explorers should implement.
    """

    def __init__(
        self,
        model: BaseChatModel,
        search_engine,
        max_candidates: int = 50,
        max_search_time: float = 60.0,
        **kwargs,
    ):
        """
        Initialize the base candidate explorer.

        Args:
            model: Language model for analysis
            search_engine: Search engine for finding candidates
            max_candidates: Maximum number of candidates to find
            max_search_time: Maximum time to spend searching
            **kwargs: Additional parameters for specific implementations
        """
        self.model = model
        self.search_engine = search_engine
        self.max_candidates = max_candidates
        self.max_search_time = max_search_time

        # Tracking
        self.explored_queries: set[str] = set()
        self.found_candidates: dict[str, Candidate] = {}

    @abstractmethod
    def explore(
        self,
        initial_query: str,
        constraints: Optional[list[Constraint]] = None,
        entity_type: Optional[str] = None,
    ) -> ExplorationResult:
        """
        Explore and discover candidates.

        Args:
            initial_query: Starting query for exploration
            constraints: Optional constraints to guide exploration
            entity_type: Optional entity type to focus on

        Returns:
            ExplorationResult: Complete exploration results
        """
        pass

    @abstractmethod
    def generate_exploration_queries(
        self,
        base_query: str,
        found_candidates: list[Candidate],
        constraints: Optional[list[Constraint]] = None,
    ) -> list[str]:
        """
        Generate new queries for continued exploration.

        Args:
            base_query: Original base query
            found_candidates: Candidates found so far
            constraints: Optional constraints to consider

        Returns:
            List[str]: New queries to explore
        """
        pass

    def _execute_search(self, query: str) -> dict[str, Any]:
        """Execute a search query."""
        try:
            # Mark query as explored
            self.explored_queries.add(query.lower())

            # Execute search
            results = self.search_engine.run(query)

            # Handle different result formats
            if isinstance(results, list):
                # If results is a list, wrap it in the expected format
                formatted_results = {"results": results, "query": query}
                logger.info(
                    f"Search '{query[:50]}...' returned {len(results)} results"
                )
                return formatted_results
            if isinstance(results, dict):
                # If results is already a dict, use it as is
                result_count = len(results.get("results", []))
                logger.info(
                    f"Search '{query[:50]}...' returned {result_count} results"
                )
                return results
            # Unknown format, return empty
            logger.warning(f"Unknown search result format: {type(results)}")
            return {"results": [], "query": query}

        except Exception:
            logger.exception(f"Error executing search '{query}'")
            return {"results": []}

    def _extract_candidates_from_results(
        self,
        results: dict[str, Any],
        original_query: str = None,
        entity_type: Optional[str] = None,
    ) -> list[Candidate]:
        """Generate answer candidates directly from search results using LLM."""
        candidates = []

        # Collect all search result content
        all_content = []
        for result in results.get("results", []):
            title = result.get("title", "")
            snippet = result.get("snippet", "")
            if title or snippet:
                all_content.append(f"Title: {title}\nContent: {snippet}")

        if not all_content or not original_query:
            return candidates

        # Generate answer candidates using LLM
        answer_candidates = self._generate_answer_candidates(
            original_query,
            "\n\n".join(all_content[:10]),  # Limit to first 10 results
        )

        for answer in answer_candidates:
            if answer and answer not in self.found_candidates:
                candidate = Candidate(
                    name=answer,
                    metadata={
                        "source": "llm_answer_generation",
                        "query": results.get("query", ""),
                        "original_query": original_query,
                        "result_count": len(results.get("results", [])),
                    },
                )
                candidates.append(candidate)
                self.found_candidates[answer] = candidate

        return candidates

    def _generate_answer_candidates(
        self, question: str, search_content: str
    ) -> list[str]:
        """Generate multiple answer candidates from search results."""
        prompt = f"""
Question: {question}

Based on these search results, provide 3-5 possible answers:

{search_content}

Give me multiple possible answers, one per line:
"""

        try:
            response = self.model.invoke(prompt)
            content = get_llm_response_text(response)

            # Parse multiple answers
            answers = []
            for line in content.split("\n"):
                line = line.strip()
                if line:
                    # Clean up common prefixes and formatting
                    line = line.lstrip("•-*1234567890.").strip()
                    if line and len(line) > 2:  # Skip very short answers
                        answers.append(line)

            return answers[:5]  # Limit to 5 candidates max

        except Exception:
            logger.exception("Error generating answer candidates")
            return []

    def _extract_entity_names(
        self, text: str, entity_type: Optional[str] = None
    ) -> list[str]:
        """Extract entity names from text using LLM."""
        if not text.strip():
            return []

        prompt = f"""
Extract specific entity names from this text.
{"Focus on: " + entity_type if entity_type else "Extract any named entities."}

Text: {text[:500]}

Return only the names, one per line. Be selective - only include clear, specific names.
Do not include:
- Generic terms or categories
- Adjectives or descriptions
- Common words

Names:
"""

        try:
            response = get_llm_response_text(self.model.invoke(prompt))

            # Parse response into names
            names = []
            for line in response.split("\n"):
                name = line.strip()
                if (
                    name
                    and len(name) > 2
                    and not name.lower().startswith(("the ", "a ", "an "))
                ):
                    names.append(name)

            return names[:5]  # Limit to top 5 per text

        except Exception:
            logger.exception("Error extracting entity names")
            return []

    def _should_continue_exploration(
        self, start_time: float, candidates_found: int
    ) -> bool:
        """Determine if exploration should continue."""
        import time

        elapsed = time.time() - start_time

        # Stop if time limit reached
        if elapsed > self.max_search_time:
            logger.info(f"Time limit reached ({elapsed:.1f}s)")
            return False

        # Stop if candidate limit reached
        if candidates_found >= self.max_candidates:
            logger.info(f"Candidate limit reached ({candidates_found})")
            return False

        return True

    def _deduplicate_candidates(
        self, candidates: list[Candidate]
    ) -> list[Candidate]:
        """Remove duplicate candidates based on name similarity."""
        unique_candidates = []
        seen_names = set()

        for candidate in candidates:
            # Simple deduplication by exact name match
            name_key = candidate.name.lower().strip()
            if name_key not in seen_names:
                seen_names.add(name_key)
                unique_candidates.append(candidate)

        return unique_candidates

    def _rank_candidates_by_relevance(
        self, candidates: list[Candidate], query: str
    ) -> list[Candidate]:
        """Rank candidates by relevance to original query."""
        if not candidates:
            return candidates

        # Simple relevance scoring based on metadata
        for candidate in candidates:
            score = 0.0

            # Score based on source query similarity
            if "query" in candidate.metadata:
                # Simple word overlap scoring
                query_words = set(query.lower().split())
                candidate_query_words = set(
                    candidate.metadata["query"].lower().split()
                )
                overlap = len(query_words.intersection(candidate_query_words))
                score += overlap * 0.1

            # Score based on result title relevance
            if "result_title" in candidate.metadata:
                title_words = set(
                    candidate.metadata["result_title"].lower().split()
                )
                overlap = len(query_words.intersection(title_words))
                score += overlap * 0.2

            candidate.relevance_score = score

        # Sort by relevance
        return sorted(
            candidates,
            key=lambda c: getattr(c, "relevance_score", 0.0),
            reverse=True,
        )
