"""Normalize common charger fields from heterogeneous Garo JSON responses."""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

# /status/charging-state returns short codes; map to display labels (extend as needed).
CHARGING_STATE_LABELS: dict[str, str] = {
    "A1": "Available",
}


def _as_float(value: Any) -> float | None:
    if value is None:
        return None
    if isinstance(value, bool):
        return None
    if isinstance(value, (int, float)):
        return float(value)
    try:
        return float(str(value))
    except (TypeError, ValueError):
        return None


def _list_is_ocpp_meter_values(lst: list[Any]) -> bool:
    """True for OCPP MeterValue arrays (timestamp + sampledValue with measurand)."""
    for item in lst:
        if isinstance(item, dict) and isinstance(item.get("sampledValue"), list):
            return True
    return False


def merge_layers(*layers: Mapping[str, Any]) -> dict[str, Any]:
    """Later mappings override earlier keys.

    If the API returns a JSON array it is wrapped as ``{"_list": [...]}``.
    OCPP-style meter arrays (``sampledValue`` / ``measurand``) are **not** merged
    per-element into the top level (that would overwrite ``sampledValue``); they
    are left under ``_list`` for :func:`_enrich_from_ocpp_meter_list`.
    Other list shapes are merged dict-by-dict as before.
    """
    merged: dict[str, Any] = {}
    for m in layers:
        merged.update(m)
    lst = merged.get("_list")
    if isinstance(lst, list) and not _list_is_ocpp_meter_values(lst):
        for item in lst:
            if isinstance(item, dict):
                merged.update(item)
    return merged


def _location_matches(
    sv: dict[str, Any],
    location: str | None,
    *,
    location_must_be_absent: bool,
) -> bool:
    """When location_must_be_absent is True, only rows without a location field match."""
    loc = sv.get("location")
    if location_must_be_absent:
        return loc is None or loc == ""
    if location is not None:
        return loc == location
    return True


def get_ocpp_sample_float(
    *layers: Mapping[str, Any],
    measurand: str,
    phase: str | None = None,
    location: str | None = None,
    location_must_be_absent: bool = False,
) -> float | None:
    """Last matching sampledValue wins (metervalue-sample overrides energy-meter when merged later).

    Use location_must_be_absent=True for plain L1/L2/L3/N currents without a ``location`` field.
    """
    merged = merge_layers(*layers)
    val: float | None = None
    for block in _iter_ocpp_blocks(merged):
        sampled = block.get("sampledValue")
        if not isinstance(sampled, list):
            continue
        for sv in sampled:
            if not isinstance(sv, dict):
                continue
            if sv.get("measurand") != measurand:
                continue
            if phase is not None and sv.get("phase") != phase:
                continue
            if not _location_matches(sv, location, location_must_be_absent=location_must_be_absent):
                continue
            v = _as_float(sv.get("value"))
            if v is not None:
                val = v
    return val


def _iter_ocpp_blocks(merged: dict[str, Any]) -> list[dict[str, Any]]:
    """Collect OCPP MeterValue blocks from a top-level array and/or a single sampledValue object."""
    blocks: list[dict[str, Any]] = []
    lst = merged.get("_list")
    if isinstance(lst, list) and _list_is_ocpp_meter_values(lst):
        blocks.extend(b for b in lst if isinstance(b, dict))
    sv = merged.get("sampledValue")
    if isinstance(sv, list) and sv and isinstance(sv[0], dict):
        blocks.append({"sampledValue": sv, "timestamp": merged.get("timestamp")})
    return blocks


def _enrich_from_ocpp_meter_list(merged: dict[str, Any]) -> dict[str, Any]:
    """Map OCPP MeterValue sampledValue entries to flat keys used by pick_first."""
    blocks = _iter_ocpp_blocks(merged)
    if not blocks:
        return merged

    updates: dict[str, Any] = {}
    currents: dict[str, float] = {}

    for block in blocks:
        sampled = block.get("sampledValue")
        if not isinstance(sampled, list):
            continue
        for sv in sampled:
            if not isinstance(sv, dict):
                continue
            measurand = sv.get("measurand", "")
            val_f = _as_float(sv.get("value"))
            if val_f is None:
                continue
            if measurand == "Power.Active.Import":
                updates["power"] = val_f
                updates["activePower"] = val_f
            elif measurand == "Energy.Active.Import.Register":
                # Cumulative register in Wh (used by parse_energy_kwh)
                updates["energy_wh"] = val_f
                updates["energy"] = val_f
                updates["totalEnergy"] = val_f
            elif measurand == "Current.Import":
                phase = sv.get("phase", "")
                if phase in ("L1", "L2", "L3"):
                    currents[phase] = val_f

    if currents:
        l123 = [currents[p] for p in ("L1", "L2", "L3") if p in currents]
        if l123:
            updates["current"] = max(l123)

    out = dict(merged)
    out.update(updates)
    return out


def _flatten_nested_leaves(d: Any) -> dict[str, Any]:
    """Pull nested dict leaves into top-level keys so pick_first finds ``power`` inside ``meter: {power: …}``."""
    flat: dict[str, Any] = {}
    if not isinstance(d, dict):
        return flat
    for k, v in d.items():
        if k.startswith("_"):
            continue
        if isinstance(v, dict):
            flat.update(_flatten_nested_leaves(v))
        elif isinstance(v, list):
            for item in v:
                if isinstance(item, dict):
                    flat.update(_flatten_nested_leaves(item))
        else:
            flat[k] = v
    return flat


def merge_layers_for_parse(*layers: Mapping[str, Any]) -> dict[str, Any]:
    """merge_layers, OCPP MeterValue extraction, then flatten nested dicts for key lookup."""
    merged = merge_layers(*layers)
    merged = _enrich_from_ocpp_meter_list(merged)
    extra = _flatten_nested_leaves(merged)
    out = dict(merged)
    out.update(extra)
    return out


def pick_first(merged: Mapping[str, Any], keys: tuple[str, ...]) -> Any:
    for key in keys:
        if key in merged and merged[key] is not None:
            return merged[key]
    return None


def parse_power_w(*layers: Mapping[str, Any]) -> float | None:
    merged = merge_layers_for_parse(*layers)
    raw = pick_first(
        merged,
        (
            "power",
            "activePower",
            "ActivePower",
            "active_power",
            "powerW",
            "power_w",
            "totalPower",
            "total_power",
            "instantaneousPower",
            "InstantaneousPower",
            "chargePower",
            "ChargePower",
            "evsePower",
            "EvsePower",
            "measuredPower",
            "MeasuredPower",
            "P",
            "p",
            "W",
            "w",
            "value",
            "Value",
            "reading",
            "Reading",
        ),
    )
    val = _as_float(raw)
    return val


def parse_current_a(*layers: Mapping[str, Any]) -> float | None:
    merged = merge_layers_for_parse(*layers)
    raw = pick_first(
        merged,
        (
            "current",
            "Current",
            "rmsCurrent",
            "RmsCurrent",
            "rms_current",
            "currentL1",
            "CurrentL1",
            "current_l1",
            "currentPhase1",
            "I",
            "i",
            "measuredCurrent",
            "MeasuredCurrent",
            "phaseCurrent",
            "L1",
            "importCurrent",
            "value",
            "Value",
            "reading",
            "Reading",
        ),
    )
    return _as_float(raw)


def parse_energy_kwh(*layers: Mapping[str, Any]) -> float | None:
    merged = merge_layers_for_parse(*layers)
    if merged.get("energy_wh") is not None:
        v = _as_float(merged["energy_wh"])
        if v is not None:
            return v / 1000.0
    raw = pick_first(
        merged,
        (
            "energy",
            "Energy",
            "sessionEnergy",
            "SessionEnergy",
            "session_energy",
            "totalEnergy",
            "TotalEnergy",
            "total_energy",
            "energyKwh",
            "energy_kwh",
            "meterEnergy",
            "MeterEnergy",
            "importedEnergy",
            "ImportedEnergy",
            "absoluteEnergy",
            "AbsoluteEnergy",
            "accumulatedEnergy",
            "AccumulatedEnergy",
            "total_import_energy",
            "TotalImportEnergy",
            "value",
            "Value",
            "reading",
            "Reading",
        ),
    )
    val = _as_float(raw)
    if val is None:
        return None
    # Heuristic: large values are often Wh → kWh
    if val > 10000:
        return val / 1000.0
    return val


def parse_state_text(*layers: Mapping[str, Any]) -> str | None:
    merged = merge_layers_for_parse(*layers)
    raw = pick_first(
        merged,
        (
            "state",
            "State",
            "chargingState",
            "ChargingState",
            "charging_state",
            "status",
            "Status",
            "cpStatus",
            "CpStatus",
            "cp_status",
            "chargerState",
            "ChargerState",
            "evseStatus",
            "EvseStatus",
            "operationalState",
            "OperationalState",
            "chargerStatus",
            "ChargerStatus",
        ),
    )
    if raw is not None:
        if isinstance(raw, bool):
            return "Charging" if raw else "Idle"
        code = str(raw).strip()
        return CHARGING_STATE_LABELS.get(code, code)
    # OCPP meter payloads often omit a text state; infer from power/current
    p = _as_float(merged.get("power"))
    c = _as_float(merged.get("current"))
    if p is not None and p > 1.0:
        return "Charging"
    if c is not None and c > 0.01:
        return "Charging"
    if p is not None or c is not None:
        return "Idle"
    return None


def parse_cpu_temperature_c(temperatures: Mapping[str, Any]) -> float | None:
    """CPU temperature from /status/temperatures (°C)."""
    merged = merge_layers_for_parse(temperatures)
    raw = pick_first(
        merged,
        (
            "cpu",
            "cpuTemperature",
            "cpu_temperature",
            "CPU",
            "processor",
            "ProcessorTemperature",
        ),
    )
    return _as_float(raw)


def parse_baseboard_temperature_c(temperatures: Mapping[str, Any]) -> float | None:
    """Baseboard temperature from /status/temperatures (°C)."""
    merged = merge_layers_for_parse(temperatures)
    raw = pick_first(
        merged,
        (
            "baseboard",
            "baseboardTemperature",
            "baseboard_temperature",
            "BaseboardTemperature",
            "board",
            "Board",
            "mb",
            "MB",
            "pcb",
            "Pcb",
            "PCB",
            "motherboard",
            "Motherboard",
            "ambient",
            "Ambient",
            "base_board",
            "BaseBoard",
        ),
    )
    return _as_float(raw)
