"""Heating application entry point and DI container for Intelligent Heating Pilot."""

from __future__ import annotations

import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util

from .application.heating_cycle_lifecycle_manager import HeatingCycleLifecycleManager
from .application.heating_cycle_lifecycle_manager_factory import (
    HeatingCycleLifecycleManagerFactory,
)
from .application.lhs_lifecycle_manager import LhsLifecycleManager
from .application.lhs_lifecycle_manager_factory import LhsLifecycleManagerFactory
from .application.orchestrator import HeatingOrchestrator
from .application.use_cases import (
    CalculateAnticipationUseCase,
    CheckOvershootRiskUseCase,
    ControlPreheatingUseCase,
    ScheduleAnticipationActionUseCase,
    UpdateCacheDataUseCase,
)
from .const import CONF_IHP_ENABLED, DECISION_MODE_SIMPLE, DOMAIN, EVENT_DEAD_TIME_UPDATED
from .domain.interfaces.device_config_reader_interface import DeviceConfig
from .domain.services import (
    ContextualLHSCalculatorService,
    DeadTimeCalculationService,
    GlobalLHSCalculatorService,
    HeatingCycleService,
    PredictionService,
)
from .infrastructure.adapters import (
    HAClimateCommander,
    HAClimateDataReader,
    HAContextReader,
    HAEnvironmentReader,
    HAHeatingCycleStorage,
    HALhsStorage,
    HASchedulerCommander,
    HASchedulerReader,
    HATimerScheduler,
)
from .infrastructure.event_bridge import HAEventBridge

_LOGGER = logging.getLogger(__name__)


class HeatingApplication:
    """Heating application entry point and DI container.

    This class:
    - Creates and wires adapters
    - Creates application service
    - Setups event bridge
    - Exposes data for sensors (via application service)

    NO business logic - pure dependency injection and lifecycle management.
    """

    def __init__(
        self,
        hass: HomeAssistant,
        device_config: DeviceConfig,
    ) -> None:
        """Initialize the coordinator with dependency injection.

        Args:
            hass: Home Assistant instance
            device_config: Complete device configuration (injected, not read from config_entry)

        NOTE: config_entry is NOT passed here. This respects DDD principles:
        - The domain/application layer does NOT depend on HA infrastructure (ConfigEntry)
        - All configuration comes from the value object (DeviceConfig)
        - For entry_id-based operations, use device_config.device_id
        """
        _LOGGER.debug("Initializing HeatingApplication with injected DeviceConfig")

        self.hass = hass
        self._device_id = device_config.device_id  # Store entry ID for adapter creation

        # Store immutable device configuration
        self._device_config = device_config

        # Extract configuration from DeviceConfig (NOT from config_entry!)
        # This is the key change: we read from the injected value object
        self._vtherm_id = device_config.vtherm_entity_id
        self._scheduler_ids = device_config.scheduler_entities
        self._humidity_in_id = device_config.humidity_in_entity_id
        self._humidity_out_id = device_config.humidity_out_entity_id
        self._temperature_out_id = device_config.temperature_out_entity_id
        self._cloud_cover_id = device_config.cloud_cover_entity_id
        self._data_retention_days = device_config.lhs_retention_days
        self._decision_mode = DECISION_MODE_SIMPLE

        # Heating cycle detection parameters
        self._temp_delta_threshold = device_config.temp_delta_threshold
        self._cycle_split_duration_minutes = device_config.cycle_split_duration_minutes
        self._min_cycle_duration_minutes = device_config.min_cycle_duration_minutes
        self._max_cycle_duration_minutes = device_config.max_cycle_duration_minutes
        self._dead_time_minutes = device_config.dead_time_minutes
        self._auto_learning = device_config.auto_learning
        self._safety_shutoff_grace_minutes = device_config.safety_shutoff_grace_minutes

        # IHP enabled state
        self._ihp_enabled = device_config.ihp_enabled
        self._last_known_dead_time: float | None = None

        # Infrastructure adapters
        self._lhs_storage: HALhsStorage | None = None
        self._cycle_storage: HAHeatingCycleStorage | None = None
        self._scheduler_reader: HASchedulerReader | None = None
        self._scheduler_commander: HASchedulerCommander | None = None
        self._climate_commander: HAClimateCommander | None = None
        self._environment_reader: HAEnvironmentReader | None = None
        self._context_reader: HAContextReader | None = None
        self._climate_data_reader: HAClimateDataReader | None = None
        self._timer_scheduler: HATimerScheduler | None = None

        # Orchestrator (coordinates use cases)
        self._orchestrator: Any | None = None

        # Lifecycle managers
        self._heating_cycle_manager: HeatingCycleLifecycleManager | None = None
        self._lhs_manager: LhsLifecycleManager | None = None

        # Event bridge
        self._event_bridge: HAEventBridge | None = None

        # Cached data for sensors (refreshed by application service)
        self._last_anticipation_data: dict[str, Any] | None = None
        self._lhs_cache: float = 2.0  # Default global LHS
        self._contextual_lhs_cache: dict[int, float] = {}  # Contextual LHS by hour (0-23)

        # Config entry for options updates (set later via setup_config_entry_access)
        self._config_entry: ConfigEntry | None = None
        self._options_snapshot: dict[str, Any] | None = None

    async def async_load(self) -> None:
        """Load and initialize all components."""
        # Create infrastructure adapters
        self._lhs_storage = HALhsStorage(
            self.hass, self._device_id, retention_days=self._data_retention_days
        )

        # Create cycle cache for incremental cycle extraction
        self._cycle_storage = HAHeatingCycleStorage(
            self.hass, self._device_id, retention_days=self._data_retention_days
        )

        # Import recorder queue for HAClimateDataReader
        from .infrastructure.recorder_queue import get_recorder_queue

        # Create climate data reader first so it can be injected into scheduler_reader
        self._climate_data_reader = HAClimateDataReader(
            self.hass, get_recorder_queue(self.hass), self._vtherm_id
        )

        self._scheduler_reader = HASchedulerReader(
            self.hass,
            self._scheduler_ids,
            vtherm_entity_id=self._vtherm_id,
            climate_reader=self._climate_data_reader,
        )

        self._scheduler_commander = HASchedulerCommander(self.hass)

        self._climate_commander = HAClimateCommander(self.hass, self._vtherm_id)
        self._environment_reader = HAEnvironmentReader(
            self.hass,
            self._vtherm_id,
            outdoor_temp_entity_id=self._temperature_out_id,
            humidity_in_entity_id=self._humidity_in_id,
            humidity_out_entity_id=self._humidity_out_id,
            cloud_cover_entity_id=self._cloud_cover_id,
        )
        self._context_reader = HAContextReader(
            self.hass,
            outdoor_temp_entity_id=self._temperature_out_id,
            humidity_in_entity_id=self._humidity_in_id,
            humidity_out_entity_id=self._humidity_out_id,
            cloud_cover_entity_id=self._cloud_cover_id,
        )

        # Create timer scheduler adapter
        self._timer_scheduler = HATimerScheduler(self.hass)

        # Create domain services (they're stateless, can be created once)
        heating_cycle_service = HeatingCycleService(
            temp_delta_threshold=self._temp_delta_threshold,
            cycle_split_duration_minutes=self._cycle_split_duration_minutes,
            min_cycle_duration_minutes=self._min_cycle_duration_minutes,
            max_cycle_duration_minutes=self._max_cycle_duration_minutes,
            safety_shutoff_grace_minutes=self._safety_shutoff_grace_minutes,
        )
        global_lhs_calculator = GlobalLHSCalculatorService()
        contextual_lhs_calculator = ContextualLHSCalculatorService()

        # Create LHS lifecycle manager FIRST (required by heating_cycle_manager)
        self._lhs_manager = LhsLifecycleManagerFactory.create(
            model_storage=self._lhs_storage,
            global_lhs_calculator=global_lhs_calculator,
            contextual_lhs_calculator=contextual_lhs_calculator,
            timer_scheduler=self._timer_scheduler,
        )

        # Create heating cycle lifecycle manager (after LHS manager is available)
        self._heating_cycle_manager = HeatingCycleLifecycleManagerFactory.create(
            hass=self.hass,
            device_config=self._device_config,
            heating_cycle_service=heating_cycle_service,
            cycle_cache=self._cycle_storage,
            timer_scheduler=self._timer_scheduler,
            model_storage=self._lhs_storage,
            lhs_lifecycle_manager=self._lhs_manager,
            dead_time_updated_callback=self._fire_dead_time_updated_event,
            on_extraction_complete_callback=self._on_extraction_complete,
        )

        # Create use cases for orchestrator
        _LOGGER.debug("Creating use cases for orchestrator")

        # Create services directly (they're simple domain services, not state-dependent)
        prediction_service = PredictionService()
        dead_time_calculator = DeadTimeCalculationService()

        calculate_anticipation = CalculateAnticipationUseCase(
            scheduler_reader=self._scheduler_reader,
            environment_reader=self._environment_reader,
            climate_data_reader=self._climate_data_reader,
            heating_cycle_manager=self._heating_cycle_manager,
            lhs_lifecycle_manager=self._lhs_manager,
            prediction_service=prediction_service,
            dead_time_calculator=dead_time_calculator,
            auto_learning=self._auto_learning,
            default_dead_time_minutes=self._dead_time_minutes,
            lhs_storage=self._lhs_storage,
        )

        control_preheating = ControlPreheatingUseCase(
            scheduler_commander=self._scheduler_commander,
        )

        schedule_anticipation_action = ScheduleAnticipationActionUseCase(
            scheduler_reader=self._scheduler_reader,
            scheduler_commander=self._scheduler_commander,
            timer_scheduler=self._timer_scheduler,
            control_preheating_use_case=control_preheating,  # Delegate state management
            anticipation_recalc_tolerance_minutes=self._device_config.anticipation_recalc_tolerance_minutes,
        )

        check_overshoot_risk = CheckOvershootRiskUseCase(
            scheduler_reader=self._scheduler_reader,
            environment_reader=self._environment_reader,
            climate_data_reader=self._climate_data_reader,
            control_preheating=control_preheating,
        )

        update_cache = UpdateCacheDataUseCase(
            cycle_storage=self._cycle_storage,
            lhs_storage=self._lhs_storage,
            lhs_lifecycle_manager=self._lhs_manager,
        )

        # Create orchestrator
        self._orchestrator = HeatingOrchestrator(
            calculate_anticipation=calculate_anticipation,
            control_preheating=control_preheating,
            schedule_anticipation_action=schedule_anticipation_action,
            check_overshoot_risk=check_overshoot_risk,
            update_cache=update_cache,
        )
        _LOGGER.debug("Orchestrator created successfully")

        # Create event bridge
        monitored_entities = []
        if self._humidity_in_id:
            monitored_entities.append(self._humidity_in_id)
        if self._humidity_out_id:
            monitored_entities.append(self._humidity_out_id)
        if self._cloud_cover_id:
            monitored_entities.append(self._cloud_cover_id)

        self._event_bridge = HAEventBridge(
            self.hass,
            self._orchestrator,
            self._vtherm_id,
            self._scheduler_ids,
            monitored_entities,
            entry_id=self._device_id,
            get_ihp_enabled_func=self.is_ihp_enabled,
        )

        # Load initial data
        if self._lhs_manager:
            self._lhs_cache = await self._lhs_manager.get_global_lhs()
            # Also load contextual LHS cache for all hours
            await self._load_contextual_lhs_cache()

        # NOTE: Cycle extraction is deferred to async_initialize_cycle_extraction()
        # which is called after HA fully started (EVENT_HOMEASSISTANT_STARTED)
        # to ensure VTherm entity is available before querying its history.
        # See __init__.py async_setup_entry for the deferred initialization logic.

        _LOGGER.info(
            "[%s] Coordinator initialized (VTherm: %s, Schedulers: %d)",
            self._device_id,
            self._vtherm_id,
            len(self._scheduler_ids),
        )

        # NOTE: Initial update is now deferred to async_setup_entry to avoid blocking
        # the config flow during device creation (prevents HA watchdog restart).
        # See async_setup_entry for the deferred update logic.

    async def _load_contextual_lhs_cache(self) -> None:
        """Load contextual LHS values for all hours from storage.

        This populates the synchronous cache with contextual LHS values
        that were previously calculated and stored.
        """
        if not self._lhs_manager:
            return

        try:
            # Load contextual LHS for all 24 hours
            for hour in range(24):
                contextual_lhs = await self._lhs_manager.get_contextual_lhs(
                    target_time=dt_util.now().replace(hour=hour, minute=0, second=0, microsecond=0),
                    cycles=[],  # Empty cycles - will use stored cache
                )
                if contextual_lhs is not None and contextual_lhs != 2.0:  # 2.0 is default value
                    self._contextual_lhs_cache[hour] = contextual_lhs
                    _LOGGER.debug(
                        "Loaded contextual LHS from storage: hour %d = %.2f °C/h",
                        hour,
                        contextual_lhs,
                    )
        except Exception as exc:  # noqa: BLE001
            _LOGGER.warning("Failed to load contextual LHS cache: %s", exc, exc_info=True)

    def setup_config_entry_access(self, config_entry: ConfigEntry) -> None:
        """Set config_entry reference for options updates.

        This is called after async_load to enable set_ihp_enabled to persist
        state changes back to the config entry.

        Args:
            config_entry: The config entry for this device
        """
        self._config_entry = config_entry

    def setup_listeners(self) -> None:
        """Setup event listeners via event bridge."""
        if self._event_bridge:
            self._event_bridge.setup_listeners()

    async def async_initialize_cycle_extraction(self) -> None:
        """Initialize cycle extraction and schedule 24h periodic refresh.

        This method MUST be called after EVENT_HOMEASSISTANT_STARTED to ensure
        the VTherm entity is available before querying its history.

        This method:
        - Verifies VTherm entity is available
        - Performs initial extraction over retention window
        - Schedules 24h periodic refresh timer
        - Logs initialization with retention days
        """
        _LOGGER.debug("Entering async_initialize_cycle_extraction for device=%s", self._device_id)

        try:
            # Check if cycle extraction is enabled
            if self._data_retention_days <= 0:
                _LOGGER.debug(
                    "Cycle extraction disabled (history_lookback_days=%d)",
                    self._data_retention_days,
                )
                return

            # Sanity checks
            if not self._device_config:
                _LOGGER.warning("Cannot initialize cycle refresh: device_config not available")
                return

            if not self._heating_cycle_manager:
                _LOGGER.warning("Cannot initialize cycle refresh: manager not available")
                return

            # Verify VTherm entity exists before attempting to query its history
            vtherm_state = self.hass.states.get(self._vtherm_id)
            if vtherm_state is None:
                _LOGGER.error(
                    "Cannot initialize cycle extraction: VTherm entity %s not found. "
                    "Ensure the climate entity is loaded before IHP starts.",
                    self._vtherm_id,
                )
                return

            _LOGGER.debug(
                "Initializing cycle extraction: device_id=%s, retention=%d days",
                self._device_config.device_id,
                self._data_retention_days,
            )

            # Trigger cache refresh: schedules 24h timer and extracts missing days asynchronously
            await self._heating_cycle_manager.refresh_heating_cycle_cache()

            _LOGGER.info(
                "Cycle extraction initialized: device=%s, retention=%d days (async)",
                self._device_config.device_id,
                self._data_retention_days,
            )

            _LOGGER.debug(
                "Exiting async_initialize_cycle_extraction for device=%s", self._device_id
            )

        except Exception as err:  # pylint: disable=broad-except
            _LOGGER.error(
                "Failed to initialize cycle extraction for %s: %s",
                self._vtherm_id,
                err,
                exc_info=True,
            )

    async def _update_global_lhs_from_cycles(self, cycles: list) -> None:
        """Update global LHS in model storage from extracted cycles.

        This method calculates the global learned heating slope (LHS) from
        all extracted cycles and persists it to model storage for later use
        as a fallback when no contextual data is available.

        Args:
            cycles: List of HeatingCycle objects
        """
        if not cycles or not self._lhs_manager:
            return

        try:
            global_lhs = await self._lhs_manager.update_global_lhs_from_cycles(cycles)

            # Refresh cache so sensors reflect updated value
            await self.refresh_caches()

            _LOGGER.info(
                "Updated global LHS from %d cycles: %.2f°C/h",
                len(cycles),
                global_lhs,
            )

        except Exception as exc:  # pylint: disable=broad-except
            _LOGGER.warning(
                "Failed to update global LHS from cycles: %s",
                exc,
                exc_info=True,
            )

    async def async_notify_retention_change(self, new_retention_days: int) -> None:
        """Handle configuration change for retention/history lookback.

        Called from config flow when history_lookback_days changes.

        Args:
            new_retention_days: New retention window in days
        """
        _LOGGER.debug(
            "Retention change notification: old=%d, new=%d",
            self._data_retention_days,
            new_retention_days,
        )

        try:
            # Update stored retention days
            self._data_retention_days = new_retention_days

            # Delegate to use case for reconfiguration handling
            if self._heating_cycle_manager:
                await self._heating_cycle_manager.on_retention_change(new_retention_days)
            else:
                _LOGGER.debug("No active cycle manager; cannot propagate retention change")

            # Note: LhsLifecycleManager receives cycles from HeatingCycleLifecycleManager
            # No need to call on_retention_change directly since cascade handles it

        except Exception as err:  # pylint: disable=broad-except
            _LOGGER.warning("Error handling retention change: %s", err, exc_info=True)

    async def async_update(self) -> None:
        """Trigger anticipation calculation and cache results for sensors."""
        if not self._orchestrator:
            return

        # Calculate and schedule via orchestrator (passing IHP enabled state)
        anticipation_data = await self._orchestrator.calculate_and_schedule_anticipation(
            ihp_enabled=self._ihp_enabled
        )

        # Cache for sensors
        self._last_anticipation_data = anticipation_data

        # Persist and notify dead_time when freshly calculated and different from last known value
        new_dead_time = anticipation_data.get("dead_time") if anticipation_data else None
        if new_dead_time is not None and new_dead_time != self._last_known_dead_time:
            self._last_known_dead_time = new_dead_time
            if self._lhs_storage:
                await self._lhs_storage.set_learned_dead_time(new_dead_time)
            self._fire_dead_time_updated_event(new_dead_time)

        # Refresh LHS caches
        if self._lhs_manager:
            self._lhs_cache = await self._lhs_manager.get_global_lhs()

            # Reload contextual LHS cache for the scheduled hour only (optimization)
            if anticipation_data and anticipation_data.get("next_schedule_time") is not None:
                next_schedule_time = anticipation_data.get("next_schedule_time")
                if next_schedule_time:
                    scheduled_hour = next_schedule_time.hour
                    try:
                        contextual_lhs = await self._lhs_manager.get_contextual_lhs(
                            target_time=next_schedule_time,
                            cycles=[],  # Empty cycles - will use stored cache
                        )
                        if contextual_lhs is not None and contextual_lhs != 2.0:
                            self._contextual_lhs_cache[scheduled_hour] = contextual_lhs
                            _LOGGER.debug(
                                "Updated contextual LHS cache: hour %d = %.2f °C/h",
                                scheduled_hour,
                                contextual_lhs,
                            )
                    except Exception as exc:  # noqa: BLE001
                        _LOGGER.debug("Failed to update contextual LHS cache: %s", exc)

        # Fire event for sensors
        if anticipation_data:
            # Always publish complete structure with None values for missing data
            anticipated_start = anticipation_data.get("anticipated_start_time")
            next_schedule = anticipation_data.get("next_schedule_time")

            event_data = {
                "entry_id": self._device_id,
                "anticipated_start_time": anticipated_start.isoformat()
                if anticipated_start
                else None,
                "next_schedule_time": next_schedule.isoformat() if next_schedule else None,
                "next_target_temperature": anticipation_data.get("next_target_temperature"),
                "anticipation_minutes": anticipation_data.get("anticipation_minutes"),
                "current_temp": anticipation_data.get("current_temp"),
                "learned_heating_slope": anticipation_data.get("learned_heating_slope"),
                "confidence_level": anticipation_data.get("confidence_level"),
                "scheduler_entity": anticipation_data.get("scheduler_entity", ""),
            }
            self.hass.bus.async_fire(
                f"{DOMAIN}_anticipation_calculated",
                event_data,
            )

    def _on_extraction_complete(self) -> None:
        """Trigger sensor recalculation after background extraction completes."""
        if self._event_bridge is not None:
            self._event_bridge._request_recalculate()

    def _fire_dead_time_updated_event(self, learned_dead_time: float) -> None:
        """Publish an event when learned dead time is persisted."""
        _LOGGER.debug(
            "Publishing dead time update for entry_id=%s: %.1f minutes",
            self._device_id,
            learned_dead_time,
        )
        self.hass.bus.async_fire(
            EVENT_DEAD_TIME_UPDATED,
            {
                "entry_id": self._device_id,
                "learned_dead_time": learned_dead_time,
            },
        )

    async def refresh_caches(self) -> None:
        """Refresh cached LHS value used by sensors.

        Called by sensors after an anticipation event to keep LHS in sync
        when the event publication bypasses the coordinator's async_update path.
        """
        if self._lhs_manager is None:
            return
        try:
            # Reload cached global LHS value
            self._lhs_cache = await self._lhs_manager.get_global_lhs()
        except Exception:  # noqa: BLE001
            _LOGGER.debug("Failed to refresh LHS cache", exc_info=True)

    # Sensor accessors (synchronous for sensor entities)

    def get_learned_heating_slope(self) -> float:
        """Get cached global LHS for sensors."""
        return self._lhs_cache

    def get_contextual_learned_heating_slope(self, hour: int) -> float | None:
        """Get contextual LHS for a specific hour (from cache).

        This returns the contextual LHS from the synchronously-accessible cache.
        Falls back to global LHS if no contextual data is available for the hour.

        Args:
            hour: Hour of day (0-23)

        Returns:
            Contextual LHS for the hour, or global LHS as fallback
        """
        if hour < 0 or hour > 23:
            return self.get_learned_heating_slope()

        try:
            # Check contextual LHS cache first
            if hour in self._contextual_lhs_cache:
                contextual_value = self._contextual_lhs_cache[hour]
                # Return None only if truly no data (None in cache means no contextual data for this hour)
                if contextual_value is None:
                    return self.get_learned_heating_slope()  # Fallback to global
                return contextual_value

            # Not in cache, fallback to global LHS
            return self.get_learned_heating_slope()
        except Exception:  # noqa: BLE001
            _LOGGER.debug("Failed to get LHS for hour %d", hour, exc_info=True)
            return self.get_learned_heating_slope()

    def is_ihp_enabled(self) -> bool:
        """Get IHP enabled state."""
        return self._ihp_enabled

    async def set_ihp_enabled(self, enabled: bool) -> None:
        """Set IHP enabled state.

        Args:
            enabled: True to enable IHP preheating, False to disable
        """
        _LOGGER.info("Setting IHP enabled state to: %s", enabled)
        self._ihp_enabled = enabled

        # Persist state to config entry if available
        # Note: async_update_options() listener handles the state application (cancel or reschedule)
        if self._config_entry:
            new_options = dict(self._config_entry.options) if self._config_entry.options else {}
            new_options[CONF_IHP_ENABLED] = enabled
            self.hass.config_entries.async_update_entry(self._config_entry, options=new_options)

    def get_device_id(self) -> str:
        """Get device identifier (config entry ID)."""
        return self._device_id

    def get_vtherm_entity(self) -> str:
        """Get VTherm entity ID."""
        return self._vtherm_id

    def get_scheduler_entities(self) -> list[str]:
        """Get scheduler entity IDs."""
        return self._scheduler_ids[:]

    def is_auto_learning_enabled(self) -> bool:
        """Check if auto-learning is enabled.

        Returns:
            True if auto_learning is enabled in configuration
        """
        return self._auto_learning

    async def get_current_dead_time(self) -> float | None:
        """Get the current learned dead time value.

        Returns the dead time value persisted from auto-learning.

        Returns:
            Dead time in minutes, or None if not yet learned
        """
        _LOGGER.debug("Entering get_current_dead_time")

        if not self._lhs_storage:
            _LOGGER.debug("No LHS storage available")
            return None

        learned_dead_time = await self._lhs_storage.get_learned_dead_time()
        _LOGGER.debug("Exiting get_current_dead_time: result=%s", learned_dead_time)
        return learned_dead_time

    async def get_effective_dead_time(self) -> float:
        """Get the effective dead time for heating predictions.

        Returns either the auto-learned value or the user-configured value
        depending on the auto_learning configuration flag.

        Returns:
            Dead time in minutes (configured value or learned value)
        """
        _LOGGER.debug(
            "Entering get_effective_dead_time: auto_learning=%s, configured=%.1f",
            self._auto_learning,
            self._dead_time_minutes,
        )

        if self._auto_learning:
            # Use learned value if available, fall back to configured
            learned_dead_time = await self.get_current_dead_time()
            if learned_dead_time is not None:
                _LOGGER.debug(
                    "Exiting get_effective_dead_time: using learned value=%.1f",
                    learned_dead_time,
                )
                return learned_dead_time

        # Fall back to configured value (either auto_learning=False or no learned value yet)
        _LOGGER.debug(
            "Exiting get_effective_dead_time: using configured value=%.1f",
            self._dead_time_minutes,
        )
        return self._dead_time_minutes

    async def async_cleanup(self) -> None:
        """Cleanup coordinator resources.

        Cancels timers, stops cycle extraction, and cleanup event bridge.
        Called when coordinator is being unloaded.
        """
        _LOGGER.debug("Cleaning up coordinator: device_id=%s", self._device_config.device_id)

        try:
            # Clean up event bridge first (cancels pending tasks)
            if self._event_bridge:
                await self._event_bridge.async_cleanup()
                _LOGGER.debug("Event bridge cleanup completed")

            # Stop cycle extraction and cancel timer
            if self._heating_cycle_manager:
                await self._heating_cycle_manager.cancel()
                _LOGGER.debug("Cycle extraction cancelled")

        except Exception as err:  # pylint: disable=broad-except
            _LOGGER.warning("Error during cleanup: %s", err, exc_info=True)
