"""
Layer 1 entry: resolve a multi-dimensional training evaluation from a template.

Pure calculation orchestration — no DB, no HTTP, no formatting for KI/charts.
"""

from __future__ import annotations

from collections import defaultdict
from typing import Any, Dict, List, Mapping, Optional

from data_layer.training_profile.algorithms.registry import get_algorithm
from data_layer.training_profile.models import (
    CalculationTemplate,
    DimensionResult,
    DimensionSpec,
    TrainingBaseProfile,
    TrainingEvaluationResult,
)


def _required_inputs_present(
    activity_inputs: Mapping[str, Any], keys: tuple[str, ...]
) -> tuple[bool, List[str]]:
    missing: List[str] = []
    for k in keys:
        if k not in activity_inputs or activity_inputs[k] is None:
            missing.append(k)
    return (len(missing) == 0, missing)


def _confidence_level(total_dims: int, dims_with_any_missing: int) -> str:
    if total_dims == 0:
        return "insufficient"
    if dims_with_any_missing == 0:
        return "high"
    if dims_with_any_missing >= total_dims:
        return "insufficient"
    if dims_with_any_missing == 1:
        return "medium"
    return "low"


def _filter_dimensions(
    template: CalculationTemplate, base_profile: Optional[TrainingBaseProfile]
) -> tuple[DimensionSpec, ...]:
    if base_profile is None or base_profile.allowed_dimension_keys is None:
        return template.dimensions
    allowed = base_profile.allowed_dimension_keys
    return tuple(d for d in template.dimensions if d.key in allowed)


def resolve_training_evaluation(
    *,
    activity_inputs: Mapping[str, Any],
    template: CalculationTemplate,
    base_profile: Optional[TrainingBaseProfile] = None,
    include_trace: bool = False,
) -> TrainingEvaluationResult:
    """
    Run all template dimensions, aggregate Focus Area contributions, attach evidence.

    activity_inputs: flat dict (e.g. avg_hr, duration_min, distance_km) supplied by caller.
    """
    dimensions = _filter_dimensions(template, base_profile)
    dimension_results: List[DimensionResult] = []
    contributions: Dict[str, float] = defaultdict(float)
    evidence: Dict[str, Any] = {
        "dimensions_total": len(dimensions),
        "inputs_keys": sorted(activity_inputs.keys()),
    }
    trace: Optional[Dict[str, Any]] = {} if include_trace else None

    dims_with_missing = 0

    for spec in dimensions:
        ok, missing = _required_inputs_present(activity_inputs, spec.inputs)
        if not ok:
            dims_with_missing += 1
            dimension_results.append(
                DimensionResult(
                    dimension_key=spec.key,
                    algorithm_id=spec.algorithm_id,
                    raw_score=0.0,
                    normalized_score=0.0,
                    missing_inputs=list(missing),
                    evidence={"skipped": True, "reason": "required_inputs_missing"},
                )
            )
            if trace is not None:
                trace[spec.key] = {"skipped": True, "missing": missing}
            continue

        algo = get_algorithm(spec.algorithm_id)
        slice_inputs = {k: activity_inputs[k] for k in spec.inputs}
        run = algo(inputs=slice_inputs, params=dict(spec.params))

        if run.missing_inputs:
            dims_with_missing += 1

        dimension_results.append(
            DimensionResult(
                dimension_key=spec.key,
                algorithm_id=spec.algorithm_id,
                raw_score=run.raw_score,
                normalized_score=run.normalized_score,
                missing_inputs=list(run.missing_inputs),
                evidence={"algorithm_detail": run.detail},
            )
        )

        for m in spec.maps_to:
            contributions[m.focus_area_key] += run.normalized_score * m.weight

        if trace is not None:
            trace[spec.key] = {
                "inputs": dict(slice_inputs),
                "params": dict(spec.params),
                "run": {
                    "raw_score": run.raw_score,
                    "normalized_score": run.normalized_score,
                    "missing_inputs": run.missing_inputs,
                    "detail": run.detail,
                },
                "maps_to": [(x.focus_area_key, x.weight) for x in spec.maps_to],
            }

    conf = _confidence_level(len(dimensions), dims_with_missing)
    evidence["dimensions_with_missing_or_failed"] = dims_with_missing

    return TrainingEvaluationResult(
        template_id=template.id,
        template_version=template.version,
        base_profile_key=base_profile.key if base_profile else None,
        dimension_results=dimension_results,
        focus_area_contributions=dict(contributions),
        confidence=conf,
        evidence=evidence,
        trace=trace,
    )


def resolve_for_base_profile(
    *,
    activity_inputs: Mapping[str, Any],
    base_profile_key: str,
    include_trace: bool = False,
) -> TrainingEvaluationResult:
    """Convenience: load profile + default template from registries."""
    from data_layer.training_profile.profiles.registry import get_training_base_profile
    from data_layer.training_profile.templates.registry import get_calculation_template

    profile = get_training_base_profile(base_profile_key)
    template = get_calculation_template(profile.default_template_id)
    return resolve_training_evaluation(
        activity_inputs=activity_inputs,
        template=template,
        base_profile=profile,
        include_trace=include_trace,
    )
