"""Canonical NetWeaver truth-value color scheme — the single source of truth.

Every dialog, renderer, legend, and swatch in the plugin imports its class
breaks from here (parity plan Phase Q0). The values mirror the ArcGIS
add-in's ``NetWeaverNodeDisplay/TruthValueScheme.cs`` byte-for-byte so a user
moving between the two products sees the SAME color for the same value:

* The ramp runs blue (very low) -> dark red (very high). The previous
  in-plugin ramp was inverted (red = very low), contradicting the ArcGIS
  renderer, hotlink swatches, and node viewer (analysis defect D1).
* "No data" is ONLY: NaN, +/-infinity, the engine's unevaluated marker
  (.NET double.MaxValue), or a value below ``NO_DATA_FLOOR``. The engine's
  persisted no-data sentinel is -9999.888777. -1.0 is a REAL "Very low"
  value and must never classify as no-data (analysis defect D2).

This module is deliberately dependency-free (stdlib ``math`` only) so it can
be imported by headless tests without QGIS or pandas installed.
"""

from __future__ import annotations

import math
from typing import Any, List, Optional, Tuple

# Values strictly below this floor are the engine's persisted no-data
# sentinels (e.g. -9999.888777). Exactly -9000.0 classifies as "Very low".
NO_DATA_FLOOR = -9000.0

# .NET double.MaxValue — the engine reports this for unevaluated nodes.
UNEVALUATED_SENTINEL = 1.7976931348623157e308

NO_DATA_LABEL = "No data"

# (lower, upper, label, fill hex). Classification uses lower <= v < upper,
# with the top bin closed at 1.0 (v == 1.0 -> "Very high"). The first entry
# is the no-data bin so map renderers assign grey to persisted sentinels.
NETWEAVER_CLASS_BREAKS: List[Tuple[float, float, str, str]] = [
    (-999999.0, NO_DATA_FLOOR, NO_DATA_LABEL, "#AAAAAA"),
    (NO_DATA_FLOOR, -0.6, "Very low", "#2B83BA"),
    (-0.6, -0.2, "Low", "#ABDDA4"),
    (-0.2, 0.2, "Medium", "#FFFFBF"),
    (0.2, 0.6, "High", "#FDAE61"),
    (0.6, 1.0, "Very high", "#D7191C"),
]

_COLOR_BY_LABEL = {label: color for _lo, _hi, label, color in NETWEAVER_CLASS_BREAKS}


def is_no_data(value: Any) -> bool:
    """True when *value* is a numeric no-data marker (NaN / infinity /
    unevaluated sentinel / below ``NO_DATA_FLOOR``). Non-numeric values
    return False — they are unclassifiable, not no-data."""
    try:
        numeric = float(value)
    except (TypeError, ValueError):
        return False
    if math.isnan(numeric) or math.isinf(numeric):
        return True
    return numeric >= UNEVALUATED_SENTINEL or numeric < NO_DATA_FLOOR


def numeric_or_none(value: Any) -> Optional[float]:
    """Persistence coercion (analysis defect D5): the float value, or None when
    *value* is blank, unparseable, or a no-data marker.

    Callers write the None straight through as SQL NULL, so ``avg()``,
    histogram bin edges and symbology never see -9999.888777 or the engine's
    ``double.MaxValue`` unevaluated marker. Writing the raw sentinel once made
    ``avg(nw1__tree_mortality_recent_5yr)`` read -2359.30 against a true +0.68.

    The ArcGIS twin is ``TruthValueScheme.NullIfNoData``.
    """
    try:
        if value is None:
            return None
        if hasattr(value, "item"):  # numpy scalar -> python scalar
            value = value.item()
        if value == "":
            return None
        numeric = float(value)
    except (TypeError, ValueError):
        return None
    return None if is_no_data(numeric) else numeric


def classify_netweaver_value(value: Any) -> Optional[str]:
    """Return the class label for a NetWeaver value, or None when the value
    is not classifiable (non-numeric, or numeric outside every bin)."""
    try:
        numeric = float(value)
    except (TypeError, ValueError):
        return None

    if is_no_data(numeric):
        return NO_DATA_LABEL

    for lower, upper, label, _color in NETWEAVER_CLASS_BREAKS:
        if lower <= numeric < upper:
            return label
    if numeric == 1.0:
        return "Very high"
    return None


def color_for_label(label: Optional[str]) -> Optional[str]:
    """Fill hex for a class label from :data:`NETWEAVER_CLASS_BREAKS`."""
    if not label:
        return None
    return _COLOR_BY_LABEL.get(label)


def color_for_value(value: Any) -> Optional[str]:
    """Fill hex for a NetWeaver value (grey for no-data), or None when the
    value is not classifiable."""
    return color_for_label(classify_netweaver_value(value))


def text_color_for_fill(fill_hex: str) -> str:
    """Black or white text, whichever is legible on *fill_hex* (same
    luminance rule as the ArcGIS ``TruthValueScheme.ForegroundFor``)."""
    try:
        hex_digits = fill_hex.lstrip("#")
        r, g, b = (int(hex_digits[i : i + 2], 16) for i in (0, 2, 4))
    except (ValueError, IndexError):
        return "#000000"
    luminance = 0.299 * r + 0.587 * g + 0.114 * b
    return "#000000" if luminance > 150 else "#FFFFFF"
