"""Local, versioned Swiss BLV nutrient reference comparisons.

Reference CSV files are snapshots downloaded from the official BLV dynamic table.
No dashboard request performs an external network call.  A missing personal profile
is deliberately not replaced by a generic adult assumption.
"""
from __future__ import annotations

import csv
import html
import math
import re
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Any, Mapping

from .nutrition_contract import NUTRIENT_CONTRACTS

REFERENCE_VERSION = "blv-nutrient-reference-2026-07-22-v1"
REFERENCE_RETRIEVED = "2026-07-22"
REFERENCE_SOURCE = "Bundesamt für Lebensmittelsicherheit und Veterinärwesen (BLV) – Nährstoffzufuhr"
REFERENCE_ROOT = Path(__file__).with_name("references") / "blv-2026-07-22"

_GROUP_FILES = {
    "7-11months": "7-11months-both.csv",
    "1-3years": "1-3years-both.csv",
    "4-6years": "4-6years-both.csv",
    "7-10years": "7-10years-both.csv",
    "11-14years": "11-14years-both.csv",
    "15-17years": "15-17years-both.csv",
    "18years": "18years-both.csv",
    "65years": "65years-both.csv",
    "preg": "preg-female.csv",
    "lac": "lac-female.csv",
}

# Exact mapping from the established local nutrient contract to BLV table labels.
_BLV_NAMES = {
    "energy.energy": "Energie (kcal)",
    "nutrient.carb": "Gesamtkohlenhydrate",
    "nutrient.sugar": "Freie Zucker (einschliesslich zugesetzter Zucker)",
    "nutrient.dietaryfiber": "Nahrungsfasern",
    "nutrient.monounsaturated": "Einfach ungesättigte Fettsäuren",
    "nutrient.saturated": "Gesättigte Fettsäuren",
    "nutrient.fat": "Lipide insgesamt",
    "nutrient.protein": "Proteine",
    "nutrient.water": "Wasser",
    "mineral.calcium": "Calcium",
    "mineral.fluorine": "Fluorid",
    "mineral.potassium": "Kalium",
    "mineral.manganese": "Mangan",
    "mineral.copper": "Kupfer",
    "mineral.phosphorus": "Phosphor",
    "mineral.iron": "Eisen",
    "mineral.magnesium": "Magnesium",
    "mineral.iodine": "Jod",
    "mineral.selenium": "Selen",
    "mineral.zinc": "Zink",
    "mineral.chlorine": "Chlorid",
    "nutrient.sodium": "Natrium",
    "vitamin.b3": "Niacin",
    "vitamin.b6": "Vitamin B6",
    "vitamin.k": "Vitamin K",
    "vitamin.b11": "Folat",
    "vitamin.c": "Vitamin C",
    "vitamin.e": "Vitamin E",
    "vitamin.b2": "Riboflavin (B2)",
    "vitamin.b5": "Pantothensäure (B5)",
    "vitamin.b7": "Biotin",
    "vitamin.b12": "Cobalamin (B12)",
    "vitamin.b1": "Thiamin",
    "vitamin.d": "Vitamin D",
    "vitamin.a": "Vitamin A",
}

_FUNCTIONS = {
    "energy.energy": "Energie steht dem Körper für Grundfunktionen und Aktivität zur Verfügung.",
    "nutrient.protein": "Protein liefert Aminosäuren und trägt zum Aufbau und Erhalt von Körpergewebe bei.",
    "nutrient.carb": "Kohlenhydrate sind eine wichtige Energiequelle.",
    "nutrient.sugar": "Zucker gehört zu den Kohlenhydraten; dokumentierter Gesamtzucker ist nicht automatisch freier Zucker.",
    "nutrient.dietaryfiber": "Nahrungsfasern unterstützen normale Darmfunktionen.",
    "nutrient.fat": "Fette liefern Energie und dienen als Träger fettlöslicher Vitamine.",
    "nutrient.saturated": "Gesättigte Fettsäuren sind ein Bestandteil der dokumentierten Fettzufuhr.",
    "nutrient.monounsaturated": "Einfach ungesättigte Fettsäuren sind ein Bestandteil der Nahrungsfette.",
    "nutrient.polyunsaturated": "Mehrfach ungesättigte Fettsäuren sind ein Bestandteil der Nahrungsfette.",
    "nutrient.water": "Wasser ist Bestandteil von Körperflüssigkeiten und unterstützt Transport- und Temperaturfunktionen.",
    "mineral.calcium": "Calcium trägt unter anderem zu Knochen- und Muskelfunktionen bei.",
    "mineral.potassium": "Kalium ist an normalen Nerven- und Muskelfunktionen beteiligt.",
    "mineral.magnesium": "Magnesium ist an zahlreichen Stoffwechsel- sowie Muskel- und Nervenfunktionen beteiligt.",
    "mineral.phosphorus": "Phosphor ist unter anderem Bestandteil von Knochen, Zellen und Energieträgern.",
    "mineral.iron": "Eisen ist Bestandteil von Proteinen des Sauerstofftransports.",
    "mineral.zinc": "Zink ist an zahlreichen Enzym- und Zellfunktionen beteiligt.",
    "mineral.copper": "Kupfer ist Bestandteil verschiedener Enzyme.",
    "mineral.manganese": "Mangan ist Bestandteil verschiedener Enzymsysteme.",
    "mineral.selenium": "Selen ist Bestandteil selenhaltiger Proteine.",
    "mineral.iodine": "Jod wird für die Bildung von Schilddrüsenhormonen benötigt.",
    "nutrient.sodium": "Natrium ist an Flüssigkeits-, Nerven- und Muskelfunktionen beteiligt.",
}


def _vitamin_function(label: str) -> str:
    return f"{label} erfüllt dokumentierte Funktionen im normalen Stoffwechsel; die Tagesansicht stellt keine medizinische Diagnose dar."


@dataclass(frozen=True)
class ProfileResolution:
    status: str
    group: str | None
    label: str | None
    sex: str | None
    age_years: int | None
    missing: tuple[str, ...]
    profile: Mapping[str, Any]


def _years_on(born: date, on_date: date) -> int:
    return on_date.year - born.year - ((on_date.month, on_date.day) < (born.month, born.day))


def resolve_profile(profile: Mapping[str, Any] | None, on_date: date) -> ProfileResolution:
    profile = profile or {}
    sex_raw = str(profile.get("sex") or profile.get("gender") or "").casefold().strip()
    sex = {"m": "male", "male": "male", "maennlich": "male", "männlich": "male", "f": "female", "female": "female", "weiblich": "female"}.get(sex_raw)
    stage = str(profile.get("life_stage") or "").casefold().strip()
    missing = []
    if sex is None:
        missing.append("sex")
    try:
        born = date.fromisoformat(str(profile.get("birth_date") or ""))
        if born > on_date:
            raise ValueError
    except ValueError:
        born = None
        missing.append("birth_date")
    if stage in {"pregnant", "pregnancy", "schwanger", "lactating", "breastfeeding", "stillend"} and sex not in {None, "female"}:
        missing.append("female_life_stage_profile")
    if missing:
        return ProfileResolution("missing_required", None, None, sex, None, tuple(dict.fromkeys(missing)), profile)
    assert born is not None and sex is not None
    age = _years_on(born, on_date)
    ordinary_stages = {"none", "not_applicable", "not_pregnant_or_lactating", "keine", "nicht zutreffend"}
    if sex == "female" and 15 <= age <= 55 and not stage:
        return ProfileResolution("missing_required", None, None, sex, age, ("life_stage",), profile)
    if stage and stage not in ordinary_stages | {"pregnant", "pregnancy", "schwanger", "lactating", "breastfeeding", "stillend"}:
        return ProfileResolution("missing_required", None, None, sex, age, ("life_stage",), profile)
    if stage in {"pregnant", "pregnancy", "schwanger"}:
        return ProfileResolution("resolved", "preg", "Schwangere", sex, age, (), profile)
    if stage in {"lactating", "breastfeeding", "stillend"}:
        return ProfileResolution("resolved", "lac", "Stillende", sex, age, (), profile)
    months = (on_date.year - born.year) * 12 + on_date.month - born.month - (on_date.day < born.day)
    if 7 <= months <= 11:
        group, label = "7-11months", "7–11 Monate"
    elif 1 <= age <= 3:
        group, label = "1-3years", "1–3 Jahre"
    elif 4 <= age <= 6:
        group, label = "4-6years", "4–6 Jahre"
    elif 7 <= age <= 10:
        group, label = "7-10years", "7–10 Jahre"
    elif 11 <= age <= 14:
        group, label = "11-14years", "11–14 Jahre"
    elif 15 <= age <= 17:
        group, label = "15-17years", "15–17 Jahre"
    elif 18 <= age <= 65:
        group, label = "18years", "18–65 Jahre"
    elif age >= 66:
        group, label = "65years", "66 Jahre und älter"
    else:
        return ProfileResolution("unsupported_age", None, None, sex, age, ("supported_age_group",), profile)
    return ProfileResolution("resolved", group, f"{label}, {'männlich' if sex == 'male' else 'weiblich'}", sex, age, (), profile)


def _load_rows(resolution: ProfileResolution) -> dict[str, dict[str, str]]:
    if resolution.group is None or resolution.sex is None:
        return {}
    path = REFERENCE_ROOT / _GROUP_FILES[resolution.group]
    with path.open(encoding="utf-8", newline="") as handle:
        rows = list(csv.DictReader(handle, delimiter=";"))
    marker = "maennlich" if resolution.sex == "male" else "weiblich"
    result = {}
    for row in rows:
        population = str(row.get("Alter und Geschlecht") or "").casefold()
        if resolution.group not in {"preg", "lac"} and marker not in population:
            continue
        result[str(row.get("Nährstoffe") or "")] = {str(key): str(value or "").strip() for key, value in row.items()}
    return result


def _numbers(raw: str) -> list[float]:
    cleaned = re.sub(r"\([^)]*\)", "", html.unescape(raw)).replace("*Infos", "")
    cleaned = re.sub(r"(?<=\d),(?=\d)", ".", cleaned)
    return [float(value) for value in re.findall(r"(?<![A-Za-z])\d+(?:\.\d+)?", cleaned)]


def _reference_display(raw: str | None) -> str | None:
    if not raw:
        return None
    text = html.unescape(raw).replace("**Infos", "").replace("*Infos", "")
    text = re.sub(r"\s*\+\s*Ergänzung\b", "", text, flags=re.IGNORECASE)
    text = re.sub(r"\(\s*\*?\s*\)", "", text)
    text = re.sub(r"\s*\(\d+(?:\s*-\s*\d+)?\)\s*$", "", text)
    return " ".join(text.split())


def _convert(value: float, source_unit: str, target_unit: str) -> float | None:
    source_unit = source_unit.replace("ug", "µg")
    target_unit = target_unit.replace("ug", "µg")
    factors = {"g": 1.0, "mg": 0.001, "µg": 0.000001, "ml": 1.0}
    if source_unit == target_unit:
        return value
    if source_unit in factors and target_unit in factors:
        return value * factors[source_unit] / factors[target_unit]
    return None


def _rule_for(key: str, raw: str, contract_unit: str, resolution: ProfileResolution, energy_kcal: float | None) -> dict[str, Any]:
    decoded = html.unescape(raw)
    values = _numbers(decoded)
    result: dict[str, Any] = {"kind": "not_determinable", "target": None, "minimum": None, "maximum": None, "comparison_unit": contract_unit, "reason": None}
    if key == "nutrient.sugar":
        result["reason"] = "Dokumentierter Gesamtzucker ist nicht gleich freier Zucker; deshalb keine rechnerische Bewertung."
        return result
    if key == "energy.energy" and resolution.group in {"preg", "lac"}:
        result["reason"] = "Der BLV-Wert ist ein Zusatz zum persönlichen Energiegrundziel; ohne separat bestimmtes Grundziel keine Gesamtreferenz."
        return result
    if key in {"vitamin.b3", "vitamin.b11", "vitamin.a"}:
        result["reason"] = "Die dokumentierte Einheit entspricht nicht sicher der BLV-Referenzeinheit (NE, DFE beziehungsweise RE)."
        return result
    if key == "mineral.zinc" and len(values) >= 4:
        phytate = resolution.profile.get("phytate_mg_per_day")
        try:
            phytate_value = float(phytate)
        except (TypeError, ValueError):
            result["reason"] = "Für die Auswahl des Zink-Referenzwerts fehlt die dokumentierte Phytatzufuhr."
            return result
        index = 0 if phytate_value < 450 else 1 if phytate_value < 750 else 2 if phytate_value < 1050 else 3
        result.update(kind="value", target=values[index])
        return result
    if key == "mineral.calcium" and len(values) >= 2 and resolution.age_years is not None:
        result.update(kind="value", target=values[0] if resolution.age_years <= 24 else values[1])
        return result
    if key == "nutrient.protein" and values:
        try:
            weight = float(resolution.profile.get("body_weight_kg"))
        except (TypeError, ValueError):
            result["reason"] = "Für das gewichtsbezogene Proteinziel fehlt das dokumentierte Körpergewicht."
            return result
        if not math.isfinite(weight) or not 20 <= weight <= 400:
            result["reason"] = "Das dokumentierte Körpergewicht ist für die Referenzberechnung nicht verwendbar."
            return result
        target = values[0] * weight
        if resolution.group == "preg":
            try:
                trimester = int(str(resolution.profile.get("pregnancy_trimester") or ""))
            except (TypeError, ValueError):
                result["reason"] = "Für das Proteinreferenzziel in der Schwangerschaft fehlt das dokumentierte Trimester."
                return result
            if trimester not in {1, 2, 3} or len(values) < 4:
                result["reason"] = "Das dokumentierte Schwangerschaftstrimester ist für diese Referenz nicht verwendbar."
                return result
            target += values[trimester]
        elif resolution.group == "lac":
            try:
                lactation_month = int(str(resolution.profile.get("lactation_month") or ""))
            except (TypeError, ValueError):
                result["reason"] = "Für das Proteinreferenzziel in der Stillzeit fehlt der dokumentierte Stillmonat."
                return result
            if lactation_month < 1 or len(values) < 3:
                result["reason"] = "Der dokumentierte Stillmonat ist für diese Referenz nicht verwendbar."
                return result
            target += values[1] if lactation_month <= 6 else values[2]
        result.update(kind="minimum", minimum=target)
        return result
    if "% EN" in decoded:
        if energy_kcal is None or not math.isfinite(energy_kcal) or energy_kcal <= 0:
            result["reason"] = "Für die Umrechnung aus Energieprozent fehlt dokumentierte Energie."
            return result
        kcal_per_g = 9.0 if key in {"nutrient.fat", "nutrient.saturated", "nutrient.monounsaturated"} else 4.0
        converted = [energy_kcal * value / 100.0 / kcal_per_g for value in values]
        if "≤" in decoded or "<" in decoded:
            result.update(kind="maximum", maximum=converted[0])
        elif "≥" in decoded or ">" in decoded:
            result.update(kind="minimum", minimum=converted[0])
        elif len(converted) >= 2:
            result.update(kind="range", minimum=converted[0], maximum=converted[1])
        elif converted:
            result.update(kind="value", target=converted[0])
        return result
    if key == "energy.energy" and not resolution.profile.get("activity_level"):
        result["reason"] = "Persönliches Ziel nicht bestimmbar: Aktivitätsniveau fehlt."
        return result
    if key == "vitamin.b1" and "mg/MJ" in decoded:
        if energy_kcal is None or energy_kcal <= 0:
            result["reason"] = "Für den energiebezogenen Referenzwert fehlt dokumentierte Energie."
            return result
        result.update(kind="value", target=values[0] * energy_kcal * 4.184 / 1000.0)
        return result
    unit_match = re.search(r"\b(kcal|MJ|ml|mg|µg|g)(?:-|/)", decoded)
    source_unit = unit_match.group(1) if unit_match else contract_unit
    converted = [item for value in values if (item := _convert(value, source_unit, contract_unit)) is not None]
    if not converted:
        result["reason"] = "Der lokale BLV-Wert ist nicht sicher in die dokumentierte Einheit umrechenbar."
    elif "≤" in decoded or "<" in decoded:
        result.update(kind="maximum", maximum=converted[0])
    elif "≥" in decoded or ">" in decoded:
        result.update(kind="minimum", minimum=converted[0])
    elif "-" in decoded and len(converted) >= 2:
        result.update(kind="range", minimum=converted[0], maximum=converted[1])
    elif len(converted) == 1:
        result.update(kind="value", target=converted[0])
    else:
        result["reason"] = "Der BLV-Katalog enthält mehrere kontextabhängige Werte; notwendige Einflussgrössen fehlen."
    return result


def _comparison(value: float | None, rule: Mapping[str, Any], complete: bool) -> dict[str, Any]:
    if value is None or not complete or rule.get("kind") == "not_determinable":
        return {"percentage": None, "percentage_range": None, "difference": None, "interpretation": "nicht beurteilbar – Daten unvollständig"}
    kind = rule["kind"]
    target = rule.get("target")
    low, high = rule.get("minimum"), rule.get("maximum")
    percentage = None
    percentage_range = None
    difference = None
    if kind == "value" and target:
        percentage = value / target * 100.0
        difference = value - target
        interpretation = "unter dem Referenzwert" if value < target else "Referenzwert erreicht" if math.isclose(value, target, rel_tol=1e-9) else "über dem Referenzwert"
    elif kind == "minimum" and low:
        percentage = value / low * 100.0
        difference = value - low
        interpretation = "unter dem Referenzwert" if value < low else "Referenzwert erreicht"
    elif kind == "maximum" and high:
        percentage = value / high * 100.0
        difference = value - high
        interpretation = "über dem Referenzwert" if value > high else "Referenzwert erreicht"
    elif kind == "range" and low and high:
        percentage_range = {"from": value / high * 100.0, "to": value / low * 100.0}
        difference = value - low if value < low else value - high if value > high else 0.0
        interpretation = "unter dem Referenzwert" if value < low else "über dem Referenzwert" if value > high else "Referenzwert erreicht"
    else:
        return {"percentage": None, "percentage_range": None, "difference": None, "interpretation": "nicht beurteilbar – Daten unvollständig"}
    return {
        "percentage": round(percentage, 1) if percentage is not None else None,
        "percentage_range": {key: round(number, 1) for key, number in percentage_range.items()} if percentage_range else None,
        "difference": round(difference, 3) if difference is not None else None,
        "interpretation": interpretation,
    }


def build_overview(
    *,
    on_date: date,
    profile: Mapping[str, Any] | None,
    nutrients: Mapping[str, Mapping[str, Any]],
    energy_kcal: float | None,
    documented_days: int,
    period_from: str,
    period_to: str,
    contributions: Mapping[str, list[Mapping[str, Any]]] | None = None,
) -> dict[str, Any]:
    resolution = resolve_profile(profile, on_date)
    try:
        period_start = date.fromisoformat(period_from)
    except ValueError:
        period_start = on_date
    start_resolution = resolve_profile(profile, period_start)
    if (
        period_start != on_date
        and resolution.status == "resolved"
        and (start_resolution.status != "resolved" or start_resolution.group != resolution.group)
    ):
        resolution = ProfileResolution(
            "period_group_boundary",
            None,
            None,
            resolution.sex,
            resolution.age_years,
            ("period_profile_group_boundary",),
            resolution.profile,
        )
    source_rows = _load_rows(resolution)
    contributions = contributions or {}
    rows = []
    for key, contract in NUTRIENT_CONTRACTS.items():
        nutrient = nutrients.get(key, {})
        value = nutrient.get("food_value", nutrient.get("value"))
        try:
            numeric = float(value) if value is not None else None
        except (TypeError, ValueError):
            numeric = None
        item_count = int(nutrient.get("item_count") or 0)
        unknown_items = int(nutrient.get("unknown_item_count") or 0)
        nutrient_days = int(nutrient.get("documented_days") or (1 if numeric is not None else 0))
        unknown_days = max(0, documented_days - nutrient_days)
        complete = numeric is not None and unknown_items == 0 and unknown_days == 0
        blv_name = _BLV_NAMES.get(key)
        source = source_rows.get(blv_name or "")
        raw = source.get("Menge und Quellen") if source else None
        if resolution.status != "resolved":
            rule = {"kind": "not_determinable", "target": None, "minimum": None, "maximum": None, "comparison_unit": contract.unit, "reason": "Persönliches Ziel nicht bestimmbar: Profilangaben fehlen."}
        elif raw:
            rule = _rule_for(key, raw, contract.unit, resolution, energy_kcal)
        else:
            rule = {"kind": "not_determinable", "target": None, "minimum": None, "maximum": None, "comparison_unit": contract.unit, "reason": "Kein passender Wert im lokal versionierten BLV-Katalog."}
        comparison = _comparison(numeric, rule, complete)
        contribution_rows = []
        for contribution in list(contributions.get(key, []))[:5]:
            contribution_copy = dict(contribution)
            try:
                contribution_copy["amount"] = round(
                    float(contribution_copy["amount"]) / max(1, nutrient_days),
                    contract.precision,
                )
            except (KeyError, TypeError, ValueError):
                pass
            contribution_rows.append(contribution_copy)
        rows.append({
            "key": key,
            "name": contract.label,
            "group": contract.group,
            "documented_amount": round(numeric, contract.precision) if numeric is not None else None,
            "unit": contract.unit,
            "alternate_amount": round(numeric * 4.184, 1) if key == "energy.energy" and numeric is not None else None,
            "alternate_unit": "kJ" if key == "energy.energy" else None,
            "reference": {
                "display": _reference_display(raw),
                **rule,
                "type": rule.get("kind"),
                "upper_safety_limit": None,
                "source_record": {
                    "snapshot_file": _GROUP_FILES.get(resolution.group or ""),
                    "catalog_nutrient": blv_name,
                } if source else None,
            },
            **comparison,
            "data_completeness": {
                "status": "complete" if complete else "partial_unknown" if numeric is not None else "unknown",
                "documented_days": nutrient_days,
                "period_documented_days": documented_days,
                "unknown_days": unknown_days,
                "item_count": item_count,
                "unknown_item_count": unknown_items,
                "message": "Wert teilweise unbekannt" if numeric is not None and (unknown_items or unknown_days) else "Keine belastbare dokumentierte Menge" if numeric is None else "Dokumentierte Lebensmittelwerte vollständig",
            },
            "food_contributions": contribution_rows,
            "function": _FUNCTIONS.get(key) or (_vitamin_function(contract.label) if contract.group == "vitamin" else f"{contract.label} ist ein dokumentierter Nährstoff; diese Ansicht nimmt keine medizinische Einordnung vor."),
        })
    return {
        "version": 1,
        "period": {"from": period_from, "to": period_to, "documented_days": documented_days},
        "profile": {"status": resolution.status, "reference_group": resolution.label, "missing_fields": list(resolution.missing)},
        "source": {"name": REFERENCE_SOURCE, "catalog": "naehrstofftabelle-de", "version": REFERENCE_VERSION, "retrieved_at": REFERENCE_RETRIEVED, "runtime_external_requests": False},
        "rows": rows,
        "medical_notice": "Referenzwerte beschreiben Bevölkerungsgruppen und entsprechen nicht zwingend dem individuellen medizinischen Bedarf. Ein einzelner Tag erlaubt keine Diagnose eines Mangels oder einer Überversorgung.",
    }
