"""
Equipment-specific operating profile for Sentinel Brain.

This module builds the first Sentinel DNA from historical measurements.
"""

from dataclasses import asdict, dataclass
from statistics import mean, median, pstdev


@dataclass(frozen=True)
class ProfileMetric:
    """
    Statistical profile for one equipment metric.
    """

    mean: float
    median: float
    standard_deviation: float
    minimum: float
    maximum: float
    sample_count: int


@dataclass(frozen=True)
class CycleProfile:
    """
    Provisional compressor cycle profile.

    Values are None when the current signal does not contain enough
    identifiable start and stop transitions.
    """

    current_threshold_a: float
    average_run_minutes: float | None
    average_rest_minutes: float | None
    starts_detected: int
    complete_run_cycles: int
    complete_rest_cycles: int


@dataclass(frozen=True)
class EquipmentProfile:
    """
    Sentinel DNA for one equipment.
    """

    sample_count: int
    internal_temperature: ProfileMetric
    ambient_temperature: ProfileMetric
    evaporator_temperature: ProfileMetric
    compressor_temperature: ProfileMetric
    compressor_current: ProfileMetric
    evaporator_delta: ProfileMetric
    cycle_profile: CycleProfile

    def to_dict(self) -> dict[str, object]:
        """
        Return the profile as a serializable dictionary.
        """
        return asdict(self)


class EquipmentProfileBuilder:
    """
    Build an equipment-specific profile from historical measurements.
    """

    REQUIRED_FIELDS = (
        "recorded_at",
        "internal_temperature_c",
        "ambient_temperature_c",
        "evaporator_temperature_c",
        "compressor_temperature_c",
        "compressor_current_a",
    )

    MINIMUM_SAMPLE_COUNT = 30

    @staticmethod
    def _parse_timestamp(value: object):
        """
        Parse one SQLite/ISO timestamp.
        """
        from datetime import datetime

        timestamp = str(value).strip().replace("Z", "+00:00")
        return datetime.fromisoformat(timestamp)

    @classmethod
    def _valid_rows(
        cls,
        history: list[dict[str, object]],
    ) -> list[dict[str, object]]:
        """
        Keep rows containing all values needed for the equipment profile.
        """
        valid_rows: list[dict[str, object]] = []

        for row in history:
            if any(row.get(field) is None for field in cls.REQUIRED_FIELDS):
                continue

            try:
                valid_rows.append(
                    {
                        "recorded_at": cls._parse_timestamp(
                            row["recorded_at"]
                        ),
                        "internal_temperature_c": float(
                            row["internal_temperature_c"]
                        ),
                        "ambient_temperature_c": float(
                            row["ambient_temperature_c"]
                        ),
                        "evaporator_temperature_c": float(
                            row["evaporator_temperature_c"]
                        ),
                        "compressor_temperature_c": float(
                            row["compressor_temperature_c"]
                        ),
                        "compressor_current_a": float(
                            row["compressor_current_a"]
                        ),
                    }
                )
            except (TypeError, ValueError):
                continue

        return sorted(
            valid_rows,
            key=lambda row: row["recorded_at"],
        )

    @staticmethod
    def _metric(values: list[float]) -> ProfileMetric:
        """
        Build descriptive statistics for one metric.
        """
        if not values:
            raise ValueError("Cannot build a metric from an empty list.")

        deviation = pstdev(values) if len(values) >= 2 else 0.0

        return ProfileMetric(
            mean=round(mean(values), 4),
            median=round(median(values), 4),
            standard_deviation=round(deviation, 4),
            minimum=round(min(values), 4),
            maximum=round(max(values), 4),
            sample_count=len(values),
        )

    @staticmethod
    def _durations_minutes(
        states: list[bool],
        timestamps: list[object],
        expected_state: bool,
    ) -> list[float]:
        """
        Return durations of complete consecutive state sequences.
        """
        durations: list[float] = []
        sequence_start = None

        for index, state in enumerate(states):
            if state == expected_state and sequence_start is None:
                sequence_start = index

            sequence_ends = (
                sequence_start is not None
                and (
                    state != expected_state
                    or index == len(states) - 1
                )
            )

            if not sequence_ends:
                continue

            end_index = index if state == expected_state else index - 1

            if sequence_start > 0 and end_index < len(states) - 1:
                duration = (
                    timestamps[end_index]
                    - timestamps[sequence_start]
                ).total_seconds() / 60.0

                if duration >= 0:
                    durations.append(duration)

            sequence_start = None

        return durations

    @classmethod
    def _cycle_profile(
        cls,
        rows: list[dict[str, object]],
    ) -> CycleProfile:
        """
        Estimate compressor run/rest cycles from current measurements.

        The threshold is derived from the observed current distribution.
        """
        currents = [
            float(row["compressor_current_a"])
            for row in rows
        ]

        timestamps = [
            row["recorded_at"]
            for row in rows
        ]

        current_minimum = min(currents)
        current_maximum = max(currents)
        current_span = current_maximum - current_minimum

        if current_span < 0.10:
            return CycleProfile(
                current_threshold_a=round(current_minimum, 3),
                average_run_minutes=None,
                average_rest_minutes=None,
                starts_detected=0,
                complete_run_cycles=0,
                complete_rest_cycles=0,
            )

        threshold = current_minimum + current_span * 0.35

        running_states = [
            current >= threshold
            for current in currents
        ]

        starts_detected = sum(
            1
            for previous, current in zip(
                running_states,
                running_states[1:],
            )
            if not previous and current
        )

        run_durations = cls._durations_minutes(
            states=running_states,
            timestamps=timestamps,
            expected_state=True,
        )

        rest_durations = cls._durations_minutes(
            states=running_states,
            timestamps=timestamps,
            expected_state=False,
        )

        return CycleProfile(
            current_threshold_a=round(threshold, 3),
            average_run_minutes=(
                round(mean(run_durations), 2)
                if run_durations
                else None
            ),
            average_rest_minutes=(
                round(mean(rest_durations), 2)
                if rest_durations
                else None
            ),
            starts_detected=starts_detected,
            complete_run_cycles=len(run_durations),
            complete_rest_cycles=len(rest_durations),
        )

    @classmethod
    def build(
        cls,
        history: list[dict[str, object]],
    ) -> EquipmentProfile:
        """
        Build the first Sentinel DNA for one equipment.
        """
        rows = cls._valid_rows(history)

        if len(rows) < cls.MINIMUM_SAMPLE_COUNT:
            raise ValueError(
                "At least "
                f"{cls.MINIMUM_SAMPLE_COUNT} valid measurements "
                "are required to build an equipment profile."
            )

        internal_values = [
            float(row["internal_temperature_c"])
            for row in rows
        ]

        ambient_values = [
            float(row["ambient_temperature_c"])
            for row in rows
        ]

        evaporator_values = [
            float(row["evaporator_temperature_c"])
            for row in rows
        ]

        compressor_temperature_values = [
            float(row["compressor_temperature_c"])
            for row in rows
        ]

        compressor_current_values = [
            float(row["compressor_current_a"])
            for row in rows
        ]

        evaporator_delta_values = [
            float(row["internal_temperature_c"])
            - float(row["evaporator_temperature_c"])
            for row in rows
        ]

        return EquipmentProfile(
            sample_count=len(rows),
            internal_temperature=cls._metric(
                internal_values
            ),
            ambient_temperature=cls._metric(
                ambient_values
            ),
            evaporator_temperature=cls._metric(
                evaporator_values
            ),
            compressor_temperature=cls._metric(
                compressor_temperature_values
            ),
            compressor_current=cls._metric(
                compressor_current_values
            ),
            evaporator_delta=cls._metric(
                evaporator_delta_values
            ),
            cycle_profile=cls._cycle_profile(rows),
        )
