"""Prediction service for calculating heating times."""

from __future__ import annotations

import logging
from datetime import datetime, timedelta

from ..constants import (
    BASE_HIGH_CONFIDENCE,
    BASE_LOW_CONFIDENCE,
    BASE_MEDIUM_CONFIDENCE,
    CLOUD_COVERAGE_FACTOR,
    CONFIDENCE_BOOST_PER_SENSOR,
    DEFAULT_ANTICIPATION_BUFFER,
    HIGH_CONFIDENCE_SLOPE,
    HUMIDITY_FACTOR,
    HUMIDITY_REFERENCE,
    MAX_ANTICIPATION_TIME,
    MEDIUM_CONFIDENCE_SLOPE,
    MIN_ANTICIPATION_TIME,
    OUTDOOR_TEMP_FACTOR,
    OUTDOOR_TEMP_REFERENCE,
)
from ..value_objects import PredictionResult

_LOGGER = logging.getLogger(__name__)


class PredictionService:
    """Service for predicting heating start times.

    This service contains the core prediction algorithm that determines
    when heating should start to reach target temperature at a scheduled time.

    The calculation considers:
    1. Temperature difference to heat
    2. Outdoor temperature impact on heat loss
    3. Humidity effects on heating efficiency
    4. Solar gain from cloud coverage
    5. Learned heating slope (heating rate) from historical data
    """

    def predict_heating_time(
        self,
        current_temp: float | None,
        target_temp: float,
        learned_slope: float,
        target_time: datetime,
        outdoor_temp: float | None = None,
        humidity: float | None = None,
        cloud_coverage: float | None = None,
        dead_time_minutes: float = 0.0,
    ) -> PredictionResult:
        """Calculate when heating should start.

        Args:
            current_temp: Current room temperature in Celsius (None = cannot calculate)
            target_temp: Target temperature in Celsius
            learned_slope: Learned heating slope in °C/hour
            target_time: When target should be reached (mandatory)
            outdoor_temp: Outdoor temperature in Celsius (optional)
            humidity: Indoor humidity percentage (0-100) (optional)
            cloud_coverage: Cloud coverage percentage (0-100, 0=clear sky) (optional)
            dead_time_minutes: Dead time in minutes (initial period with minimal heating effect)

        Returns:
            Prediction result with start time and confidence
        """
        # Handle missing current temperature
        if current_temp is None:
            _LOGGER.warning("Cannot calculate prediction: current_temp is None")
            return PredictionResult(
                anticipated_start_time=target_time,
                estimated_duration_minutes=0.0,
                confidence_level=0.0,
                learned_heating_slope=learned_slope,
            )

        # Calculate temperature difference
        temp_delta = target_temp - current_temp

        if temp_delta <= 0:
            # Already at target, anticipated start time = target time
            _LOGGER.debug(
                "Already at target temperature (%.1f°C >= %.1f°C), no heating needed",
                current_temp,
                target_temp,
            )
            return PredictionResult(
                anticipated_start_time=target_time,
                estimated_duration_minutes=0.0,
                confidence_level=1.0,
                learned_heating_slope=learned_slope,
            )

        # Protection against invalid slope (should not happen with proper validation)
        if learned_slope is None or learned_slope <= 0:
            _LOGGER.error(
                "CRITICAL: Invalid learned heating slope (%.4f°C/h <= 0) reached prediction_service. "
                "This indicates missing validation in calling code. Cannot calculate prediction.",
                learned_slope or 0,
            )
            return PredictionResult(
                anticipated_start_time=target_time,
                estimated_duration_minutes=0.0,
                confidence_level=0.0,
                learned_heating_slope=learned_slope or 0,
            )

        # Calculate base anticipation time (in minutes)
        # Formula: heating_time = dead_time + (temp_delta / slope) * 60
        anticipation_minutes = dead_time_minutes + (temp_delta / learned_slope) * 60.0
        # Apply environmental correction factors
        correction_factor = self._calculate_environmental_correction(
            outdoor_temp, humidity, cloud_coverage
        )

        anticipation_minutes *= correction_factor

        # Apply buffer and limits
        anticipation_minutes += DEFAULT_ANTICIPATION_BUFFER
        anticipation_minutes = max(
            MIN_ANTICIPATION_TIME, min(MAX_ANTICIPATION_TIME, anticipation_minutes)
        )

        # Calculate anticipated start time
        anticipated_start = target_time - timedelta(minutes=anticipation_minutes)

        # Calculate confidence level based on slope and available environmental data
        confidence = self._calculate_confidence(learned_slope, outdoor_temp, humidity)

        _LOGGER.debug(
            "Prediction: ΔT=%.1f°C, slope=%.2f°C/h, dead_time=%.1f min, correction=%.2f, "
            "duration=%.1f min, confidence=%.2f",
            temp_delta,
            learned_slope,
            dead_time_minutes,
            correction_factor,
            anticipation_minutes,
            confidence,
        )

        return PredictionResult(
            anticipated_start_time=anticipated_start,
            estimated_duration_minutes=anticipation_minutes,
            confidence_level=confidence,
            learned_heating_slope=learned_slope,
        )

    def _calculate_environmental_correction(
        self,
        outdoor_temp: float | None,
        humidity: float | None,
        cloud_coverage: float | None,
    ) -> float:
        """Calculate combined environmental correction factor.

        This method combines multiple environmental factors that affect
        heating efficiency:
        - Outdoor temperature (heat loss)
        - Indoor humidity (thermal mass effect)
        - Cloud coverage (solar gain)

        Args:
            outdoor_temp: Outdoor temperature in Celsius
            humidity: Indoor humidity percentage (0-100)
            cloud_coverage: Cloud coverage percentage (0-100)

        Returns:
            Combined correction factor (>1 means slower heating)
        """
        correction_factor = 1.0

        # Outdoor temperature factor: colder outside means slower heating
        # Formula: outdoor_factor = 1 + (OUTDOOR_TEMP_REFERENCE - outdoor_temp) * OUTDOOR_TEMP_FACTOR
        # At outdoor_temp = 20°C: factor = 1.0 (no impact)
        # At outdoor_temp = 0°C: factor = 2.0 (heating takes twice as long)
        # At outdoor_temp = -10°C: factor = 2.5 (even slower)
        if outdoor_temp is not None:
            outdoor_factor = 1.0 + (OUTDOOR_TEMP_REFERENCE - outdoor_temp) * OUTDOOR_TEMP_FACTOR
            outdoor_factor = max(0.5, outdoor_factor)  # Minimum factor of 0.5
            correction_factor *= outdoor_factor
            _LOGGER.debug("Outdoor temp %.1f°C -> factor %.2f", outdoor_temp, outdoor_factor)

        # Humidity factor: higher humidity makes heating feel slower
        # Formula: humidity_factor = 1 + (humidity - HUMIDITY_REFERENCE) * HUMIDITY_FACTOR
        # At 50% humidity: factor = 1.0 (neutral)
        # At 80% humidity: factor = 1.06 (6% slower)
        # At 20% humidity: factor = 0.94 (6% faster)
        if humidity is not None:
            humidity_factor = 1.0 + (humidity - HUMIDITY_REFERENCE) * HUMIDITY_FACTOR
            humidity_factor = max(0.8, min(1.2, humidity_factor))
            correction_factor *= humidity_factor
            _LOGGER.debug("Humidity %.1f%% -> factor %.2f", humidity, humidity_factor)

        # Solar gain factor: less cloud coverage means more solar heat gain
        # Formula: solar_factor = 1 - (100 - cloud_coverage) * CLOUD_COVERAGE_FACTOR
        # At 100% cloud: factor = 1.0 (no solar gain)
        # At 0% cloud (clear sky): factor = 0.9 (10% faster due to sun)
        # At 50% cloud: factor = 0.95 (5% faster)
        if cloud_coverage is not None:
            solar_factor = 1.0 - (100.0 - cloud_coverage) * CLOUD_COVERAGE_FACTOR
            solar_factor = max(0.8, min(1.0, solar_factor))
            correction_factor *= solar_factor
            _LOGGER.debug("Cloud coverage %.1f%% -> factor %.2f", cloud_coverage, solar_factor)

        return correction_factor

    def _calculate_confidence(
        self,
        learned_slope: float,
        outdoor_temp: float | None,
        humidity: float | None,
    ) -> float:
        """Calculate confidence level in the prediction.

        Confidence is based on:
        - Slope validity (higher slope = better learning)
        - Available environmental data (more data = better prediction)

        Args:
            learned_slope: Learned heating slope in °C/hour
            outdoor_temp: Outdoor temperature (if available)
            humidity: Indoor humidity (if available)

        Returns:
            Confidence level between 0.0 and 1.0
        """
        # Base confidence from slope validity
        if learned_slope > HIGH_CONFIDENCE_SLOPE:
            confidence = BASE_HIGH_CONFIDENCE
        elif learned_slope > MEDIUM_CONFIDENCE_SLOPE:
            confidence = BASE_MEDIUM_CONFIDENCE
        else:
            confidence = BASE_LOW_CONFIDENCE

        # Adjust confidence based on available environmental data
        data_availability = 0
        if outdoor_temp is not None:
            data_availability += 1
        if humidity is not None:
            data_availability += 1

        # Increase confidence slightly with more environmental data
        confidence += data_availability * CONFIDENCE_BOOST_PER_SENSOR

        # Cap at 1.0
        return min(1.0, confidence)
