"""
Constraint-guided candidate explorer implementation.

This explorer uses constraints to guide the search process, prioritizing
searches that are likely to find candidates satisfying the constraints.
"""

import time
from typing import List, Optional

from loguru import logger

from ..candidates.base_candidate import Candidate
from ..constraints.base_constraint import Constraint, ConstraintType
from .base_explorer import (
    BaseCandidateExplorer,
    ExplorationResult,
    ExplorationStrategy,
)


class ConstraintGuidedExplorer(BaseCandidateExplorer):
    """
    Constraint-guided candidate explorer.

    This explorer:
    1. Prioritizes searches based on constraint importance
    2. Uses constraint-specific search strategies
    3. Validates candidates against constraints early
    4. Focuses on constraint satisfaction over quantity
    """

    def __init__(
        self,
        *args,
        constraint_weight_threshold: float = 0.7,  # Focus on constraints above this weight
        early_validation: bool = True,  # Validate candidates during search
        **kwargs,
    ):
        """
        Initialize constraint-guided explorer.

        Args:
            constraint_weight_threshold: Focus on constraints above this weight
            early_validation: Whether to validate candidates during exploration
        """
        super().__init__(*args, **kwargs)
        self.constraint_weight_threshold = constraint_weight_threshold
        self.early_validation = early_validation

    def explore(
        self,
        initial_query: str,
        constraints: Optional[List[Constraint]] = None,
        entity_type: Optional[str] = None,
    ) -> ExplorationResult:
        """Explore candidates using constraint-guided strategy."""
        start_time = time.time()
        logger.info(
            f"Starting constraint-guided exploration for: {initial_query}"
        )

        if not constraints:
            logger.warning(
                "No constraints provided - falling back to basic search"
            )
            return self._basic_exploration(
                initial_query, entity_type, start_time
            )

        all_candidates = []
        exploration_paths = []
        total_searched = 0

        # Prioritize constraints by weight and type
        prioritized_constraints = self._prioritize_constraints(constraints)

        # Search for each constraint
        for i, constraint in enumerate(prioritized_constraints):
            if not self._should_continue_exploration(
                start_time, len(all_candidates)
            ):
                break

            logger.info(
                f"Exploring constraint {i + 1}/{len(prioritized_constraints)}: {constraint.value}"
            )

            # Generate constraint-specific queries
            constraint_queries = self._generate_constraint_queries(
                constraint, initial_query, entity_type
            )

            constraint_candidates = []

            for query in constraint_queries[:3]:  # Limit queries per constraint
                if query.lower() in self.explored_queries:
                    continue

                results = self._execute_search(query)
                candidates = self._extract_candidates_from_results(
                    results, entity_type
                )

                # Early validation if enabled
                if self.early_validation:
                    validated_candidates = self._early_validate_candidates(
                        candidates, constraint
                    )
                    constraint_candidates.extend(validated_candidates)
                else:
                    constraint_candidates.extend(candidates)

                total_searched += 1
                exploration_paths.append(
                    f"Constraint '{constraint.value}': {query} -> {len(candidates)} candidates"
                )

            all_candidates.extend(constraint_candidates)
            logger.info(
                f"Found {len(constraint_candidates)} candidates for constraint: {constraint.value}"
            )

        # Cross-constraint exploration
        if len(prioritized_constraints) > 1:
            cross_candidates = self._cross_constraint_exploration(
                prioritized_constraints[:2], initial_query, entity_type
            )
            all_candidates.extend(cross_candidates)
            exploration_paths.append(
                f"Cross-constraint search -> {len(cross_candidates)} candidates"
            )
            total_searched += 1

        # Process final results
        unique_candidates = self._deduplicate_candidates(all_candidates)
        ranked_candidates = self._rank_by_constraint_alignment(
            unique_candidates, constraints, initial_query
        )
        final_candidates = ranked_candidates[: self.max_candidates]

        elapsed_time = time.time() - start_time
        logger.info(
            f"Constraint-guided exploration completed: {len(final_candidates)} candidates in {elapsed_time:.1f}s"
        )

        return ExplorationResult(
            candidates=final_candidates,
            total_searched=total_searched,
            unique_candidates=len(unique_candidates),
            exploration_paths=exploration_paths,
            metadata={
                "strategy": "constraint_guided",
                "constraints_used": len(prioritized_constraints),
                "early_validation": self.early_validation,
                "entity_type": entity_type,
            },
            elapsed_time=elapsed_time,
            strategy_used=ExplorationStrategy.CONSTRAINT_GUIDED,
        )

    def generate_exploration_queries(
        self,
        base_query: str,
        found_candidates: List[Candidate],
        constraints: Optional[List[Constraint]] = None,
    ) -> List[str]:
        """Generate constraint-guided exploration queries."""
        if not constraints:
            return [base_query]

        queries = []

        # Generate queries for each constraint
        for constraint in constraints[:3]:  # Limit to avoid too many queries
            constraint_queries = self._generate_constraint_queries(
                constraint, base_query
            )
            queries.extend(constraint_queries[:2])  # Top 2 per constraint

        # Generate queries combining multiple constraints
        if len(constraints) > 1:
            combined_query = self._combine_constraints_query(
                base_query, constraints[:2]
            )
            if combined_query:
                queries.append(combined_query)

        return queries

    def _prioritize_constraints(
        self, constraints: List[Constraint]
    ) -> List[Constraint]:
        """Prioritize constraints by weight and type."""
        # Sort by weight (descending) and then by type priority
        type_priority = {
            ConstraintType.NAME_PATTERN: 1,
            ConstraintType.PROPERTY: 2,
            ConstraintType.EVENT: 3,
            ConstraintType.LOCATION: 4,
            ConstraintType.TEMPORAL: 5,
            ConstraintType.STATISTIC: 6,
            ConstraintType.COMPARISON: 7,
            ConstraintType.EXISTENCE: 8,
        }

        return sorted(
            constraints,
            key=lambda c: (c.weight, type_priority.get(c.type, 9)),
            reverse=True,
        )

    def _generate_constraint_queries(
        self,
        constraint: Constraint,
        base_query: str,
        entity_type: Optional[str] = None,
    ) -> List[str]:
        """Generate search queries specific to a constraint."""
        queries = []

        # Base constraint query
        if entity_type:
            queries.append(f"{entity_type} {constraint.value}")
        else:
            queries.append(f"{base_query} {constraint.value}")

        # Constraint-type specific queries
        if constraint.type == ConstraintType.NAME_PATTERN:
            queries.extend(
                self._name_pattern_queries(constraint, base_query, entity_type)
            )
        elif constraint.type == ConstraintType.PROPERTY:
            queries.extend(
                self._property_queries(constraint, base_query, entity_type)
            )
        elif constraint.type == ConstraintType.EVENT:
            queries.extend(
                self._event_queries(constraint, base_query, entity_type)
            )
        elif constraint.type == ConstraintType.LOCATION:
            queries.extend(
                self._location_queries(constraint, base_query, entity_type)
            )

        return queries

    def _name_pattern_queries(
        self,
        constraint: Constraint,
        base_query: str,
        entity_type: Optional[str],
    ) -> List[str]:
        """Generate queries for name pattern constraints."""
        queries = []

        if "body part" in constraint.value.lower():
            body_parts = [
                "arm",
                "leg",
                "foot",
                "hand",
                "eye",
                "ear",
                "head",
                "tooth",
                "nose",
                "heart",
            ]
            for part in body_parts[:3]:  # Sample a few
                if entity_type:
                    queries.append(f"{entity_type} {part}")
                else:
                    queries.append(f"{base_query} {part} name")

        return queries

    def _property_queries(
        self,
        constraint: Constraint,
        base_query: str,
        entity_type: Optional[str],
    ) -> List[str]:
        """Generate queries for property constraints."""
        base = entity_type or base_query
        return [
            f"{base} with {constraint.value}",
            f"{base} that {constraint.value}",
            f"{constraint.value} {base}",
        ]

    def _event_queries(
        self,
        constraint: Constraint,
        base_query: str,
        entity_type: Optional[str],
    ) -> List[str]:
        """Generate queries for event constraints."""
        base = entity_type or base_query
        return [
            f"{base} {constraint.value} incident",
            f"{base} {constraint.value} accident",
            f"{constraint.value} at {base}",
        ]

    def _location_queries(
        self,
        constraint: Constraint,
        base_query: str,
        entity_type: Optional[str],
    ) -> List[str]:
        """Generate queries for location constraints."""
        return [
            f"{constraint.value} {base_query}",
            f"{base_query} in {constraint.value}",
            f"{constraint.value} locations",
        ]

    def _cross_constraint_exploration(
        self,
        constraints: List[Constraint],
        base_query: str,
        entity_type: Optional[str],
    ) -> List[Candidate]:
        """Explore candidates satisfying multiple constraints."""
        if len(constraints) < 2:
            return []

        # Combine top 2 constraints
        combined_query = self._combine_constraints_query(
            base_query, constraints
        )

        if (
            combined_query
            and combined_query.lower() not in self.explored_queries
        ):
            results = self._execute_search(combined_query)
            return self._extract_candidates_from_results(results, entity_type)

        return []

    def _combine_constraints_query(
        self, base_query: str, constraints: List[Constraint]
    ) -> Optional[str]:
        """Combine multiple constraints into a single query."""
        if len(constraints) < 2:
            return None

        constraint_values = [c.value for c in constraints[:2]]
        return f"{base_query} {' AND '.join(constraint_values)}"

    def _early_validate_candidates(
        self, candidates: List[Candidate], constraint: Constraint
    ) -> List[Candidate]:
        """Perform early validation of candidates against constraint."""
        if not candidates or constraint.type != ConstraintType.NAME_PATTERN:
            return candidates  # Only validate name patterns for now

        validated = []

        for candidate in candidates:
            if self._quick_name_validation(candidate.name, constraint):
                validated.append(candidate)

        return validated

    def _quick_name_validation(
        self, candidate_name: str, constraint: Constraint
    ) -> bool:
        """Quick validation of candidate name against constraint."""
        if "body part" in constraint.value.lower():
            body_parts = [
                "arm",
                "leg",
                "foot",
                "hand",
                "eye",
                "ear",
                "head",
                "tooth",
                "nose",
                "heart",
            ]
            name_lower = candidate_name.lower()
            return any(part in name_lower for part in body_parts)

        return True  # Default to accepting if can't validate

    def _rank_by_constraint_alignment(
        self,
        candidates: List[Candidate],
        constraints: List[Constraint],
        base_query: str,
    ) -> List[Candidate]:
        """Rank candidates by alignment with constraints."""
        for candidate in candidates:
            # Simple scoring based on constraint alignment
            score = 0.0

            # Score based on name pattern constraints
            for constraint in constraints:
                if constraint.type == ConstraintType.NAME_PATTERN:
                    if self._quick_name_validation(candidate.name, constraint):
                        score += constraint.weight

            candidate.constraint_alignment_score = score

        # Sort by constraint alignment, then by relevance
        ranked = self._rank_candidates_by_relevance(candidates, base_query)
        return sorted(
            ranked,
            key=lambda c: getattr(c, "constraint_alignment_score", 0.0),
            reverse=True,
        )

    def _basic_exploration(
        self, initial_query: str, entity_type: Optional[str], start_time: float
    ) -> ExplorationResult:
        """Fallback basic exploration when no constraints provided."""
        candidates = []

        results = self._execute_search(initial_query)
        candidates = self._extract_candidates_from_results(results, entity_type)

        elapsed_time = time.time() - start_time

        return ExplorationResult(
            candidates=candidates[: self.max_candidates],
            total_searched=1,
            unique_candidates=len(candidates),
            exploration_paths=[f"Basic search: {initial_query}"],
            metadata={"strategy": "basic_fallback"},
            elapsed_time=elapsed_time,
            strategy_used=ExplorationStrategy.BREADTH_FIRST,
        )
