"""
Pro-Widget-Konfiguration im Dashboard-Layout (v1).

Nur ausgewählte Widget-IDs dürfen nicht-leere config haben; bekannte Keys werden validiert.
"""
from __future__ import annotations

import json
import math
import re
from typing import Any

MAX_WIDGET_CONFIG_JSON_BYTES = 3072

WIDGETS_ALLOWING_CONFIG: frozenset[str] = frozenset({
    "body_overview",
    "activity_overview",
    "kpi_board",
    "quick_capture",
    "trend_kcal_weight",
    "nutrition_detail_charts",
    "recovery_charts_panel",
})

_QUICK_CAPTURE_KEYS: frozenset[str] = frozenset({
    "show_weight",
    "show_resting_hr",
    "show_hrv",
    "show_vo2_max",
})

_KPI_TILE_FIXED: frozenset[str] = frozenset({"body_fat", "avg_kcal"})
_KPI_REF_TILE_RE = re.compile(r"^ref:[a-z0-9_]{1,64}$")


def _config_json_size_bytes(config: dict[str, Any]) -> int:
    return len(json.dumps(config, sort_keys=True, ensure_ascii=False).encode("utf-8"))


def validate_widget_entry_config(widget_id: str, raw: Any) -> dict[str, Any]:
    if raw is None:
        return {}
    if not isinstance(raw, dict):
        raise ValueError(f"Widget {widget_id}: config muss ein Objekt sein")
    if _config_json_size_bytes(raw) > MAX_WIDGET_CONFIG_JSON_BYTES:
        raise ValueError(f"Widget {widget_id}: config zu groß (max. {MAX_WIDGET_CONFIG_JSON_BYTES} Byte JSON)")
    if not raw:
        return {}

    if widget_id not in WIDGETS_ALLOWING_CONFIG:
        raise ValueError(f"Widget {widget_id}: keine Konfiguration unterstützt")

    if widget_id == "body_overview":
        return _validate_chart_days_only(raw, label="body_overview")
    if widget_id == "activity_overview":
        return _validate_chart_days_only(raw, label="activity_overview")
    if widget_id == "kpi_board":
        return _validate_kpi_board_config(raw)
    if widget_id == "quick_capture":
        return _validate_quick_capture_config(raw)
    if widget_id == "trend_kcal_weight":
        return _validate_chart_days_only(raw, label="trend_kcal_weight")
    if widget_id == "nutrition_detail_charts":
        return _validate_chart_days_only(raw, label="nutrition_detail_charts")
    if widget_id == "recovery_charts_panel":
        return _validate_chart_days_only(raw, label="recovery_charts_panel")

    raise ValueError(f"Widget {widget_id}: keine Konfiguration unterstützt")


def _validate_quick_capture_config(raw: dict[str, Any]) -> dict[str, Any]:
    label = "quick_capture"
    unknown = set(raw) - _QUICK_CAPTURE_KEYS
    if unknown:
        raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}")
    out: dict[str, bool] = {}
    for k in _QUICK_CAPTURE_KEYS:
        if k not in raw:
            continue
        v = raw[k]
        if not isinstance(v, bool):
            raise ValueError(f"{label}: {k} muss boolean sein")
        out[k] = v
    merged = {k: True for k in _QUICK_CAPTURE_KEYS}
    merged.update(out)
    if not any(merged.values()):
        raise ValueError(f"{label}: mindestens ein Bereich muss sichtbar sein (show_*)")
    return out


def _kpi_tile_id_valid(tid: str) -> bool:
    if tid in _KPI_TILE_FIXED:
        return True
    return bool(_KPI_REF_TILE_RE.match(tid))


def _normalize_kpi_tile_entry(item: Any) -> str:
    if isinstance(item, str):
        tid = item.strip()
    elif isinstance(item, dict) and "id" in item:
        tid = str(item["id"]).strip()
    else:
        raise ValueError("kpi_board: jedes tiles-Element braucht eine id (String oder Objekt mit id)")
    if not tid:
        raise ValueError("kpi_board: leere Kachel-id")
    if not _kpi_tile_id_valid(tid):
        raise ValueError(f"kpi_board: ungültige Kachel-id {tid!r} (z. B. body_fat, avg_kcal, ref:typ_key)")
    return tid


def _validate_kpi_board_config(raw: dict[str, Any]) -> dict[str, Any]:
    if not raw:
        return {}
    # Legacy nur chart_days → entfallen, automatische Kachelwahl
    if set(raw.keys()) <= frozenset({"chart_days"}):
        return {}
    allowed = frozenset({"tiles"})
    unknown = set(raw) - allowed
    if unknown:
        raise ValueError(f"kpi_board: unbekannte config-Felder: {sorted(unknown)}")
    tiles_raw = raw.get("tiles")
    if tiles_raw is None:
        return {}
    if not isinstance(tiles_raw, list):
        raise ValueError("kpi_board: tiles muss eine Liste sein")
    if len(tiles_raw) > 9:
        raise ValueError("kpi_board: maximal 9 Kacheln")
    seen: set[str] = set()
    out: list[dict[str, str]] = []
    for item in tiles_raw:
        tid = _normalize_kpi_tile_entry(item)
        if tid in seen:
            raise ValueError(f"kpi_board: doppelte Kachel-id {tid}")
        seen.add(tid)
        out.append({"id": tid})
    return {"tiles": out}


def _parse_chart_days(v: Any, label: str) -> int:
    if isinstance(v, bool):
        raise ValueError(f"{label}: chart_days muss ganze Zahl sein")
    if isinstance(v, float):
        if not math.isfinite(v):
            raise ValueError(f"{label}: chart_days muss ganze Zahl sein")
        if abs(v - round(v)) > 1e-9:
            raise ValueError(f"{label}: chart_days muss ganze Zahl sein")
        return int(round(v))
    if isinstance(v, int):
        return v
    raise ValueError(f"{label}: chart_days muss ganze Zahl sein")


def _validate_chart_days_only(raw: dict[str, Any], *, label: str) -> dict[str, Any]:
    allowed = frozenset({"chart_days"})
    unknown = set(raw) - allowed
    if unknown:
        raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}")
    if "chart_days" not in raw:
        return {}
    v = _parse_chart_days(raw["chart_days"], label)
    if v < 7 or v > 90:
        raise ValueError(f"{label}: chart_days muss zwischen 7 und 90 liegen")
    return {"chart_days": v}


