"""
Intelligent Constraint Relaxation Strategy

This module implements progressive constraint relaxation to improve BrowseComp
performance when strict constraint matching fails.

Based on BROWSECOMP_IMPROVEMENT_STRATEGY.md recommendations for handling
complex multi-constraint queries that may not have perfect matches.
"""

from loguru import logger
from typing import Dict, List


class IntelligentConstraintRelaxer:
    """
    Progressive constraint relaxation based on search results and constraint reliability.

    Features:
    1. Maintains essential identifying constraints
    2. Relaxes problematic constraint types first
    3. Creates multiple search attempts with different constraint sets
    4. Preserves constraint importance hierarchy
    """

    def __init__(self):
        # Constraint priorities (higher = more important, never relax)
        self.constraint_priorities = {
            "NAME_PATTERN": 10,  # Never relax - essential for identification
            "EXISTENCE": 9,  # Rarely relax - basic entity existence
            "LOCATION": 8,  # Usually important for identification
            "TEMPORAL": 7,  # Dates often crucial but sometimes fuzzy
            "PROPERTY": 6,  # Basic properties, moderately important
            "EVENT": 5,  # Events can be important but sometimes optional
            "STATISTIC": 3,  # Often relax - numbers frequently imprecise
            "COMPARISON": 1,  # Frequently relax - relative comparisons problematic
            "RELATIONSHIP": 2,  # Often problematic due to complexity
        }

        # Minimum constraints to keep for meaningful search
        self.min_constraints = 2

        # Constraint relaxation strategies by type
        self.relaxation_strategies = {
            "STATISTIC": self._relax_statistical_constraint,
            "COMPARISON": self._relax_comparison_constraint,
            "TEMPORAL": self._relax_temporal_constraint,
            "PROPERTY": self._relax_property_constraint,
        }

    def relax_constraints_progressively(
        self,
        constraints: List[object],
        candidates_found: List[object],
        target_candidates: int = 5,
    ) -> List[List[object]]:
        """
        Generate progressive constraint relaxation sets based on search results.

        Args:
            constraints: Original constraint list
            candidates_found: Current candidates found
            target_candidates: Target number of candidates to find

        Returns:
            List of relaxed constraint sets to try
        """
        if len(candidates_found) >= target_candidates:
            logger.debug("Sufficient candidates found, no relaxation needed")
            return [constraints]  # No relaxation needed

        logger.info(
            f"Only {len(candidates_found)} candidates found, generating relaxation strategies"
        )

        # Sort constraints by relaxation priority (lowest first)
        relaxable_constraints = sorted(
            constraints,
            key=lambda c: self.constraint_priorities.get(
                self._get_constraint_type(c), 5
            ),
        )

        relaxed_sets = []

        # Strategy 1: Remove least important constraints progressively
        for i in range(1, min(len(constraints), 4)):  # Max 3 relaxation levels
            relaxed_set = relaxable_constraints[
                :-i
            ]  # Remove i lowest priority constraints

            if len(relaxed_set) >= self.min_constraints:
                relaxed_sets.append(relaxed_set)
                logger.debug(
                    f"Relaxation level {i}: Removed {i} constraints, {len(relaxed_set)} remaining"
                )

        # Strategy 2: Create constraint variations for difficult constraints
        variation_sets = self._create_constraint_variations(constraints)
        relaxed_sets.extend(variation_sets)

        # Strategy 3: Keep only high-priority constraints
        high_priority_constraints = [
            c
            for c in constraints
            if self.constraint_priorities.get(self._get_constraint_type(c), 5)
            >= 7
        ]

        if len(high_priority_constraints) >= self.min_constraints:
            relaxed_sets.append(high_priority_constraints)
            logger.debug(
                f"High-priority only: {len(high_priority_constraints)} constraints"
            )

        # Remove duplicates while preserving order
        unique_sets = []
        seen_sets = set()

        for constraint_set in relaxed_sets:
            # Create a hashable representation
            set_signature = tuple(sorted(str(c) for c in constraint_set))
            if set_signature not in seen_sets:
                seen_sets.add(set_signature)
                unique_sets.append(constraint_set)

        logger.info(
            f"Generated {len(unique_sets)} unique relaxation strategies"
        )
        return unique_sets

    def _create_constraint_variations(
        self, constraints: List[object]
    ) -> List[List[object]]:
        """
        Create variations of difficult constraints to improve matching.

        Args:
            constraints: Original constraints

        Returns:
            List of constraint sets with variations
        """
        variation_sets = []

        for i, constraint in enumerate(constraints):
            constraint_type = self._get_constraint_type(constraint)

            if constraint_type in self.relaxation_strategies:
                # Create variations for this constraint
                variations = self.relaxation_strategies[constraint_type](
                    constraint
                )

                if variations:
                    # Replace original constraint with each variation
                    for variation in variations:
                        new_set = constraints.copy()
                        new_set[i] = variation
                        variation_sets.append(new_set)

        return variation_sets

    def _relax_statistical_constraint(self, constraint: object) -> List[object]:
        """
        Create relaxed variations of statistical constraints.

        Statistical constraints often fail due to:
        - Outdated numbers
        - Rounding differences
        - Different measurement units
        """
        variations = []
        constraint_text = str(constraint)

        # Extract numbers from constraint
        import re

        numbers = re.findall(r"\d+(?:\.\d+)?", constraint_text)

        for number_str in numbers:
            try:
                number = float(number_str)

                # Create range variations (+/- 10%, 20%, 50%)
                for tolerance in [0.1, 0.2, 0.5]:
                    lower = number * (1 - tolerance)
                    upper = number * (1 + tolerance)

                    # Replace exact number with range
                    relaxed_text = constraint_text.replace(
                        number_str, f"between {lower:.0f} and {upper:.0f}"
                    )

                    variations.append(
                        self._create_relaxed_constraint(
                            constraint, relaxed_text
                        )
                    )

                # Create "approximately" version
                approx_text = constraint_text.replace(
                    number_str, f"approximately {number_str}"
                )
                variations.append(
                    self._create_relaxed_constraint(constraint, approx_text)
                )

            except ValueError:
                continue

        return variations[:3]  # Limit to avoid too many variations

    def _relax_comparison_constraint(self, constraint: object) -> List[object]:
        """
        Create relaxed variations of comparison constraints.

        Comparison constraints often fail due to:
        - Relative terms are context-dependent
        - "Times more" calculations are complex
        - Baseline comparisons may be unclear
        """
        variations = []
        constraint_text = str(constraint).lower()

        # Replace strict comparisons with looser ones
        relaxation_mappings = {
            "times more": "significantly more",
            "times larger": "much larger",
            "times bigger": "much bigger",
            "exactly": "approximately",
            "must be": "should be",
            "is the": "is among the",
            "largest": "one of the largest",
            "smallest": "one of the smallest",
            "highest": "among the highest",
            "lowest": "among the lowest",
        }

        for strict_term, relaxed_term in relaxation_mappings.items():
            if strict_term in constraint_text:
                relaxed_text = constraint_text.replace(
                    strict_term, relaxed_term
                )
                variations.append(
                    self._create_relaxed_constraint(constraint, relaxed_text)
                )

        # Remove comparison altogether - focus on the main entity/property
        comparison_indicators = [
            "more than",
            "less than",
            "compared to",
            "relative to",
        ]
        for indicator in comparison_indicators:
            if indicator in constraint_text:
                # Extract the part before the comparison
                parts = constraint_text.split(indicator)
                if len(parts) > 1:
                    main_part = parts[0].strip()
                    variations.append(
                        self._create_relaxed_constraint(constraint, main_part)
                    )

        return variations[:3]

    def _relax_temporal_constraint(self, constraint: object) -> List[object]:
        """
        Create relaxed variations of temporal constraints.

        Temporal constraints often fail due to:
        - Exact dates vs approximate dates
        - Different calendar systems
        - Founding vs incorporation dates
        """
        variations = []
        constraint_text = str(constraint)

        # Extract years
        import re

        years = re.findall(r"\b(19\d{2}|20\d{2})\b", constraint_text)

        for year_str in years:
            year = int(year_str)

            # Create decade ranges
            decade_start = (year // 10) * 10
            decade_text = constraint_text.replace(year_str, f"{decade_start}s")
            variations.append(
                self._create_relaxed_constraint(constraint, decade_text)
            )

            # Create +/- ranges
            for range_years in [1, 2, 5]:
                range_text = constraint_text.replace(
                    year_str,
                    f"between {year - range_years} and {year + range_years}",
                )
                variations.append(
                    self._create_relaxed_constraint(constraint, range_text)
                )

        # Replace exact temporal terms with approximate ones
        temporal_relaxations = {
            "founded in": "founded around",
            "established in": "established around",
            "created in": "created around",
            "started in": "started around",
            "exactly": "approximately",
        }

        for exact_term, relaxed_term in temporal_relaxations.items():
            if exact_term in constraint_text.lower():
                relaxed_text = constraint_text.replace(exact_term, relaxed_term)
                variations.append(
                    self._create_relaxed_constraint(constraint, relaxed_text)
                )

        return variations[:3]

    def _relax_property_constraint(self, constraint: object) -> List[object]:
        """
        Create relaxed variations of property constraints.

        Property constraints can be relaxed by:
        - Making specific properties more general
        - Allowing alternative phrasings
        - Focusing on key attributes
        """
        variations = []
        constraint_text = str(constraint).lower()

        # Make specific properties more general
        property_generalizations = {
            "multinational": "international",
            "conglomerate": "large company",
            "corporation": "company",
            "subsidiary": "part of",
            "headquarters": "based",
            "founded": "established",
            "specialized": "focused",
            "leading": "major",
        }

        for specific, general in property_generalizations.items():
            if specific in constraint_text:
                relaxed_text = constraint_text.replace(specific, general)
                variations.append(
                    self._create_relaxed_constraint(constraint, relaxed_text)
                )

        # Remove adjectives to make constraints less specific
        adjective_patterns = [
            r"\b(very|extremely|highly|most|largest|biggest|smallest)\s+",
            r"\b(major|minor|primary|secondary|main|key)\s+",
        ]

        for pattern in adjective_patterns:
            import re

            if re.search(pattern, constraint_text):
                relaxed_text = re.sub(pattern, "", constraint_text)
                variations.append(
                    self._create_relaxed_constraint(constraint, relaxed_text)
                )

        return variations[:2]

    def _create_relaxed_constraint(
        self, original_constraint: object, relaxed_text: str
    ) -> object:
        """
        Create a new constraint object with relaxed text.

        This is a helper method that preserves the constraint structure
        while updating the constraint value/text.
        """
        # Try to create a copy of the constraint with updated text
        if hasattr(original_constraint, "__dict__"):
            # Create a copy of the constraint object
            import copy

            relaxed_constraint = copy.deepcopy(original_constraint)

            # Update the constraint value/description
            if hasattr(relaxed_constraint, "value"):
                relaxed_constraint.value = relaxed_text
            elif hasattr(relaxed_constraint, "description"):
                relaxed_constraint.description = relaxed_text
            elif hasattr(relaxed_constraint, "text"):
                relaxed_constraint.text = relaxed_text

            return relaxed_constraint
        else:
            # If we can't copy the constraint, return a simple string representation
            return relaxed_text

    def _get_constraint_type(self, constraint: object) -> str:
        """Extract constraint type from constraint object."""
        if hasattr(constraint, "type"):
            if hasattr(constraint.type, "value"):
                return constraint.type.value
            else:
                return str(constraint.type)
        elif hasattr(constraint, "constraint_type"):
            return constraint.constraint_type
        else:
            # Try to infer from constraint text
            constraint_text = str(constraint).lower()

            if any(
                word in constraint_text
                for word in ["name", "called", "known as"]
            ):
                return "NAME_PATTERN"
            elif any(
                word in constraint_text
                for word in ["location", "country", "city"]
            ):
                return "LOCATION"
            elif any(
                word in constraint_text
                for word in ["year", "date", "when", "time"]
            ):
                return "TEMPORAL"
            elif any(
                word in constraint_text
                for word in ["number", "count", "amount"]
            ):
                return "STATISTIC"
            elif any(
                word in constraint_text
                for word in ["event", "happened", "occurred"]
            ):
                return "EVENT"
            elif any(
                word in constraint_text
                for word in ["than", "more", "less", "compared"]
            ):
                return "COMPARISON"
            else:
                return "PROPERTY"

    def analyze_relaxation_impact(
        self,
        original_constraints: List[object],
        relaxed_constraints: List[object],
    ) -> Dict:
        """
        Analyze the impact of constraint relaxation.

        Returns analysis of what was changed and the expected impact.
        """
        analysis = {
            "original_count": len(original_constraints),
            "relaxed_count": len(relaxed_constraints),
            "constraints_removed": len(original_constraints)
            - len(relaxed_constraints),
            "constraint_changes": [],
            "priority_impact": "low",
            "recommendation": "",
        }

        # Check what types of constraints were removed/modified
        original_types = [
            self._get_constraint_type(c) for c in original_constraints
        ]
        relaxed_types = [
            self._get_constraint_type(c) for c in relaxed_constraints
        ]

        removed_types = []
        for orig_type in original_types:
            if orig_type not in relaxed_types:
                removed_types.append(orig_type)

        # Assess impact based on what was removed
        high_impact_types = {"NAME_PATTERN", "EXISTENCE", "LOCATION"}
        medium_impact_types = {"TEMPORAL", "EVENT", "PROPERTY"}

        if any(t in removed_types for t in high_impact_types):
            analysis["priority_impact"] = "high"
            analysis["recommendation"] = (
                "High-priority constraints removed. Results may be less accurate."
            )
        elif any(t in removed_types for t in medium_impact_types):
            analysis["priority_impact"] = "medium"
            analysis["recommendation"] = (
                "Medium-priority constraints removed. Check results carefully."
            )
        else:
            analysis["priority_impact"] = "low"
            analysis["recommendation"] = (
                "Low-priority constraints removed. Results should remain accurate."
            )

        analysis["removed_constraint_types"] = removed_types

        return analysis
