"""
Activity session metrics (EAV) and resolved attribute schema — Layer 1.

See: .claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md
"""
from __future__ import annotations

from decimal import Decimal
from typing import Any, Dict, List, Optional, Sequence


class ActivitySessionMetricsError(Exception):
    """Raised by Layer 1; routers map to HTTP (404/400)."""

    def __init__(self, status_code: int, detail: str):
        self.status_code = status_code
        self.detail = detail
        super().__init__(detail)


def _effective_training_category(
    cur, training_category: Optional[str], training_type_id: Optional[int]
) -> Optional[str]:
    if training_category:
        return training_category.strip() or None
    if training_type_id is None:
        return None
    cur.execute("SELECT category FROM training_types WHERE id = %s", (training_type_id,))
    row = cur.fetchone()
    if row and row.get("category"):
        return row["category"]
    return None


def merge_parameter_schema_rows(
    category_rows: Sequence[Dict[str, Any]],
    type_rows: Sequence[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """
    Pure merge: category assignments + type assignments → sorted schema list.
    Row shapes match SELECTs in resolve_activity_attribute_schema (cat_sort / typ_* aliases).
    """
    merged: Dict[int, Dict[str, Any]] = {}

    for r in category_rows:
        pid = r["training_parameter_id"]
        merged[pid] = {
            "training_parameter_id": pid,
            "key": r["key"],
            "name_de": r["name_de"],
            "name_en": r["name_en"],
            "param_category": r["param_category"],
            "data_type": r["data_type"],
            "unit": r["unit"],
            "validation_rules": r["validation_rules"] or {},
            "source_field": r["source_field"],
            "sort_order": r["cat_sort"],
            "required": bool(r["cat_required"]),
            "ui_group": r["cat_ui_group"],
        }

    for r in type_rows:
        pid = r["training_parameter_id"]
        base = merged.get(pid)
        if base is None:
            merged[pid] = {
                "training_parameter_id": pid,
                "key": r["key"],
                "name_de": r["name_de"],
                "name_en": r["name_en"],
                "param_category": r["param_category"],
                "data_type": r["data_type"],
                "unit": r["unit"],
                "validation_rules": r["validation_rules"] or {},
                "source_field": r["source_field"],
                "sort_order": r["typ_sort"] if r["typ_sort"] is not None else 0,
                "required": bool(r["typ_required"]) if r["typ_required"] is not None else False,
                "ui_group": r["typ_ui_group"],
            }
        else:
            if r["typ_sort"] is not None:
                base["sort_order"] = r["typ_sort"]
            if r["typ_required"] is not None:
                base["required"] = bool(r["typ_required"])
            if r["typ_ui_group"] is not None:
                base["ui_group"] = r["typ_ui_group"]

    out = list(merged.values())
    out.sort(key=lambda x: (x["sort_order"], x["key"]))
    return out


def resolve_activity_attribute_schema(
    cur,
    training_category: Optional[str],
    training_type_id: Optional[int],
) -> List[Dict[str, Any]]:
    """
    Merged parameter definitions for UI / validation (category base + type overrides/additions).
    Sorted by sort_order, then key.
    """
    cat = _effective_training_category(cur, training_category, training_type_id)
    category_rows: List[Dict[str, Any]] = []
    type_rows: List[Dict[str, Any]] = []

    if cat:
        cur.execute(
            """
            SELECT
                tcp.training_parameter_id,
                tcp.sort_order AS cat_sort,
                tcp.required AS cat_required,
                tcp.ui_group AS cat_ui_group,
                tp.key, tp.name_de, tp.name_en, tp.category AS param_category,
                tp.data_type, tp.unit, tp.validation_rules, tp.source_field
            FROM training_category_parameter tcp
            JOIN training_parameters tp ON tp.id = tcp.training_parameter_id
            WHERE tcp.training_category = %s AND tp.is_active = true
            """,
            (cat,),
        )
        category_rows = list(cur.fetchall())

    if training_type_id is not None:
        cur.execute(
            """
            SELECT
                ttp.training_parameter_id,
                ttp.sort_order AS typ_sort,
                ttp.required AS typ_required,
                ttp.ui_group AS typ_ui_group,
                tp.key, tp.name_de, tp.name_en, tp.category AS param_category,
                tp.data_type, tp.unit, tp.validation_rules, tp.source_field
            FROM training_type_parameter ttp
            JOIN training_parameters tp ON tp.id = ttp.training_parameter_id
            WHERE ttp.training_type_id = %s AND tp.is_active = true
            """,
            (training_type_id,),
        )
        type_rows = list(cur.fetchall())

    return merge_parameter_schema_rows(category_rows, type_rows)


def _validation_rules_dict(raw: Any) -> Dict[str, Any]:
    if isinstance(raw, dict):
        return raw
    return {}


def _validate_single_value(data_type: str, value: Any, rules: Dict[str, Any]) -> None:
    if data_type == "integer":
        if not isinstance(value, int) or isinstance(value, bool):
            raise ActivitySessionMetricsError(400, f"Erwartet integer, erhalten: {type(value).__name__}")
        if "min" in rules and value < rules["min"]:
            raise ActivitySessionMetricsError(400, f"Wert unter min ({rules['min']})")
        if "max" in rules and value > rules["max"]:
            raise ActivitySessionMetricsError(400, f"Wert über max ({rules['max']})")
    elif data_type == "float":
        if isinstance(value, bool) or not isinstance(value, (int, float, Decimal)):
            raise ActivitySessionMetricsError(400, f"Erwartet Zahl, erhalten: {type(value).__name__}")
        v = float(value)
        if "min" in rules and v < float(rules["min"]):
            raise ActivitySessionMetricsError(400, f"Wert unter min ({rules['min']})")
        if "max" in rules and v > float(rules["max"]):
            raise ActivitySessionMetricsError(400, f"Wert über max ({rules['max']})")
    elif data_type == "string":
        if not isinstance(value, str):
            raise ActivitySessionMetricsError(400, f"Erwartet string, erhalten: {type(value).__name__}")
        if rules.get("not_empty") and not value.strip():
            raise ActivitySessionMetricsError(400, "Leerer String nicht erlaubt")
        if "max_length" in rules and len(value) > int(rules["max_length"]):
            raise ActivitySessionMetricsError(400, f"String zu lang (max {rules['max_length']})")
        allowed = rules.get("allowed_values")
        if allowed and value not in allowed:
            raise ActivitySessionMetricsError(400, "Wert nicht in erlaubter Menge")
    elif data_type == "boolean":
        if not isinstance(value, bool):
            raise ActivitySessionMetricsError(400, f"Erwartet boolean, erhalten: {type(value).__name__}")
    else:
        raise ActivitySessionMetricsError(400, f"Unbekannter data_type: {data_type}")


def _row_value_tuple(data_type: str, value: Any) -> tuple:
    if data_type == "integer":
        return (None, int(value), None, None)
    if data_type == "float":
        return (float(value), None, None, None)
    if data_type == "string":
        return (None, None, str(value), None)
    if data_type == "boolean":
        return (None, None, None, bool(value))
    raise ValueError(data_type)


def fetch_activity_session_metrics(cur, activity_log_id: str) -> List[Dict[str, Any]]:
    cur.execute(
        """
        SELECT
            m.id,
            m.activity_log_id,
            m.training_parameter_id,
            m.value_num,
            m.value_int,
            m.value_text,
            m.value_bool,
            tp.key,
            tp.data_type,
            tp.unit
        FROM activity_session_metrics m
        JOIN training_parameters tp ON tp.id = m.training_parameter_id
        WHERE m.activity_log_id = %s
        ORDER BY tp.key
        """,
        (activity_log_id,),
    )
    rows = cur.fetchall()
    out: List[Dict[str, Any]] = []
    for r in rows:
        dt = r["data_type"]
        if dt == "integer":
            val = int(r["value_int"]) if r["value_int"] is not None else None
        elif dt == "float":
            val = float(r["value_num"]) if r["value_num"] is not None else None
        elif dt == "string":
            val = r["value_text"]
        else:
            val = r["value_bool"]
        out.append(
            {
                "training_parameter_id": r["training_parameter_id"],
                "key": r["key"],
                "data_type": dt,
                "unit": r["unit"],
                "value": val,
            }
        )
    return out


def replace_activity_session_metrics(
    cur,
    profile_id: str,
    activity_log_id: str,
    metrics: Sequence[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """
    Full replace of EAV rows for this session. metrics: [{ "parameter_key": str, "value": ... }, ...]
    """
    cur.execute(
        """
        SELECT id, profile_id, training_category, training_type_id
        FROM activity_log WHERE id = %s
        """,
        (activity_log_id,),
    )
    row = cur.fetchone()
    if not row or str(row["profile_id"]) != str(profile_id):
        raise ActivitySessionMetricsError(404, "Aktivität nicht gefunden")

    schema = resolve_activity_attribute_schema(
        cur, row.get("training_category"), row.get("training_type_id")
    )
    by_key = {s["key"]: s for s in schema}
    payload_keys = set()
    for item in metrics:
        raw_k = item.get("parameter_key")
        if raw_k is None or not str(raw_k).strip():
            raise ActivitySessionMetricsError(400, "parameter_key fehlt")
        k = str(raw_k).strip()
        if k not in by_key:
            raise ActivitySessionMetricsError(400, f"Unbekannter oder nicht zugewiesener Parameter: {k}")
        payload_keys.add(k)

    for s in schema:
        if s["required"] and s["key"] not in payload_keys:
            raise ActivitySessionMetricsError(400, f"Pflichtfeld fehlt: {s['key']}")

    cur.execute(
        "DELETE FROM activity_session_metrics WHERE activity_log_id = %s",
        (activity_log_id,),
    )

    for item in metrics:
        k = str(item["parameter_key"]).strip()
        spec = by_key[k]
        rules = _validation_rules_dict(spec["validation_rules"])
        _validate_single_value(spec["data_type"], item.get("value"), rules)
        vn, vi, vt, vb = _row_value_tuple(spec["data_type"], item["value"])
        cur.execute(
            """
            INSERT INTO activity_session_metrics (
                activity_log_id, training_parameter_id,
                value_num, value_int, value_text, value_bool, updated_at
            ) VALUES (%s, %s, %s, %s, %s, %s, NOW())
            """,
            (activity_log_id, spec["training_parameter_id"], vn, vi, vt, vb),
        )

    return fetch_activity_session_metrics(cur, activity_log_id)


def get_activity_session_logical_unit(cur, profile_id: str, activity_log_id: str) -> Dict[str, Any]:
    cur.execute("SELECT * FROM activity_log WHERE id = %s", (activity_log_id,))
    row = cur.fetchone()
    if not row or str(row["profile_id"]) != str(profile_id):
        raise ActivitySessionMetricsError(404, "Aktivität nicht gefunden")

    header = dict(row)
    schema = resolve_activity_attribute_schema(
        cur, header.get("training_category"), header.get("training_type_id")
    )
    metrics = fetch_activity_session_metrics(cur, activity_log_id)
    return {
        "header": header,
        "schema": schema,
        "metrics": metrics,
    }


def enrich_sessions_with_metrics(cur, sessions: List[Dict[str, Any]]) -> None:
    """Mutates each session dict: adds key 'session_metrics' (list) when sessions non-empty."""
    if not sessions:
        return
    ids = [str(s["id"]) for s in sessions if s.get("id")]
    if not ids:
        return
    ph = ",".join(["%s"] * len(ids))
    cur.execute(
        f"""
        SELECT
            m.activity_log_id,
            tp.key,
            tp.data_type,
            tp.unit,
            m.value_num,
            m.value_int,
            m.value_text,
            m.value_bool
        FROM activity_session_metrics m
        JOIN training_parameters tp ON tp.id = m.training_parameter_id
        WHERE m.activity_log_id IN ({ph})
        ORDER BY m.activity_log_id, tp.key
        """,
        ids,
    )
    by_act: Dict[str, List[Dict[str, Any]]] = {}
    for r in cur.fetchall():
        aid = str(r["activity_log_id"])
        dt = r["data_type"]
        if dt == "integer":
            val = int(r["value_int"]) if r["value_int"] is not None else None
        elif dt == "float":
            val = float(r["value_num"]) if r["value_num"] is not None else None
        elif dt == "string":
            val = r["value_text"]
        else:
            val = r["value_bool"]
        by_act.setdefault(aid, []).append(
            {"key": r["key"], "data_type": dt, "unit": r["unit"], "value": val}
        )
    for s in sessions:
        aid = str(s.get("id"))
        s["session_metrics"] = by_act.get(aid, [])
