"""
Training Type Profiles - Rule Engine
Flexible rule evaluation system for activity quality assessment.

Issue: #15
Date: 2026-03-23
"""
from typing import Any, Dict, List, Optional, Callable
from datetime import datetime
import logging

logger = logging.getLogger(__name__)


class RuleEvaluator:
    """
    Generic rule evaluator for arbitrary parameters and operators.

    Supports flexible rule definitions with various operators:
    - gte, lte, gt, lt: Comparison operators
    - eq, neq: Equality operators
    - between: Range checks
    - in, not_in: Set membership
    """

    # Operator definitions
    OPERATORS: Dict[str, Callable[[Any, Any], bool]] = {
        "gte": lambda actual, expected: actual is not None and actual >= expected,
        "lte": lambda actual, expected: actual is not None and actual <= expected,
        "gt": lambda actual, expected: actual is not None and actual > expected,
        "lt": lambda actual, expected: actual is not None and actual < expected,
        "eq": lambda actual, expected: actual == expected,
        "neq": lambda actual, expected: actual != expected,
        "between": lambda actual, expected: actual is not None and expected[0] <= actual <= expected[1],
        "in": lambda actual, expected: actual in expected,
        "not_in": lambda actual, expected: actual not in expected,
    }

    OPERATOR_SYMBOLS = {
        "gte": "≥",
        "lte": "≤",
        "gt": ">",
        "lt": "<",
        "eq": "=",
        "neq": "≠",
        "between": "⟷",
        "in": "∈",
        "not_in": "∉",
    }

    @classmethod
    def evaluate_rule(
        cls,
        rule: Dict,
        activity: Dict,
        parameters_registry: Dict[str, Dict]
    ) -> Dict:
        """
        Evaluates a single rule against an activity.

        Args:
            rule: {
                "parameter": str,
                "operator": str,
                "value": Any,
                "weight": int,
                "optional": bool,
                "reason": str
            }
            activity: Activity data dictionary
            parameters_registry: Mapping parameter_key -> config

        Returns:
            {
                "passed": bool,
                "actual_value": Any,
                "expected_value": Any,
                "parameter": str,
                "operator": str,
                "operator_symbol": str,
                "reason": str,
                "weight": int,
                "skipped": bool (optional),
                "error": str (optional)
            }
        """
        param_key = rule.get("parameter")
        operator = rule.get("operator")
        expected_value = rule.get("value")
        weight = rule.get("weight", 1)
        reason = rule.get("reason", "")
        optional = rule.get("optional", False)

        # Get parameter configuration
        param_config = parameters_registry.get(param_key)
        if not param_config:
            return {
                "passed": False,
                "parameter": param_key,
                "error": f"Unknown parameter: {param_key}"
            }

        # Extract value from activity
        source_field = param_config.get("source_field", param_key)
        actual_value = activity.get(source_field)

        # Optional and not provided? → Pass
        if optional and actual_value is None:
            return {
                "passed": True,
                "actual_value": None,
                "expected_value": expected_value,
                "parameter": param_key,
                "operator": operator,
                "operator_symbol": cls.OPERATOR_SYMBOLS.get(operator, operator),
                "reason": "Optional parameter not provided",
                "weight": weight,
                "skipped": True
            }

        # Required but not provided? → Fail
        if actual_value is None:
            return {
                "passed": False,
                "actual_value": None,
                "expected_value": expected_value,
                "parameter": param_key,
                "operator": operator,
                "operator_symbol": cls.OPERATOR_SYMBOLS.get(operator, operator),
                "reason": reason or "Required parameter missing",
                "weight": weight
            }

        # Apply operator
        operator_func = cls.OPERATORS.get(operator)
        if not operator_func:
            return {
                "passed": False,
                "parameter": param_key,
                "error": f"Unknown operator: {operator}"
            }

        try:
            passed = operator_func(actual_value, expected_value)
        except Exception as e:
            logger.error(f"[RULE-ENGINE] Error evaluating rule {param_key}: {e}")
            return {
                "passed": False,
                "parameter": param_key,
                "error": f"Evaluation error: {str(e)}"
            }

        return {
            "passed": passed,
            "actual_value": actual_value,
            "expected_value": expected_value,
            "parameter": param_key,
            "operator": operator,
            "operator_symbol": cls.OPERATOR_SYMBOLS.get(operator, operator),
            "reason": reason,
            "weight": weight
        }

    @classmethod
    def evaluate_rule_set(
        cls,
        rule_set: Dict,
        activity: Dict,
        parameters_registry: Dict[str, Dict]
    ) -> Dict:
        """
        Evaluates a complete rule set (e.g., minimum_requirements).

        Args:
            rule_set: {
                "enabled": bool,
                "pass_strategy": str,
                "pass_threshold": float,
                "rules": [...]
            }
            activity: Activity data
            parameters_registry: Parameter configurations

        Returns:
            {
                "enabled": bool,
                "passed": bool,
                "score": float (0-1),
                "rule_results": [...],
                "pass_strategy": str,
                "pass_threshold": float,
                "failed_rules": [...]
            }
        """
        if not rule_set.get("enabled", False):
            return {
                "enabled": False,
                "passed": True,
                "score": 1.0,
                "rule_results": [],
                "failed_rules": []
            }

        rules = rule_set.get("rules", [])
        pass_strategy = rule_set.get("pass_strategy", "weighted_score")
        pass_threshold = rule_set.get("pass_threshold", 0.6)

        rule_results = []
        failed_rules = []
        total_weight = 0
        passed_weight = 0

        # Evaluate each rule
        for rule in rules:
            result = cls.evaluate_rule(rule, activity, parameters_registry)
            rule_results.append(result)

            if result.get("skipped"):
                continue

            if result.get("error"):
                logger.warning(f"[RULE-ENGINE] Rule error: {result['error']}")
                continue

            weight = result.get("weight", 1)
            total_weight += weight

            if result["passed"]:
                passed_weight += weight
            else:
                failed_rules.append(result)

        # Calculate score
        score = passed_weight / total_weight if total_weight > 0 else 1.0

        # Apply pass strategy
        if pass_strategy == "all_must_pass":
            passed = all(
                r["passed"] for r in rule_results
                if not r.get("skipped") and not r.get("error")
            )
        elif pass_strategy == "weighted_score":
            passed = score >= pass_threshold
        elif pass_strategy == "at_least_n":
            n = rule_set.get("at_least_n", 1)
            passed_count = sum(
                1 for r in rule_results
                if r["passed"] and not r.get("skipped")
            )
            passed = passed_count >= n
        else:
            passed = False
            logger.warning(f"[RULE-ENGINE] Unknown pass strategy: {pass_strategy}")

        return {
            "enabled": True,
            "passed": passed,
            "score": round(score, 2),
            "rule_results": rule_results,
            "failed_rules": failed_rules,
            "pass_strategy": pass_strategy,
            "pass_threshold": pass_threshold
        }


class IntensityZoneEvaluator:
    """
    Evaluates heart rate zones and time distribution.
    """

    @staticmethod
    def evaluate(
        zone_config: Dict,
        activity: Dict,
        user_profile: Dict
    ) -> Dict:
        """
        Evaluates which HR zone the activity was in.

        Args:
            zone_config: intensity_zones configuration
            activity: Activity data (with hr_avg)
            user_profile: User profile (with hf_max)

        Returns:
            {
                "enabled": bool,
                "dominant_zone": str,
                "avg_hr_percent": float,
                "zone_color": str,
                "zone_effect": str,
                "duration_quality": float (0-1),
                "recommendation": str
            }
        """
        if not zone_config.get("enabled", False):
            return {"enabled": False}

        avg_hr = activity.get("hr_avg")
        user_max_hr = user_profile.get("hf_max", 180)  # Default 180 if not set

        if not avg_hr or not user_max_hr:
            return {
                "enabled": True,
                "dominant_zone": "unknown",
                "avg_hr_percent": None,
                "recommendation": "Herzfrequenz-Daten fehlen"
            }

        avg_hr_percent = (avg_hr / user_max_hr) * 100

        # Find matching zone
        zones = zone_config.get("zones", [])
        dominant_zone = None

        for zone in zones:
            zone_rules = zone.get("rules", [])
            for rule in zone_rules:
                if rule["parameter"] == "avg_hr_percent":
                    min_percent, max_percent = rule["value"]
                    if min_percent <= avg_hr_percent <= max_percent:
                        dominant_zone = zone
                        break
            if dominant_zone:
                break

        if not dominant_zone:
            return {
                "enabled": True,
                "dominant_zone": "out_of_range",
                "avg_hr_percent": round(avg_hr_percent, 1),
                "recommendation": "Herzfrequenz außerhalb definierter Zonen"
            }

        # Check duration quality
        duration = activity.get("duration_min", 0)
        target_duration = dominant_zone.get("target_duration_min", 30)
        duration_quality = min(duration / target_duration, 1.0) if target_duration > 0 else 1.0

        recommendation = f"Training in Zone '{dominant_zone['name']}' (Effekt: {dominant_zone['effect']})."
        if duration < target_duration:
            recommendation += f" Für optimale Wirkung: {target_duration}min empfohlen."

        return {
            "enabled": True,
            "dominant_zone": dominant_zone.get("id"),
            "dominant_zone_name": dominant_zone.get("name"),
            "avg_hr_percent": round(avg_hr_percent, 1),
            "zone_color": dominant_zone.get("color"),
            "zone_effect": dominant_zone.get("effect"),
            "duration_quality": round(duration_quality, 2),
            "target_duration_min": target_duration,
            "actual_duration_min": duration,
            "recommendation": recommendation
        }


class TrainingEffectsEvaluator:
    """
    Evaluates which abilities are trained by the activity.
    """

    @staticmethod
    def evaluate(
        effects_config: Dict,
        activity: Dict,
        intensity_zone_result: Optional[Dict] = None
    ) -> Dict:
        """
        Evaluates training effects (abilities trained).

        Args:
            effects_config: training_effects configuration
            activity: Activity data
            intensity_zone_result: Result from intensity zone evaluation

        Returns:
            {
                "enabled": bool,
                "abilities_trained": [...],
                "total_training_load": float
            }
        """
        if not effects_config.get("enabled", False):
            return {"enabled": False}

        abilities_trained = []

        # Use default effects if no conditional matching
        default_effects = effects_config.get("default_effects", {})
        primary_abilities = default_effects.get("primary_abilities", [])
        secondary_abilities = default_effects.get("secondary_abilities", [])

        # Calculate quality factor (simplified for now)
        quality_factor = 1.0

        # Primary abilities
        for ability in primary_abilities:
            abilities_trained.append({
                "category": ability["category"],
                "ability": ability["ability"],
                "intensity": ability["intensity"],
                "quality": quality_factor,
                "contribution": ability["intensity"] * quality_factor,
                "type": "primary"
            })

        # Secondary abilities
        for ability in secondary_abilities:
            abilities_trained.append({
                "category": ability["category"],
                "ability": ability["ability"],
                "intensity": ability["intensity"],
                "quality": quality_factor * 0.7,  # Secondary = 70%
                "contribution": ability["intensity"] * quality_factor * 0.7,
                "type": "secondary"
            })

        total_training_load = sum(a["contribution"] for a in abilities_trained)

        return {
            "enabled": True,
            "abilities_trained": abilities_trained,
            "total_training_load": round(total_training_load, 2),
            "metabolic_focus": effects_config.get("metabolic_focus", []),
            "muscle_groups": effects_config.get("muscle_groups", [])
        }
