"""Baut die Nutzlasten, die als Entitäts-Attribut `daten` an die Oberfläche
gehen.

Das Frontend liest die Werte über das ganz normale hass.states statt über
einen Dienst mit Rückgabewert: Zustände lesen ist ein seit Jahren stabiler,
einfacher Weg, während das Verhalten response-fähiger Dienste aus Sicht des
Browsers nicht durchgängig dokumentiert ist.

Hier stehen nur die reinen Rechenfunktionen - wer sie wann veröffentlicht,
entscheidet der Koordinator (koordinator.py).

GRÖSSENGRENZE, DIE JETZT KEINE MEHR IST: Zustands-Attribute sind in Home
Assistant nicht für beliebig große Datenmengen gedacht; ab ~16 KB warnt der
Recorder und schreibt sie trotzdem in die Datenbank. Die Fahrten- und
Tankvorgänge-Archive überschreiten das nach ein paar Jahren zwangsläufig. In
der pyscript-Fassung war das eine bekannte, offene Flanke. Diese Integration
markiert das Attribut `daten` als nicht aufzuzeichnen (siehe
_unrecorded_attributes in sensor.py) - die Nutzlast geht weiterhin an jede
Oberfläche, landet aber nie in der Datenbank.
"""

from __future__ import annotations

from homeassistant.core import HomeAssistant

from .einstellungen import POSITIONEN, Sensorzuordnung


def zustand_oder_none(hass: HomeAssistant, entity_id: str | None) -> str | None:
    """Sicherer Zustandszugriff.

    Eine nicht (mehr) existierende Entität ist hier der Normalfall, solange
    Rollen im Setup-Menü unbelegt sind, und bleibt auch danach relevant: fällt
    eine Datenquelle aus, soll die Oberfläche das zeigen, nicht an einem
    Fehler hängen bleiben."""
    if not entity_id:
        return None
    zustand = hass.states.get(entity_id)
    if zustand is None or zustand.state in (None, "", "unknown", "unavailable"):
        return None
    return zustand.state


def _zu_zahl(wert: object) -> float | None:
    try:
        return float(wert)  # type: ignore[arg-type]
    except (TypeError, ValueError):
        return None


def _abs_zahl(wert: object) -> float | None:
    zahl = _zu_zahl(wert)
    return None if zahl is None else abs(zahl)


def _zu_bool(wert: object) -> bool | None:
    """binary_sensor-Zustand als echtes True/False, None bei fehlender Meldung.

    None ist hier ausdrücklich kein "nein": ohne zugeordneten Sensor weiß die
    App schlicht nicht, ob gefahren wird - und muss das anzeigen dürfen, statt
    "steht" zu behaupten."""
    if wert is None:
        return None
    return str(wert).lower() in ("on", "true", "1", "open", "yes")


def _standort(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
    """Live-GPS-Position - liest STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR, zwei
    eigene sensor-Entities für Breiten-/Längengrad (flespi liefert Koordinaten
    so, nicht als Attribute einer einzelnen device_tracker-Entity).

    Fehlt eine der beiden Entity-IDs oder ist der Zustand (noch) nicht
    verfügbar, liefert diese Funktion durchgehend None statt eines geratenen
    Werts."""
    leer = {"lat": None, "lon": None, "genauigkeit_m": None, "zeit": None}

    lat_id, lon_id = werte.STANDORT_LAT_SENSOR, werte.STANDORT_LON_SENSOR
    if not lat_id or not lon_id:
        return leer
    lat_zustand = hass.states.get(lat_id)
    lon_zustand = hass.states.get(lon_id)
    if lat_zustand is None or lon_zustand is None:
        return leer
    lat = _zu_zahl(zustand_oder_none(hass, lat_id))
    lon = _zu_zahl(zustand_oder_none(hass, lon_id))
    if lat is None or lon is None:
        return leer
    return {
        "lat": lat,
        "lon": lon,
        "genauigkeit_m": None,
        "zeit": lat_zustand.last_updated.isoformat(),
    }


def _sicherheitscheck(hass: HomeAssistant, werte: Sensorzuordnung) -> list[dict]:
    """Die einzeln geprüften Punkte hinter "Sicher abgestellt", fürs Frontend -
    gruppiert über das Feld "gruppe" in vier Sammelzeilen (Fahrzeug verriegelt
    / Türen und Klappen geschlossen / Fenster und Dach geschlossen / Licht
    ausgeschaltet), die die Oberfläche durch Gruppieren dieser flachen Liste
    berechnet, statt hier schon vier separate Listen zu bauen - eine flache
    Liste bleibt die einfachere, additive Änderung gegenüber dem, was das
    Frontend vorher schon kannte.

    "ok" ist None, wenn der Sensor fehlt oder nicht verfügbar ist - genau
    daraus leitet sich auch die zusammengefasste Kennzahl unten ab, damit
    beide nie auseinanderlaufen können. Alle binary_sensor-device_classes
    hier (door/window/opening/lock/light) folgen HAs eigener Konvention "off
    = der gute Zustand" (zu/verriegelt/kein Licht) - ein einziges Prüfmuster
    für alle Zeilen, kein Sonderfall je Art."""
    eintraege: list[dict] = []
    for pos, sensor in zip(POSITIONEN, werte.TUER_SENSOREN):
        w = zustand_oder_none(hass, sensor)
        eintraege.append({"label": f"Tür {pos}", "ok": None if w is None else w == "off", "gruppe": "tueren_klappen"})
    for pos, sensor in zip(POSITIONEN, werte.FENSTER_SENSOREN):
        w = zustand_oder_none(hass, sensor)
        eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off", "gruppe": "fenster_dach"})
    # "Kofferraum" statt "Heckklappe": alltagssprachlicher Name für dieselbe
    # Öffnung beim Avant/Kombi - das Sensorfeld HECKKLAPPE_SENSOR selbst
    # bleibt unverändert, um eine bestehende Zuordnung nicht zu brechen.
    for label, sensor in (
        ("Kofferraum", werte.HECKKLAPPE_SENSOR),
        ("Motorhaube", werte.HAUBE_SENSOR),
    ):
        w = zustand_oder_none(hass, sensor)
        eintraege.append({"label": label, "ok": None if w is None else w == "off", "gruppe": "tueren_klappen"})
    # Nur die Fahrertür: die Zentralverriegelung schließt alle Türen gemeinsam,
    # ihr Zustand gilt fürs ganze Fahrzeug (siehe TUERSCHLOSS_SENSOR).
    w = zustand_oder_none(hass, werte.TUERSCHLOSS_SENSOR)
    eintraege.append({"label": "Türschloss", "ok": None if w is None else w == "off", "gruppe": "verriegelt"})
    w = zustand_oder_none(hass, werte.DACH_SENSOR)
    eintraege.append({"label": "Dach", "ok": None if w is None else w == "off", "gruppe": "fenster_dach"})
    w = zustand_oder_none(hass, werte.LICHT_SENSOR)
    eintraege.append({"label": "Standlicht", "ok": None if w is None else w == "off", "gruppe": "licht"})
    return eintraege


def fahrzeugstatus(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
    """Bündelt die live aus Home Assistant gelesenen Fahrzeugwerte
    frontend-freundlich, damit die Oberfläche keine Entity-IDs kennen muss."""
    sicherheitscheck = _sicherheitscheck(hass, werte)
    # "Sicher abgestellt": erst wenn WIRKLICH jeder einzeln geprüfte Punkt
    # zu/verriegelt ist, gilt das Fahrzeug als gesichert; fehlt auch nur eine
    # Meldung, ist der Status unbekannt statt geraten.
    if any(e["ok"] is None for e in sicherheitscheck):
        gesichert = None
    else:
        gesichert = all(e["ok"] for e in sicherheitscheck)

    standort = _standort(hass, werte)

    return {
        "km": _zu_zahl(zustand_oder_none(hass, werte.KM_SENSOR)),
        "tankprozent": _zu_zahl(zustand_oder_none(hass, werte.TANK_SENSOR)),
        "reichweite_km": _zu_zahl(zustand_oder_none(hass, werte.RANGE_SENSOR)),
        "batteriespannung": _zu_zahl(zustand_oder_none(hass, werte.BATTERIE_SENSOR)),
        "gesichert": gesichert,
        "sicherheitscheck": sicherheitscheck,
        # Fährt das Fahrzeug gerade? Kommt aus derselben Zündungs-Entität, die
        # auch die Fahrterkennung als maßgebliches Signal für Fahrtbeginn und
        # -ende nimmt - damit sagen Anzeige und Erfassung zwangsläufig
        # dasselbe. Vorher wurde dieser Zustand im Frontend aus dem Status der
        # Fahrten abgeleitet ("offen" = fährt). Das war falsch: "offen" heißt
        # unvollständige Daten, nicht "unterwegs" - eine Fahrt ohne
        # Kilometerstand blieb dauerhaft "offen" und das Fahrzeug damit
        # dauerhaft "fahrend". None bedeutet: kein Zündungssensor zugeordnet.
        "zuendung": _zu_bool(zustand_oder_none(hass, werte.ZUENDUNG_SENSOR)),
        "standort_lat": standort["lat"],
        "standort_lon": standort["lon"],
        "standort_genauigkeit_m": standort["genauigkeit_m"],
        "standort_zeit": standort["zeit"],
        # Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die
        # App-eigene Servicebuch-Berechnung) - die Streckensensoren liefern
        # negative Restkilometer-Werte, hier deshalb der Betrag.
        "oelwechsel_faellig_ts": zustand_oder_none(hass, werte.NAECHSTER_OELWECHSEL_SENSOR),
        "oelwechsel_faellig_km": _abs_zahl(zustand_oder_none(hass, werte.OELWECHSEL_STRECKE_SENSOR)),
        "inspektion_faellig_ts": zustand_oder_none(hass, werte.NAECHSTE_INSPEKTION_SENSOR),
        "inspektion_faellig_km": _abs_zahl(zustand_oder_none(hass, werte.INSPEKTION_STRECKE_SENSOR)),
    }
