# Training Type Profiles – Technisches Design

**Issue:** #15
**Status:** Technical Design (Final)
**Erstellt:** 2026-03-23

---

## Design-Prinzipien

1. **Maximale Flexibilität:** Neue Parameter ohne Code-Änderung hinzufügbar
2. **Regel-basiert:** Verschiedene Operator-Typen (`>=`, `<=`, `between`, `in`)
3. **Optional:** Kein Parameter ist Pflicht - jeder Trainingstyp wählt relevante Parameter
4. **Erweiterbar:** Neue Regel-Typen einfach implementierbar
5. **Typsicher:** Validierung auf Parameter-Typ-Ebene

---

## 1. Parameter-Registry (Zentrale Definition)

### DB-Tabelle: `training_parameters`

```sql
CREATE TABLE training_parameters (
    id SERIAL PRIMARY KEY,
    key VARCHAR(50) UNIQUE NOT NULL,  -- z.B. "duration_min", "avg_hr"
    name_de VARCHAR(100) NOT NULL,
    name_en VARCHAR(100) NOT NULL,
    category VARCHAR(50) NOT NULL,     -- "physical", "physiological", "subjective"
    data_type VARCHAR(20) NOT NULL,    -- "integer", "float", "string", "boolean"
    unit VARCHAR(20),                   -- "min", "bpm", "km", "kcal", etc.
    description_de TEXT,
    description_en TEXT,
    source_field VARCHAR(100),          -- Mapping zu activity_log Spalte
    validation_rules JSONB,             -- Min/Max/Enum für Validierung
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Beispiel-Daten:
INSERT INTO training_parameters (key, name_de, name_en, category, data_type, unit, source_field, validation_rules) VALUES
('duration_min', 'Dauer', 'Duration', 'physical', 'integer', 'min', 'duration_min', '{"min": 0, "max": 600}'),
('avg_hr', 'Durchschnittspuls', 'Average Heart Rate', 'physiological', 'integer', 'bpm', 'hr_avg', '{"min": 30, "max": 220}'),
('max_hr', 'Maximalpuls', 'Max Heart Rate', 'physiological', 'integer', 'bpm', 'hr_max', '{"min": 40, "max": 220}'),
('min_hr', 'Minimalpuls', 'Min Heart Rate', 'physiological', 'integer', 'bpm', 'hr_min', '{"min": 30, "max": 200}'),
('distance_km', 'Distanz', 'Distance', 'physical', 'float', 'km', 'distance_km', '{"min": 0, "max": 200}'),
('kcal_active', 'Aktive Kalorien', 'Active Calories', 'physical', 'integer', 'kcal', 'kcal_active', '{"min": 0, "max": 5000}'),
('rpe', 'RPE (Anstrengung)', 'RPE (Perceived Exertion)', 'subjective', 'integer', 'scale', 'rpe', '{"min": 1, "max": 10}'),
('pace_min_per_km', 'Pace', 'Pace', 'physical', 'float', 'min/km', 'pace_min_per_km', '{"min": 2, "max": 20}'),
('elevation_gain', 'Höhenmeter', 'Elevation Gain', 'physical', 'integer', 'm', 'elevation_gain', '{"min": 0, "max": 5000}'),
('avg_power', 'Durchschnittsleistung', 'Average Power', 'physiological', 'integer', 'W', 'avg_power', '{"min": 0, "max": 1000}'),
('cadence', 'Trittfrequenz', 'Cadence', 'physical', 'integer', 'rpm', 'cadence', '{"min": 0, "max": 200}'),
('temperature_celsius', 'Temperatur', 'Temperature', 'environmental', 'float', '°C', 'temperature_celsius', '{"min": -30, "max": 50}'),
('humidity_percent', 'Luftfeuchtigkeit', 'Humidity', 'environmental', 'integer', '%', 'humidity_percent', '{"min": 0, "max": 100}');
```

**Vorteile:**
- Neue Parameter einfach per INSERT hinzufügbar
- Zentrale Validierung
- UI kann dynamisch generiert werden (Admin-Interface)
- Mehrsprachig

---

## 2. Regel-System (Flexible Operators)

### Operator-Typen

```python
OPERATORS = {
    "gte": {"symbol": "≥", "name": "Größer oder gleich", "applies_to": ["integer", "float"]},
    "lte": {"symbol": "≤", "name": "Kleiner oder gleich", "applies_to": ["integer", "float"]},
    "gt": {"symbol": ">", "name": "Größer als", "applies_to": ["integer", "float"]},
    "lt": {"symbol": "<", "name": "Kleiner als", "applies_to": ["integer", "float"]},
    "eq": {"symbol": "=", "name": "Gleich", "applies_to": ["integer", "float", "string", "boolean"]},
    "neq": {"symbol": "≠", "name": "Ungleich", "applies_to": ["integer", "float", "string", "boolean"]},
    "between": {"symbol": "⟷", "name": "Zwischen", "applies_to": ["integer", "float"]},
    "in": {"symbol": "∈", "name": "Enthalten in", "applies_to": ["string", "integer"]},
    "not_in": {"symbol": "∉", "name": "Nicht enthalten in", "applies_to": ["string", "integer"]},
}
```

### Regel-Definition (JSONB in training_types.profile)

```json
{
  "version": "2.0",
  "created_at": "2026-03-23T10:00:00Z",
  "updated_at": "2026-03-23T10:00:00Z",

  // ━━━ REGEL-SETS (Verschiedene Dimensionen) ━━━
  "rule_sets": {

    // 1. MINIMUM REQUIREMENTS (Quality Gates)
    "minimum_requirements": {
      "enabled": true,
      "pass_strategy": "weighted_score",  // "all_must_pass" | "weighted_score" | "at_least_n"
      "pass_threshold": 0.6,
      "rules": [
        {
          "parameter": "duration_min",
          "operator": "gte",
          "value": 15,
          "weight": 5,
          "optional": false,
          "reason": "Mindestdauer für signifikante Trainingswirkung"
        },
        {
          "parameter": "avg_hr",
          "operator": "gte",
          "value": 100,
          "weight": 3,
          "optional": false,
          "reason": "Puls muss für Ausdauerreiz erhöht sein"
        },
        {
          "parameter": "max_hr",
          "operator": "gte",
          "value": 120,
          "weight": 1,
          "optional": true,
          "reason": "Maximalpuls zeigt echte Belastungsspitze"
        }
      ]
    },

    // 2. INTENSITY ZONES (HF-Zonen)
    "intensity_zones": {
      "enabled": true,
      "method": "percentage_max_hr",
      "zones": [
        {
          "id": "zone_1_regen",
          "name": "Regeneration",
          "rules": [
            {
              "parameter": "avg_hr_percent",  // Berechnet als (avg_hr / user_max_hr) * 100
              "operator": "between",
              "value": [50, 60],
              "weight": 1
            }
          ],
          "effect": "Aktive Erholung, Grundlagenausdauer 1",
          "color": "#4CAF50",
          "target_duration_min": 30,
          "metabolic_focus": ["aerobic_oxidation", "fat_metabolism"],
          "lactate_range": [0.5, 2.0]
        },
        {
          "id": "zone_2_ga1",
          "name": "Grundlagenausdauer",
          "rules": [
            {
              "parameter": "avg_hr_percent",
              "operator": "between",
              "value": [60, 70],
              "weight": 1
            }
          ],
          "effect": "Aerobe Kapazität, Fettstoffwechsel optimieren",
          "color": "#2196F3",
          "target_duration_min": 45,
          "metabolic_focus": ["aerobic_oxidation", "mitochondrial_density"],
          "lactate_range": [2.0, 3.5]
        },
        {
          "id": "zone_3_tempo",
          "name": "Tempo/Entwicklung",
          "rules": [
            {
              "parameter": "avg_hr_percent",
              "operator": "between",
              "value": [70, 80],
              "weight": 1
            }
          ],
          "effect": "VO2max-Training, Laktattoleranz",
          "color": "#FF9800",
          "target_duration_min": 20,
          "metabolic_focus": ["aerobic_glycolytic", "vo2max_improvement"],
          "lactate_range": [3.5, 5.5]
        },
        {
          "id": "zone_4_threshold",
          "name": "Schwellentraining",
          "rules": [
            {
              "parameter": "avg_hr_percent",
              "operator": "between",
              "value": [80, 90],
              "weight": 1
            }
          ],
          "effect": "Anaerobe Schwelle verschieben",
          "color": "#F44336",
          "target_duration_min": 10,
          "metabolic_focus": ["anaerobic_lactic", "lactate_threshold"],
          "lactate_range": [5.5, 8.0]
        },
        {
          "id": "zone_5_max",
          "name": "Maximale Intensität",
          "rules": [
            {
              "parameter": "avg_hr_percent",
              "operator": "between",
              "value": [90, 100],
              "weight": 1
            }
          ],
          "effect": "Maximalkraft, Sprint, HIIT",
          "color": "#9C27B0",
          "target_duration_min": 5,
          "metabolic_focus": ["anaerobic_alactic", "phosphocreatine"],
          "lactate_range": [8.0, 15.0]
        }
      ],
      "zone_evaluation": {
        "require_min_time_in_zone_percent": 70,
        "allow_multiple_zones": true,
        "primary_zone_weight": 1.0,
        "secondary_zone_weight": 0.5
      }
    },

    // 3. TRAINING EFFECTS (Fähigkeiten-Entwicklung)
    "training_effects": {
      "enabled": true,
      "conditional_effects": [
        {
          "condition": {
            "parameter": "avg_hr_percent",
            "operator": "between",
            "value": [60, 80]
          },
          "effects": {
            "primary_abilities": [
              {"category": "konditionell", "ability": "ausdauer", "intensity": 5}
            ],
            "secondary_abilities": [
              {"category": "psychisch", "ability": "willenskraft", "intensity": 3}
            ]
          }
        },
        {
          "condition": {
            "parameter": "avg_hr_percent",
            "operator": "gte",
            "value": 80
          },
          "effects": {
            "primary_abilities": [
              {"category": "konditionell", "ability": "schnelligkeit", "intensity": 5},
              {"category": "konditionell", "ability": "ausdauer", "intensity": 3}
            ],
            "secondary_abilities": [
              {"category": "psychisch", "ability": "stressresistenz", "intensity": 4}
            ]
          }
        }
      ],
      "default_effects": {
        "primary_abilities": [
          {"category": "konditionell", "ability": "ausdauer", "intensity": 4}
        ],
        "secondary_abilities": [
          {"category": "koordinativ", "ability": "rhythmus", "intensity": 2}
        ]
      },
      "muscle_groups": ["legs_posterior", "legs_anterior", "core"],
      "energy_systems": ["aerobic_oxidative", "anaerobic_lactic"]
    },

    // 4. PERIODIZATION (Frequenz & Erholung)
    "periodization": {
      "enabled": true,
      "frequency": {
        "per_week_min": 2,
        "per_week_max": 5,
        "per_week_optimal": 3,
        "consecutive_days_max": 2
      },
      "recovery": {
        "hours_before_same_type": 48,
        "hours_before_high_intensity": 24,
        "conditional_recovery": [
          {
            "condition": {
              "parameter": "rpe",
              "operator": "gte",
              "value": 8
            },
            "recovery_hours": 72
          }
        ]
      },
      "progression": {
        "beginner_duration_min": 20,
        "intermediate_duration_min": 30,
        "advanced_duration_min": 45,
        "volume_increase_percent_per_week": 10,
        "intensity_increase_percent_per_week": 5
      },
      "deload": {
        "every_n_weeks": 4,
        "volume_reduction_percent": 40,
        "intensity_reduction_percent": 20
      }
    },

    // 5. PERFORMANCE INDICATORS (KPIs & Benchmarks)
    "performance_indicators": {
      "enabled": true,
      "primary_metrics": ["pace_min_per_km", "avg_hr", "distance_km"],
      "secondary_metrics": ["kcal_per_km", "cadence", "elevation_gain"],
      "benchmarks": {
        "beginner": {
          "rules": [
            {"parameter": "pace_min_per_km", "operator": "gte", "value": 7.0},
            {"parameter": "distance_km", "operator": "lte", "value": 5.0}
          ]
        },
        "intermediate": {
          "rules": [
            {"parameter": "pace_min_per_km", "operator": "between", "value": [5.5, 7.0]},
            {"parameter": "distance_km", "operator": "between", "value": [5.0, 10.0]}
          ]
        },
        "advanced": {
          "rules": [
            {"parameter": "pace_min_per_km", "operator": "lte", "value": 5.5},
            {"parameter": "distance_km", "operator": "gte", "value": 10.0}
          ]
        }
      },
      "trend_analysis": {
        "lookback_activities": 12,
        "improvement_threshold_percent": 5,
        "decline_threshold_percent": -5
      }
    },

    // 6. SAFETY & WARNINGS
    "safety": {
      "enabled": true,
      "warnings": [
        {
          "parameter": "max_hr",
          "operator": "gte",
          "value": 180,
          "severity": "high",
          "message": "Maximalpuls sehr hoch - prüfe Belastung"
        },
        {
          "parameter": "duration_min",
          "operator": "gte",
          "value": 120,
          "severity": "medium",
          "message": "Sehr lange Einheit - ausreichend Erholung einplanen"
        },
        {
          "parameter": "rpe",
          "operator": "gte",
          "value": 9,
          "severity": "medium",
          "message": "Sehr hohe subjektive Anstrengung - 72h Pause empfohlen"
        }
      ],
      "contraindications": [
        "acute_injury_lower_body",
        "cardiovascular_episode_recent"
      ],
      "environmental_limits": {
        "temperature_celsius_min": -5,
        "temperature_celsius_max": 32,
        "humidity_percent_max": 85
      }
    },

    // 7. AI CONTEXT (für KI-Prompts)
    "ai_context": {
      "enabled": true,
      "evaluation_focus": [
        "Pace-Entwicklung über Distanz",
        "Herzfrequenz-Stabilität während Training",
        "Subjektive Anstrengung vs. objektive Daten"
      ],
      "questions_to_ask": [
        "Wie fühlte sich dein Tempo an?",
        "Hattest du Atembeschwerden?",
        "Wie schnell hat sich dein Puls nach dem Training normalisiert?"
      ],
      "comparison_metrics": [
        "pace_min_per_km",
        "avg_hr_at_same_pace",
        "rpe_at_same_distance"
      ]
    }
  }
}
```

---

## 3. Beispiel: Meditation (Andere Regel-Richtung)

```json
{
  "version": "2.0",
  "rule_sets": {
    "minimum_requirements": {
      "enabled": true,
      "pass_strategy": "all_must_pass",
      "rules": [
        {
          "parameter": "duration_min",
          "operator": "gte",
          "value": 10,
          "weight": 5,
          "reason": "Mindestens 10 Minuten für mentale Wirkung"
        },
        {
          "parameter": "avg_hr",
          "operator": "lte",  // ⬅ UMGEKEHRT: Kleiner oder gleich
          "value": 70,
          "weight": 4,
          "reason": "Puls sollte im Ruhezustand sein"
        },
        {
          "parameter": "max_hr",
          "operator": "lte",
          "value": 80,
          "weight": 2,
          "reason": "Keine Belastungsspitzen während Meditation"
        },
        {
          "parameter": "rpe",
          "operator": "lte",
          "value": 3,
          "weight": 3,
          "reason": "Meditation sollte nicht anstrengend sein"
        }
      ]
    },
    "intensity_zones": {
      "enabled": false  // Meditation hat keine HF-Zonen
    },
    "training_effects": {
      "enabled": true,
      "default_effects": {
        "primary_abilities": [
          {"category": "psychisch", "ability": "konzentration", "intensity": 5},
          {"category": "psychisch", "ability": "stressresistenz", "intensity": 4}
        ],
        "secondary_abilities": [
          {"category": "kognitiv", "ability": "aufmerksamkeit", "intensity": 4},
          {"category": "psychisch", "ability": "selbstvertrauen", "intensity": 3}
        ]
      }
    },
    "periodization": {
      "enabled": true,
      "frequency": {
        "per_week_min": 3,
        "per_week_max": 7,
        "per_week_optimal": 5,
        "consecutive_days_max": 7  // Meditation kann täglich
      },
      "recovery": {
        "hours_before_same_type": 0  // Keine Erholungszeit nötig
      }
    }
  }
}
```

---

## 4. Backend: Rule Engine (Flexibel & Erweiterbar)

### 4.1 Core: Rule Evaluator

```python
from typing import Any, Dict, List, Optional
from datetime import datetime

class RuleEvaluator:
    """
    Generischer Regel-Evaluator für beliebige Parameter und Operatoren.
    """

    OPERATORS = {
        "gte": lambda actual, expected: actual >= expected,
        "lte": lambda actual, expected: actual <= expected,
        "gt": lambda actual, expected: actual > expected,
        "lt": lambda actual, expected: actual < expected,
        "eq": lambda actual, expected: actual == expected,
        "neq": lambda actual, expected: actual != expected,
        "between": lambda actual, expected: expected[0] <= actual <= expected[1],
        "in": lambda actual, expected: actual in expected,
        "not_in": lambda actual, expected: actual not in expected,
    }

    @classmethod
    def evaluate_rule(cls, rule: Dict, activity: Dict, parameters_registry: Dict) -> Dict:
        """
        Evaluiert eine einzelne Regel gegen eine Aktivität.

        Args:
            rule: {"parameter": str, "operator": str, "value": Any, "weight": int, ...}
            activity: Die Aktivitäts-Daten
            parameters_registry: Mapping parameter_key -> config

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

        # Parameter aus Registry holen
        param_config = parameters_registry.get(param_key)
        if not param_config:
            return {
                "passed": False,
                "error": f"Unknown parameter: {param_key}"
            }

        # Wert aus Aktivität extrahieren
        source_field = param_config.get("source_field", param_key)
        actual_value = activity.get(source_field)

        # Optional und nicht vorhanden? → Pass
        if optional and actual_value is None:
            return {
                "passed": True,
                "actual_value": None,
                "expected_value": expected_value,
                "parameter": param_key,
                "operator": operator,
                "reason": "Optional parameter, not provided",
                "weight": weight,
                "skipped": True
            }

        # Required aber nicht vorhanden? → Fail
        if actual_value is None:
            return {
                "passed": False,
                "actual_value": None,
                "expected_value": expected_value,
                "parameter": param_key,
                "operator": operator,
                "reason": reason or "Required parameter missing",
                "weight": weight
            }

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

        try:
            passed = operator_func(actual_value, expected_value)
        except Exception as e:
            return {
                "passed": False,
                "error": f"Evaluation error: {str(e)}"
            }

        return {
            "passed": passed,
            "actual_value": actual_value,
            "expected_value": expected_value,
            "parameter": param_key,
            "operator": operator,
            "reason": reason,
            "weight": weight
        }

    @classmethod
    def evaluate_rule_set(
        cls,
        rule_set: Dict,
        activity: Dict,
        parameters_registry: Dict
    ) -> Dict:
        """
        Evaluiert ein komplettes Regel-Set (z.B. minimum_requirements).

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

        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 = []
        total_weight = 0
        passed_weight = 0

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

            if result.get("skipped"):
                continue

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

            if result["passed"]:
                passed_weight += weight

        # Score berechnen
        score = passed_weight / total_weight if total_weight > 0 else 1.0

        # Pass-Strategie anwenden
        if pass_strategy == "all_must_pass":
            passed = all(r["passed"] for r in rule_results if not r.get("skipped"))
        elif pass_strategy == "weighted_score":
            passed = score >= pass_threshold
        else:
            passed = False

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

### 4.2 Master Evaluator

```python
class TrainingProfileEvaluator:
    """
    Hauptklasse für vollständige Aktivitäts-Bewertung.
    """

    def __init__(self, parameters_registry: Dict):
        self.parameters_registry = parameters_registry
        self.rule_evaluator = RuleEvaluator()

    def evaluate_activity(
        self,
        activity: Dict,
        training_type_profile: Dict,
        context: Optional[Dict] = None
    ) -> Dict:
        """
        Vollständige Bewertung einer Aktivität gegen Trainingstyp-Profil.

        Args:
            activity: Aktivitäts-Daten
            training_type_profile: Das JSONB-Profil des Trainingstyps
            context: {
                "user_profile": {...},
                "recent_activities": [...],
                "historical_activities": [...]
            }

        Returns:
            {
                "evaluated_at": ISO timestamp,
                "profile_version": str,
                "rule_set_results": {
                    "minimum_requirements": {...},
                    "intensity_zones": {...},
                    "training_effects": {...},
                    "periodization": {...},
                    "performance_indicators": {...},
                    "safety": {...}
                },
                "overall_score": float (0-1),
                "quality_label": str,
                "recommendations": [str],
                "warnings": [str]
            }
        """
        if not training_type_profile:
            return self._create_unvalidated_result()

        rule_sets = training_type_profile.get("rule_sets", {})
        context = context or {}

        results = {
            "evaluated_at": datetime.now().isoformat(),
            "profile_version": training_type_profile.get("version", "unknown"),
            "rule_set_results": {}
        }

        # 1. Minimum Requirements
        if "minimum_requirements" in rule_sets:
            results["rule_set_results"]["minimum_requirements"] = \
                self.rule_evaluator.evaluate_rule_set(
                    rule_sets["minimum_requirements"],
                    activity,
                    self.parameters_registry
                )

        # 2. Intensity Zones
        if "intensity_zones" in rule_sets:
            results["rule_set_results"]["intensity_zones"] = \
                self._evaluate_intensity_zones(
                    rule_sets["intensity_zones"],
                    activity,
                    context.get("user_profile", {})
                )

        # 3. Training Effects
        if "training_effects" in rule_sets:
            results["rule_set_results"]["training_effects"] = \
                self._evaluate_training_effects(
                    rule_sets["training_effects"],
                    activity,
                    results["rule_set_results"].get("intensity_zones")
                )

        # 4. Periodization
        if "periodization" in rule_sets:
            results["rule_set_results"]["periodization"] = \
                self._evaluate_periodization(
                    rule_sets["periodization"],
                    activity,
                    context.get("recent_activities", [])
                )

        # 5. Performance Indicators
        if "performance_indicators" in rule_sets:
            results["rule_set_results"]["performance_indicators"] = \
                self._evaluate_performance(
                    rule_sets["performance_indicators"],
                    activity,
                    context.get("historical_activities", [])
                )

        # 6. Safety Warnings
        if "safety" in rule_sets:
            results["rule_set_results"]["safety"] = \
                self._evaluate_safety(
                    rule_sets["safety"],
                    activity
                )

        # Overall Score & Quality Label
        overall_score = self._calculate_overall_score(results["rule_set_results"])
        results["overall_score"] = overall_score
        results["quality_label"] = self._get_quality_label(overall_score)

        # Recommendations & Warnings
        results["recommendations"] = self._generate_recommendations(results)
        results["warnings"] = self._collect_warnings(results)

        return results

    def _calculate_overall_score(self, rule_set_results: Dict) -> float:
        """Berechnet gewichteten Gesamt-Score."""
        weights = {
            "minimum_requirements": 0.4,
            "intensity_zones": 0.2,
            "training_effects": 0.1,
            "periodization": 0.2,
            "performance_indicators": 0.1
        }

        total_score = 0.0
        total_weight = 0.0

        for rule_set_name, weight in weights.items():
            result = rule_set_results.get(rule_set_name)
            if result and result.get("enabled"):
                score = result.get("score", 0.5)
                total_score += score * weight
                total_weight += weight

        return round(total_score / total_weight, 2) if total_weight > 0 else 0.5

    def _get_quality_label(self, score: float) -> str:
        """Konvertiert Score zu Label."""
        if score >= 0.9:
            return "excellent"
        elif score >= 0.7:
            return "good"
        elif score >= 0.5:
            return "acceptable"
        else:
            return "poor"

    # ... Weitere Helper-Methoden für Intensity, Effects, etc. ...
```

---

## 5. DB-Schema

```sql
-- Training Parameters Registry
CREATE TABLE training_parameters (
    id SERIAL PRIMARY KEY,
    key VARCHAR(50) UNIQUE NOT NULL,
    name_de VARCHAR(100) NOT NULL,
    name_en VARCHAR(100) NOT NULL,
    category VARCHAR(50) NOT NULL,
    data_type VARCHAR(20) NOT NULL,
    unit VARCHAR(20),
    description_de TEXT,
    description_en TEXT,
    source_field VARCHAR(100),
    validation_rules JSONB,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Training Types (Erweitert)
ALTER TABLE training_types ADD COLUMN profile JSONB DEFAULT NULL;

-- Activity Log (Erweitert)
ALTER TABLE activity_log ADD COLUMN evaluation JSONB DEFAULT NULL;
ALTER TABLE activity_log ADD COLUMN quality_label VARCHAR(20);  -- Index für schnelle Queries
ALTER TABLE activity_log ADD COLUMN overall_score FLOAT;        -- Index für Sortierung

CREATE INDEX idx_activity_quality ON activity_log(quality_label) WHERE quality_label IS NOT NULL;
CREATE INDEX idx_activity_score ON activity_log(overall_score) WHERE overall_score IS NOT NULL;
```

---

## 6. API-Endpoints

### Admin-Endpoints (Parameter-Verwaltung)

```python
@router.get("/api/admin/training-parameters")
def list_training_parameters(session: dict = Depends(require_admin)):
    """Liste aller verfügbaren Parameter."""
    with get_db() as conn:
        cur = get_cursor(conn)
        cur.execute("SELECT * FROM training_parameters WHERE is_active = true ORDER BY category, key")
        return [r2d(r) for r in cur.fetchall()]

@router.post("/api/admin/training-parameters")
def create_training_parameter(data: dict, session: dict = Depends(require_admin)):
    """Neuen Parameter anlegen."""
    # Validierung + INSERT
    pass
```

### Evaluation-Endpoints

```python
@router.post("/api/activity/{activity_id}/evaluate")
def evaluate_activity_endpoint(
    activity_id: str,
    session: dict = Depends(require_auth)
):
    """
    Evaluiert eine Aktivität gegen ihr Trainingstyp-Profil.
    """
    pid = session['profile_id']

    with get_db() as conn:
        cur = get_cursor(conn)

        # Activity laden
        cur.execute("SELECT * FROM activity_log WHERE id = %s AND profile_id = %s", (activity_id, pid))
        activity = r2d(cur.fetchone())

        if not activity:
            raise HTTPException(404, "Activity not found")

        # Training Type Profil laden
        cur.execute("SELECT profile FROM training_types WHERE id = %s", (activity['training_type_id'],))
        row = cur.fetchone()
        profile = row['profile'] if row else None

        if not profile:
            raise HTTPException(400, "Training type has no profile configured")

        # Context laden (recent + historical activities)
        context = load_evaluation_context(pid, activity['date'], cur)

        # Parameters Registry laden
        cur.execute("SELECT * FROM training_parameters WHERE is_active = true")
        parameters = {r['key']: r2d(r) for r in cur.fetchall()}

        # Evaluieren
        evaluator = TrainingProfileEvaluator(parameters)
        evaluation_result = evaluator.evaluate_activity(activity, profile, context)

        # Evaluation speichern
        cur.execute("""
            UPDATE activity_log
            SET evaluation = %s,
                quality_label = %s,
                overall_score = %s
            WHERE id = %s
        """, (
            Json(evaluation_result),
            evaluation_result['quality_label'],
            evaluation_result['overall_score'],
            activity_id
        ))

        logger.info(f"[EVALUATION] Activity {activity_id}: {evaluation_result['quality_label']} ({evaluation_result['overall_score']})")

    return evaluation_result


@router.post("/api/activity/import-csv")
def import_activity_csv(...):
    """
    CSV-Import mit automatischer Evaluation.
    """
    # ... Import-Logik ...

    for entry in imported_entries:
        # INSERT Activity
        activity_id = insert_activity(entry)

        # Auto-Evaluate wenn Training Type Profil existiert
        try:
            evaluate_activity_endpoint(activity_id, session)
        except Exception as e:
            logger.warning(f"[EVALUATION] Failed for {activity_id}: {e}")
            # Nicht-blockierend: Import erfolgreich auch ohne Evaluation

    return {"imported": len(imported_entries)}
```

---

## 7. Erweiterbarkeit: Neue Parameter hinzufügen

### Beispiel: Neue Parameter "Cadence" und "Stride Length"

```sql
-- 1. Parameter registrieren
INSERT INTO training_parameters (key, name_de, name_en, category, data_type, unit, source_field, validation_rules) VALUES
('cadence', 'Schrittfrequenz', 'Cadence', 'physical', 'integer', 'spm', 'cadence', '{"min": 120, "max": 220}'),
('stride_length', 'Schrittlänge', 'Stride Length', 'physical', 'float', 'm', 'stride_length', '{"min": 0.5, "max": 2.5}');

-- 2. Activity Log Spalten hinzufügen (optional, wenn nicht schon vorhanden)
ALTER TABLE activity_log ADD COLUMN cadence INTEGER;
ALTER TABLE activity_log ADD COLUMN stride_length FLOAT;

-- 3. Training Type Profil erweitern (per Admin-UI oder SQL)
UPDATE training_types SET profile = jsonb_set(
    profile,
    '{rule_sets,minimum_requirements,rules}',
    profile->'rule_sets'->'minimum_requirements'->'rules' || '[
        {
            "parameter": "cadence",
            "operator": "gte",
            "value": 160,
            "weight": 2,
            "optional": true,
            "reason": "Optimale Laufökonomie bei 160+ spm"
        }
    ]'::jsonb
)
WHERE name_de = 'Laufen';
```

**Fertig!** Kein Backend-Code-Change nötig. Rule Engine erkennt neuen Parameter automatisch.

---

## 8. Future: Automatische Trainingstyp-Klassifizierung

**Feature Request (nach Implementierung):**

Statt nur Namen-Matching: Klassifizierung basierend auf erreichten Parametern.

```python
def classify_activity_by_profile(activity: Dict, training_types: List[Dict]) -> str:
    """
    Findet passenden Trainingstyp basierend auf Parametern.

    Beispiel:
        Laufen mit avg_hr_percent 60-70% → "Laufen: Grundlagenausdauer"
        Laufen mit avg_hr_percent 80-90% → "Laufen: Schwellentraining"
    """
    best_match = None
    best_score = 0

    for training_type in training_types:
        profile = training_type.get('profile')
        if not profile:
            continue

        # Prüfe Minimum Requirements
        min_req_result = evaluate_rule_set(profile['rule_sets']['minimum_requirements'], activity)

        # Prüfe Intensity Zone
        zone_result = evaluate_intensity_zones(profile['rule_sets']['intensity_zones'], activity)

        # Score: Wie gut passt die Aktivität zu diesem Profil?
        match_score = (min_req_result['score'] * 0.6) + (zone_result['score'] * 0.4)

        if match_score > best_score:
            best_score = match_score
            best_match = {
                "training_type_id": training_type['id'],
                "training_type_name": training_type['name_de'],
                "variant": zone_result.get('dominant_zone'),
                "match_score": match_score
            }

    return best_match
```

**UI:**
```
Importierte Aktivität: "Outdoor Run"

Automatische Klassifizierung:
✅ Laufen: Grundlagenausdauer (Match: 92%)
   - Dauer: 45min ✓
   - Ø HF: 135 bpm (68% Max-HF) ✓
   - Zone: Grundlagenausdauer ✓

Alternative Matches:
   Laufen: Tempo (Match: 45%)

[Übernehmen]  [Manual ändern]
```

---

## Zusammenfassung

### Flexibilität erreicht durch:

1. **Parameter-Registry** - Zentrale, erweiterbare Parameter-Definition
2. **Operator-System** - Verschiedene Vergleichsoperatoren (>=, <=, between, in)
3. **Regel-Strategie** - all_must_pass, weighted_score, at_least_n
4. **Optional-Flags** - Nicht jeder Parameter ist Pflicht
5. **Konditionale Regeln** - "Wenn X, dann Y"
6. **JSONB-Storage** - Beliebige Struktur ohne Schema-Changes

### Neue Parameter hinzufügen:

```sql
-- Nur 1 INSERT nötig, kein Code-Change!
INSERT INTO training_parameters (...) VALUES (...);
```

### Neue Trainingstypen anlegen:

```json
// Profile einfach kopieren & anpassen
// Meditation: avg_hr <= 70
// HIIT: avg_hr >= 160, duration_min <= 30
// Yoga: rpe <= 4, duration_min >= 30
```

---

**Bereit für Implementierung?** 🚀

**Geschätzter Aufwand:**
- Phase 1 (Foundation): 6-8h
- Phase 2 (Admin-UI): 4-6h
- Phase 3 (User-UI): 4-6h
- **Total: 14-20h**
