"""
Sensor health analysis for Sentinel Brain.
"""
 
from dataclasses import dataclass, field
 
from sentinel.brain.findings import Finding
 
 
@dataclass
class SensorHealthResult:
    """
    Result of the sensor health analysis.
 
    Attributes:
        score: Global sensor health score from 0 to 100.
        findings: Explainable sensor findings.
    """
 
    score: float
    findings: list[Finding] = field(default_factory=list)
 
 
class SensorHealthAnalyzer:
    """
    Detect frozen signals, impossible jumps and implausible values.
    """
 
    SENSOR_FIELDS = {
        "internal_temperature_c": {
            "label": "Internal temperature sensor",
            "minimum": -100.0,
            "maximum": 50.0,
            "maximum_jump": 8.0,
            "unit": "°C",
        },
        "ambient_temperature_c": {
            "label": "Ambient temperature sensor",
            "minimum": -30.0,
            "maximum": 70.0,
            "maximum_jump": 8.0,
            "unit": "°C",
        },
        "evaporator_temperature_c": {
            "label": "Evaporator temperature sensor",
            "minimum": -120.0,
            "maximum": 80.0,
            "maximum_jump": 15.0,
            "unit": "°C",
        },
        "compressor_temperature_c": {
            "label": "Compressor temperature sensor",
            "minimum": -30.0,
            "maximum": 150.0,
            "maximum_jump": 15.0,
            "unit": "°C",
        },
        "compressor_current_a": {
            "label": "Compressor current sensor",
            "minimum": 0.0,
            "maximum": 20.0,
            "maximum_jump": 5.0,
            "unit": "A",
        },
    }
 
    FROZEN_SAMPLE_THRESHOLD = 20
    FROZEN_TOLERANCE = 0.0001
 
    @staticmethod
    def _values(
        history: list[dict[str, object]],
        field: str,
    ) -> list[float]:
        """
        Extract valid numeric values for one sensor.
        """
        values: list[float] = []
 
        for row in history:
            value = row.get(field)
 
            if value is None:
                continue
 
            try:
                values.append(float(value))
            except (TypeError, ValueError):
                continue
 
        return values
 
    @classmethod
    def _check_missing_data(
        cls,
        history: list[dict[str, object]],
        field: str,
        label: str,
    ) -> tuple[list[Finding], float]:
        """
        Detect missing or invalid sensor values.
        """
        findings: list[Finding] = []
 
        valid_count = len(cls._values(history, field))
        total_count = len(history)
 
        if total_count == 0:
            return findings, 0.0
 
        missing_count = total_count - valid_count
        missing_ratio = missing_count / total_count
 
        if missing_ratio >= 0.20:
            findings.append(
                Finding(
                    code=f"{field.upper()}_DATA_MISSING",
                    severity="HIGH",
                    confidence=100.0,
                    explanation=(
                        f"{label} has {missing_count} missing or "
                        f"invalid values out of {total_count} samples."
                    ),
                    metric=f"{field}_missing_ratio",
                    value=round(missing_ratio * 100.0, 2),
                    unit="%",
                )
            )
 
            return findings, 30.0
 
        if missing_count > 0:
            findings.append(
                Finding(
                    code=f"{field.upper()}_DATA_INCOMPLETE",
                    severity="MEDIUM",
                    confidence=100.0,
                    explanation=(
                        f"{label} has {missing_count} missing or "
                        f"invalid values out of {total_count} samples."
                    ),
                    metric=f"{field}_missing_ratio",
                    value=round(missing_ratio * 100.0, 2),
                    unit="%",
                )
            )
 
            return findings, 10.0
 
        return findings, 0.0
 
    @classmethod
    def _check_physical_range(
        cls,
        values: list[float],
        field: str,
        label: str,
        minimum: float,
        maximum: float,
        unit: str,
    ) -> tuple[list[Finding], float]:
        """
        Detect values outside provisional physical limits.
        """
        findings: list[Finding] = []
 
        invalid_values = [
            value
            for value in values
            if value < minimum or value > maximum
        ]
 
        if not invalid_values:
            return findings, 0.0
 
        most_extreme = max(
            invalid_values,
            key=lambda value: max(
                minimum - value,
                value - maximum,
            ),
        )
 
        findings.append(
            Finding(
                code=f"{field.upper()}_IMPLAUSIBLE_VALUE",
                severity="CRITICAL",
                confidence=100.0,
                explanation=(
                    f"{label} reported an implausible value of "
                    f"{most_extreme:.3f} {unit}. "
                    f"Expected provisional range: "
                    f"{minimum:.1f} to {maximum:.1f} {unit}."
                ),
                metric=f"{field}_physical_range",
                value=most_extreme,
                unit=unit,
            )
        )
 
        return findings, 40.0
 
    @classmethod
    def _check_frozen_signal(
        cls,
        values: list[float],
        field: str,
        label: str,
        unit: str,
    ) -> tuple[list[Finding], float]:
        """
        Detect a signal frozen at the same value.
        """
        findings: list[Finding] = []
 
        if len(values) < cls.FROZEN_SAMPLE_THRESHOLD:
            return findings, 0.0
 
        recent_values = values[
            -cls.FROZEN_SAMPLE_THRESHOLD:
        ]
 
        variation = max(recent_values) - min(recent_values)
 
        if variation > cls.FROZEN_TOLERANCE:
            return findings, 0.0
 
        findings.append(
            Finding(
                code=f"{field.upper()}_SIGNAL_FROZEN",
                severity="HIGH",
                confidence=95.0,
                explanation=(
                    f"{label} remained fixed at "
                    f"{recent_values[-1]:.4f} {unit} for the last "
                    f"{cls.FROZEN_SAMPLE_THRESHOLD} samples."
                ),
                metric=f"{field}_frozen_variation",
                value=round(variation, 6),
                unit=unit,
            )
        )
 
        return findings, 25.0
 
    @staticmethod
    def _check_impossible_jump(
        values: list[float],
        field: str,
        label: str,
        maximum_jump: float,
        unit: str,
    ) -> tuple[list[Finding], float]:
        """
        Detect an excessive change between consecutive samples.
        """
        findings: list[Finding] = []
 
        if len(values) < 2:
            return findings, 0.0
 
        largest_jump = max(
            abs(current - previous)
            for previous, current in zip(
                values,
                values[1:],
            )
        )
 
        if largest_jump <= maximum_jump:
            return findings, 0.0
 
        findings.append(
            Finding(
                code=f"{field.upper()}_IMPOSSIBLE_JUMP",
                severity="HIGH",
                confidence=90.0,
                explanation=(
                    f"{label} changed by {largest_jump:.3f} {unit} "
                    "between two consecutive measurements. "
                    f"The provisional maximum accepted jump is "
                    f"{maximum_jump:.3f} {unit}."
                ),
                metric=f"{field}_largest_jump",
                value=round(largest_jump, 4),
                unit=unit,
            )
        )
 
        return findings, 25.0
 
    @classmethod
    def analyze(
        cls,
        history: list[dict[str, object]],
    ) -> SensorHealthResult:
        """
        Analyze all Sentinel sensors.
 
        Args:
            history: Measurements ordered from oldest to newest.
 
        Returns:
            Sensor score and explainable findings.
        """
        if not history:
            raise ValueError(
                "Measurement history cannot be empty."
            )
 
        findings: list[Finding] = []
        total_penalty = 0.0
 
        for field, configuration in cls.SENSOR_FIELDS.items():
            label = str(configuration["label"])
            minimum = float(configuration["minimum"])
            maximum = float(configuration["maximum"])
            maximum_jump = float(
                configuration["maximum_jump"]
            )
            unit = str(configuration["unit"])
 
            values = cls._values(
                history=history,
                field=field,
            )
 
            new_findings, penalty = cls._check_missing_data(
                history=history,
                field=field,
                label=label,
            )
 
            findings.extend(new_findings)
            total_penalty += penalty
 
            if not values:
                continue
 
            new_findings, penalty = cls._check_physical_range(
                values=values,
                field=field,
                label=label,
                minimum=minimum,
                maximum=maximum,
                unit=unit,
            )
 
            findings.extend(new_findings)
            total_penalty += penalty
 
            new_findings, penalty = cls._check_frozen_signal(
                values=values,
                field=field,
                label=label,
                unit=unit,
            )
 
            findings.extend(new_findings)
            total_penalty += penalty
 
            new_findings, penalty = cls._check_impossible_jump(
                values=values,
                field=field,
                label=label,
                maximum_jump=maximum_jump,
                unit=unit,
            )
 
            findings.extend(new_findings)
            total_penalty += penalty
 
        score = round(
            max(0.0, min(100.0, 100.0 - total_penalty)),
            2,
        )
 
        if not findings:
            findings.append(
                Finding(
                    code="SENSOR_HEALTH_OK",
                    severity="INFO",
                    confidence=95.0,
                    explanation=(
                        "All monitored sensor signals are "
                        "available, plausible and responsive."
                    ),
                    metric="sensor_health_score",
                    value=score,
                    unit="%",
                )
            )
 
        return SensorHealthResult(
            score=score,
            findings=findings,
        )