# src/fmatch/algorithm_selection/rule_engine.py
"""
Rule-based algorithm selection engine for intelligent matching strategy selection.
Evaluates YAML-defined rules to select the best algorithm for given data characteristics.
"""

import yaml
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass
import re

log = logging.getLogger(__name__)


@dataclass
class AlgorithmRule:
    """Represents a single algorithm selection rule."""

    name: str
    description: str
    conditions: Dict[str, Any]
    preferred_algorithms: List[str]
    weight_modifier: float = 1.0
    priority: int = 0

    def evaluate(self, column_profile: Dict[str, Any]) -> Tuple[bool, float]:
        """
        Evaluate if this rule matches the given column profile.
        Returns (matches, confidence_score).
        """
        total_conditions = len(self.conditions)
        matched_conditions = 0

        for condition_key, condition_value in self.conditions.items():
            if self._evaluate_condition(condition_key, condition_value, column_profile):
                matched_conditions += 1

        matches = matched_conditions == total_conditions
        confidence = (
            matched_conditions / total_conditions if total_conditions > 0 else 0
        )

        return matches, confidence

    def _evaluate_condition(self, key: str, expected, profile: Dict[str, Any]) -> bool:
        """Evaluate a single condition against the profile."""
        # Handle nested keys (e.g., "metrics.avg_length")
        keys = key.split(".")
        value = profile

        for k in keys:
            if isinstance(value, dict) and k in value:
                value = value[k]
            else:
                return False

        # Handle different condition types
        if isinstance(expected, dict):
            # Range conditions
            if "min" in expected or "max" in expected:
                if not isinstance(value, (int, float)):
                    return False
                if "min" in expected and value < expected["min"]:
                    return False
                if "max" in expected and value > expected["max"]:
                    return False
                return True

            # Pattern matching
            elif "pattern" in expected:
                if not isinstance(value, str):
                    return False
                pattern = re.compile(expected["pattern"], re.IGNORECASE)
                return bool(pattern.search(value))

            # In list
            elif "in" in expected:
                return value in expected["in"]

            # Not in list
            elif "not_in" in expected:
                return value not in expected["not_in"]

            # Contains (for lists)
            elif "contains" in expected:
                if not isinstance(value, (list, tuple)):
                    return False
                return expected["contains"] in value

        # Direct equality
        else:
            return value == expected


class AlgorithmSelectionEngine:
    """Engine for evaluating algorithm selection rules."""

    def __init__(self, rules_dir: Optional[Path] = None):
        self.rules_dir = rules_dir or Path(__file__).parent / "rules"
        self.rules: List[AlgorithmRule] = []
        self._load_rules()

    def _load_rules(self):
        """Load all YAML rule files from the rules directory."""
        self.rules.clear()

        if not self.rules_dir.exists():
            log.warning(f"Algorithm rules directory not found: {self.rules_dir}")
            return

        for yaml_file in self.rules_dir.glob("*.yaml"):
            try:
                with open(yaml_file, "r", encoding="utf-8") as f:
                    data = yaml.safe_load(f)

                if "rules" in data:
                    for rule_data in data["rules"]:
                        rule = AlgorithmRule(
                            name=rule_data["name"],
                            description=rule_data.get("description", ""),
                            conditions=rule_data.get("conditions", {}),
                            preferred_algorithms=rule_data.get(
                                "preferred_algorithms", []
                            ),
                            weight_modifier=rule_data.get("weight_modifier", 1.0),
                            priority=rule_data.get("priority", 0),
                        )
                        self.rules.append(rule)

                log.info(
                    f"Loaded {len(data.get('rules', []))} rules from {yaml_file.name}"
                )

            except Exception as e:
                log.error(f"Failed to load rules from {yaml_file}: {e}")

        # Sort rules by priority (higher priority first)
        self.rules.sort(key=lambda r: r.priority, reverse=True)
        log.info(f"Total algorithm selection rules loaded: {len(self.rules)}")

    def select_algorithms(
        self, column_profile: Dict[str, Any], available_algorithms: List[str] = None
    ) -> List[Tuple[str, float]]:
        """
        Select algorithms based on the column profile.
        Returns list of (algorithm, score) tuples, sorted by score descending.
        """
        if available_algorithms is None:
            from fmatch.core.engine import ALGORITHM_MAP

            available_algorithms = list(ALGORITHM_MAP.keys())

        algorithm_scores: Dict[str, float] = {
            algo: 0.5 for algo in available_algorithms
        }
        rules_matched = []

        # Evaluate all rules
        for rule in self.rules:
            matches, confidence = rule.evaluate(column_profile)

            if matches:
                rules_matched.append((rule, confidence))

                # Apply rule's algorithm preferences
                for i, algo in enumerate(rule.preferred_algorithms):
                    if algo in algorithm_scores:
                        # Higher position in preferred list = higher boost
                        position_weight = 1.0 - (i * 0.1)  # 1.0, 0.9, 0.8, etc.
                        boost = confidence * rule.weight_modifier * position_weight
                        algorithm_scores[algo] = min(
                            1.0, algorithm_scores[algo] + boost
                        )

                # Penalize algorithms not in the preferred list
                for algo in available_algorithms:
                    if algo not in rule.preferred_algorithms:
                        penalty = confidence * rule.weight_modifier * 0.2
                        algorithm_scores[algo] = max(
                            0.0, algorithm_scores[algo] - penalty
                        )

        # Log matched rules for debugging
        if rules_matched:
            log.debug(f"Matched rules: {[(r.name, c) for r, c in rules_matched]}")

        # Convert to sorted list
        results = [(algo, score) for algo, score in algorithm_scores.items()]
        results.sort(key=lambda x: x[1], reverse=True)

        return results

    def get_best_algorithm(
        self, column_profile: Dict[str, Any], available_algorithms: List[str] = None
    ) -> Optional[str]:
        """Get the single best algorithm for the given profile."""
        results = self.select_algorithms(column_profile, available_algorithms)
        return results[0][0] if results else None

    def reload_rules(self):
        """Reload rules from disk."""
        log.info("Reloading algorithm selection rules...")
        self._load_rules()


# Global engine instance
_ALGORITHM_SELECTION_ENGINE: Optional[AlgorithmSelectionEngine] = None


def get_algorithm_selection_engine() -> AlgorithmSelectionEngine:
    """Get or create the global algorithm selection engine."""
    global _ALGORITHM_SELECTION_ENGINE
    if _ALGORITHM_SELECTION_ENGINE is None:
        _ALGORITHM_SELECTION_ENGINE = AlgorithmSelectionEngine()
    return _ALGORITHM_SELECTION_ENGINE


def select_algorithm_for_data(
    column_profile: Dict[str, Any], available_algorithms: List[str] = None
) -> str:
    """
    Convenience function to select the best algorithm for given data characteristics.
    """
    engine = get_algorithm_selection_engine()
    return engine.get_best_algorithm(column_profile, available_algorithms)
