"""Home Assistant scheduler reader adapter.

This adapter implements ISchedulerReader by reading from Home Assistant
scheduler entities. It translates HA entity states into domain value objects.
"""

from __future__ import annotations

import logging
from datetime import datetime
from typing import TYPE_CHECKING, cast

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

from ...domain.interfaces.climate_data_reader_interface import IClimateDataReader
from ...domain.interfaces.scheduler_reader_interface import ISchedulerReader
from ...domain.value_objects import ScheduledTimeslot
from ..vtherm_compat import get_vtherm_attribute
from .utils import get_entity_name

if TYPE_CHECKING:
    from homeassistant.core import State

_LOGGER = logging.getLogger(__name__)

# Default temperature used for native HA schedule entities that don't store temperature.
# The actual temperature is resolved from the linked VTherm entity when available.
_DEFAULT_NATIVE_SCHEDULE_TEMPERATURE: float = 20.0


class HASchedulerReader(ISchedulerReader):
    """Home Assistant implementation of scheduler reader.

    Reads scheduled heating timeslots from Home Assistant scheduler entities.
    Supports the scheduler-component data format:
    https://github.com/nielsfaber/scheduler-component/#data-format

    This adapter contains NO business logic - it only translates HA states
    to domain value objects.
    """

    def __init__(
        self,
        hass: HomeAssistant,
        scheduler_entity_ids: list[str],
        vtherm_entity_id: str | None = None,
        climate_reader: IClimateDataReader | None = None,
    ) -> None:
        """Initialize the scheduler reader adapter.

        Args:
            hass: Home Assistant instance
            scheduler_entity_ids: List of scheduler entity IDs to monitor
            vtherm_entity_id: Optional VTherm climate entity ID used to resolve
                preset temperatures (e.g., when actions use preset modes).
            climate_reader: Optional climate data reader used to resolve the
                current VTherm target temperature for native HA schedule entities
                (schedule.*) that do not store a temperature themselves.
        """
        self._hass = hass
        self._scheduler_entity_ids = scheduler_entity_ids
        self._vtherm_entity_id = vtherm_entity_id
        self._climate_reader = climate_reader

    async def get_next_timeslot(self) -> ScheduledTimeslot | None:
        """Retrieve the next scheduled heating timeslot.

        Scans all configured scheduler entities and returns the earliest
        upcoming timeslot with a valid time and temperature.

        Supports both HACS Scheduler (switch.*) and native HA Schedule (schedule.*)
        entities. Native schedules use the next_event attribute and require special
        state-aware handling since "off" means "not in active timeslot", not disabled.

        Returns:
            The next schedule timeslot, or None if no valid timeslots found
            or if no scheduler entities are configured.
        """
        if not self._scheduler_entity_ids or len(self._scheduler_entity_ids) == 0:
            _LOGGER.debug("No scheduler entities configured - scheduler is optional")
            return None

        chosen_time: datetime | None = None
        chosen_temp: float | None = None
        chosen_entity: str | None = None

        for entity_id in self._scheduler_entity_ids:
            state = self._hass.states.get(entity_id)
            if not state:
                # Use debug level if HA is still starting up, warning otherwise
                device_name = get_entity_name(self._hass, entity_id)
                if self._hass.is_running:
                    _LOGGER.warning("[%s] Scheduler entity not found", device_name)
                else:
                    _LOGGER.debug(
                        "[%s] Scheduler entity not yet available (HA starting)", device_name
                    )
                continue

            # Native HA schedule entities (schedule.*) use next_event and state-aware logic
            if entity_id.startswith("schedule."):
                next_time, target_temp = self._extract_native_schedule_data(state)
            else:
                # Skip disabled HACS schedulers (state is "off")
                if state.state == "off":
                    device_name = get_entity_name(self._hass, entity_id)
                    _LOGGER.debug(
                        "[%s] Scheduler is disabled (state: off), skipping", device_name
                    )
                    continue

                # Extract next trigger time and target temperature (HACS format)
                next_time, target_temp = self._extract_timeslot_data(state)

            if next_time and target_temp is not None:
                # Keep track of the earliest timeslot
                if not chosen_time or next_time < chosen_time:
                    chosen_time = next_time
                    chosen_temp = target_temp
                    chosen_entity = entity_id
            else:
                _LOGGER.debug(
                    "Skipped scheduler %s: time=%s temp=%s", entity_id, next_time, target_temp
                )

        if chosen_time and chosen_temp is not None and chosen_entity:
            device_name = get_entity_name(self._hass, chosen_entity)
            _LOGGER.info(
                "[%s] Next timeslot at %s (%.1f°C)",
                device_name,
                chosen_time.strftime("%H:%M"),
                chosen_temp,
            )
            return ScheduledTimeslot(
                target_time=chosen_time,
                target_temp=chosen_temp,
                timeslot_id=f"{chosen_entity}_{chosen_time.isoformat()}",
                scheduler_entity=chosen_entity,
            )

        _LOGGER.debug(
            "No valid scheduler timeslot found (check scheduler configuration and preset temperatures)"
        )
        return None

    def _extract_timeslot_data(self, state: State) -> tuple[datetime | None, float | None]:
        """Extract next trigger time and target temperature from scheduler state.

        Supports multiple scheduler attribute layouts:
        - Standard: next_trigger + next_slot + actions
        - Fallback: next_entries with time and actions

        Args:
            state: Home Assistant scheduler entity state

        Returns:
            Tuple of (next_time, target_temp), either can be None if not found
        """
        attrs = state.attributes

        # Debug logging to understand the actual structure
        _LOGGER.debug(
            "Scheduler %s attributes: next_trigger=%s, next_slot=%s, actions=%s, next_entries=%s",
            state.entity_id,
            attrs.get("next_trigger"),
            attrs.get("next_slot"),
            type(attrs.get("actions")),
            type(attrs.get("next_entries")),
        )

        # Try standard format first
        next_time = self._parse_next_trigger(attrs.get("next_trigger"))
        target_temp = self._extract_target_temp_standard(attrs)

        # Fallback to next_entries format
        if not next_time or target_temp is None:
            next_time_fallback, target_temp_fallback = self._extract_from_next_entries(attrs)
            if not next_time:
                next_time = next_time_fallback
            if target_temp is None:
                target_temp = target_temp_fallback

        return next_time, target_temp

    def _parse_next_trigger(self, next_trigger_raw: str | None) -> datetime | None:
        """Parse next_trigger attribute to datetime.

        Args:
            next_trigger_raw: Raw next_trigger value from scheduler

        Returns:
            Parsed datetime with timezone, or None if parsing fails
        """
        if not next_trigger_raw:
            return None

        # Try HA's robust datetime parser first
        parsed = dt_util.parse_datetime(str(next_trigger_raw))

        # Fallback to ISO format parsing
        if parsed is None:
            try:
                parsed = datetime.fromisoformat(str(next_trigger_raw))
            except ValueError:
                _LOGGER.debug("Failed to parse next_trigger: %s", next_trigger_raw)
                return None

        # Ensure timezone is set
        if parsed and parsed.tzinfo is None:
            parsed = dt_util.as_local(parsed)

        return cast(datetime, parsed) if parsed else None

    def _extract_target_temp_standard(self, attrs: dict) -> float | None:
        """Extract target temperature from standard scheduler format.

        Uses next_slot index to find the action in the actions list.

        Args:
            attrs: Scheduler entity attributes

        Returns:
            Target temperature in Celsius, or None if not found
        """
        next_slot = attrs.get("next_slot")
        actions = attrs.get("actions")

        if not isinstance(actions, list):
            return None

        if not isinstance(next_slot, int) or next_slot < 0 or next_slot >= len(actions):
            return None

        action = actions[next_slot]
        return self._extract_temp_from_action(action)

    def _extract_from_next_entries(self, attrs: dict) -> tuple[datetime | None, float | None]:
        """Extract time and temperature from next_entries fallback format.

        Args:
            attrs: Scheduler entity attributes

        Returns:
            Tuple of (next_time, target_temp)
        """
        next_entries = attrs.get("next_entries")

        if not isinstance(next_entries, list) or not next_entries:
            return None, None

        entry = next_entries[0]

        # Extract time
        time_raw = entry.get("time") or entry.get("start") or entry.get("trigger_time")
        next_time = self._parse_next_trigger(time_raw) if time_raw else None

        # Extract temperature from first action
        entry_actions = entry.get("actions", [])
        target_temp = None
        if isinstance(entry_actions, list) and entry_actions:
            target_temp = self._extract_temp_from_action(entry_actions[0])

        return next_time, target_temp

    def _extract_temp_from_action(self, action: dict) -> float | None:
        """Extract target temperature from a scheduler action.

        Supports:
        - climate.set_temperature with direct temperature value
        - climate.set_preset_mode with preset mapped to temperature

        Args:
            action: Scheduler action dictionary

        Returns:
            Target temperature in Celsius, or None if not found
        """
        if not isinstance(action, dict):
            return None

        service = action.get("service") or action.get("service_call")
        data = action.get("data") or action.get("service_data") or {}

        # Direct temperature setting
        if service == "climate.set_temperature":
            temp = data.get("temperature")
            if temp is not None:
                try:
                    return float(temp)
                except (ValueError, TypeError):
                    _LOGGER.warning("Invalid temperature in action: %s", temp)
            return None

        # Preset mode requires mapping via VTherm attributes
        # This is handled by getting the current VTherm state when the preset is active
        if service == "climate.set_preset_mode":
            preset = data.get("preset_mode") or data.get("preset") or data.get("mode")
            if isinstance(preset, str):
                # Try resolving using VTherm attributes if available
                resolved = self._resolve_preset_temperature(preset)
                if resolved is not None:
                    return resolved
                _LOGGER.debug(
                    "Could not resolve preset '%s' to temperature (entity=%s)",
                    preset,
                    self._vtherm_entity_id,
                )
            return None

        return None

    def _extract_native_schedule_data(self, state: State) -> tuple[datetime | None, float | None]:
        """Extract next timeslot from a native HA schedule entity.

        Native HA schedule entities (schedule.*) use the next_event attribute,
        which always points to the NEXT state change:
        - State = "off" → next_event = next ON time (use for preheating anticipation)
        - State = "on"  → next_event = next OFF time (skip - already in active period)

        Args:
            state: Native HA schedule entity state

        Returns:
            Tuple of (next_time, target_temp), either can be None
        """
        # Use the already-available state object to derive a friendly device name
        device_name = cast(str, state.attributes.get("friendly_name") or state.entity_id)

        if state.state != "off":
            # Schedule is ON, next_event points to next OFF time - not useful for preheating
            _LOGGER.debug(
                "[%s] Native schedule is ON (next_event = next OFF time), skipping for preheating",
                device_name,
            )
            return None, None

        # Schedule is OFF, next_event is the next ON time - use for preheating
        attrs = state.attributes
        next_time = self._parse_datetime_value(attrs.get("next_event"))
        if not next_time:
            _LOGGER.debug("[%s] Native schedule has no valid next_event attribute", device_name)
            return None, None

        target_temp = self._get_native_schedule_temperature()
        _LOGGER.debug(
            "[%s] Native schedule next ON event at %s (%.1f°C)",
            device_name,
            next_time.strftime("%H:%M"),
            target_temp,
        )
        return next_time, target_temp

    def _parse_datetime_value(self, value: object) -> datetime | None:
        """Parse a datetime value that can be either a datetime object or an ISO string.

        Native HA schedule entities can return next_event as either a datetime
        object or an ISO format string, so both formats must be handled.

        Args:
            value: Value to parse (datetime object or string)

        Returns:
            Parsed datetime with timezone, or None if parsing fails
        """
        if value is None:
            return None

        if isinstance(value, datetime):
            # Already a datetime - ensure timezone is set
            if value.tzinfo is None:
                return dt_util.as_local(value)
            return value

        # Try string parsing using existing method
        return self._parse_next_trigger(str(value))

    def _get_native_schedule_temperature(self) -> float:
        """Get target temperature for native HA schedule entities.

        Since native HA schedules don't store temperature, retrieves it from the
        injected climate data reader (IClimateDataReader), which is the designated
        adapter for reading VTherm state. Falls back to
        _DEFAULT_NATIVE_SCHEDULE_TEMPERATURE if no climate reader is configured or
        the VTherm temperature cannot be resolved.

        Returns:
            Target temperature in Celsius from VTherm, or the default value
        """
        if self._climate_reader is None:
            _LOGGER.debug(
                "No climate reader configured for native schedule temperature resolution, "
                "using %.1f°C default",
                _DEFAULT_NATIVE_SCHEDULE_TEMPERATURE,
            )
            return _DEFAULT_NATIVE_SCHEDULE_TEMPERATURE

        temp = self._climate_reader.get_current_target_temperature()
        if temp is not None:
            _LOGGER.debug(
                "Native schedule temperature resolved from climate reader: %.1f°C",
                temp,
            )
            return temp

        _LOGGER.debug(
            "Could not resolve temperature from climate reader, using %.1f°C default",
            _DEFAULT_NATIVE_SCHEDULE_TEMPERATURE,
        )
        return _DEFAULT_NATIVE_SCHEDULE_TEMPERATURE

    async def is_scheduler_enabled(self, scheduler_entity_id: str) -> bool:
        """Check if a specific scheduler is enabled.

        For native HA schedule entities (schedule.*), the "off" state means the
        schedule is not in an active timeslot — not that it is disabled.  These
        entities are therefore always considered enabled *when the entity exists
        and is available*.  Returns False if the entity cannot be found.

        For HACS switch-based schedulers, a scheduler is considered enabled if
        its state is NOT "off". States like "on", "idle", "waiting" are enabled.

        Args:
            scheduler_entity_id: The scheduler entity ID to check

        Returns:
            True if the scheduler is enabled, False otherwise
        """
        state = self._hass.states.get(scheduler_entity_id)
        if not state:
            _LOGGER.debug("Scheduler entity not found when checking state: %s", scheduler_entity_id)
            return False

        # Native HA schedules: "off" means outside an active timeslot, not disabled
        if scheduler_entity_id.startswith("schedule."):
            if state.state == "unavailable":
                _LOGGER.debug("Native HA schedule %s is unavailable", scheduler_entity_id)
                return False
            _LOGGER.debug(
                "Native HA schedule %s is considered enabled (state: %s)",
                scheduler_entity_id,
                state.state,
            )
            return True

        is_enabled = state.state != "off"
        _LOGGER.debug(
            "Scheduler %s state: %s (enabled: %s)", scheduler_entity_id, state.state, is_enabled
        )
        return cast(bool, is_enabled)

    def _resolve_preset_temperature(self, preset: str) -> float | None:
        """Resolve a preset name to a numeric temperature using VTherm attributes.

        This uses the VTherm climate entity attributes as the source of truth. It
        attempts common attribute naming conventions, with v8.0.0+ compatibility
        using get_vtherm_attribute() to access both legacy and new data structures.

        Args:
            preset: Preset mode name from the scheduler action

        Returns:
            Temperature in Celsius, or None if it cannot be resolved.
        """
        if not self._vtherm_entity_id:
            return None
        state = self._hass.states.get(self._vtherm_entity_id)
        if not state:
            _LOGGER.debug("VTherm entity not found: %s", self._vtherm_entity_id)
            return None

        key = str(preset).lower().replace(" ", "_")

        # First, try the preset_temperatures dict (VTherm v8.0.0+ format)
        preset_temps = get_vtherm_attribute(state, "preset_temperatures")
        if isinstance(preset_temps, dict):
            # VTherm uses format like "eco_temp", "boost_temp", "comfort_temp"
            # Try multiple key patterns
            preset_keys_to_try = [
                f"{key}_temp",  # eco_temp, boost_temp
                f"{key}_temperature",  # eco_temperature
                key,  # eco, boost (fallback)
                preset.lower(),  # Original lowercase
            ]

            for preset_key in preset_keys_to_try:
                if preset_key in preset_temps:
                    try:
                        temp_value = float(preset_temps[preset_key])
                        # Ignore 0 values as they indicate uninitialized presets
                        if temp_value > 0:
                            _LOGGER.debug(
                                "Resolved preset '%s' to %.1f°C (from %s)",
                                preset,
                                temp_value,
                                preset_key,
                            )
                            return temp_value
                        else:
                            _LOGGER.debug(
                                "Skipping preset '%s' with 0°C (likely uninitialized)", preset
                            )
                    except (ValueError, TypeError):
                        _LOGGER.debug(
                            "Invalid preset_temperatures value for %s: %s",
                            preset_key,
                            preset_temps[preset_key],
                        )

        # Fallback: try common naming patterns with v8.0.0+ compatibility
        candidate_keys = [
            f"{key}_temperature",
            f"{key}_temp",
            f"temperature_{key}",
            f"temp_{key}",
        ]

        for k in candidate_keys:
            # Use get_vtherm_attribute to handle both legacy and v8.0.0+ formats
            value = get_vtherm_attribute(state, k)
            if value is not None:
                try:
                    return float(value)
                except (ValueError, TypeError):
                    _LOGGER.debug("Invalid preset attribute %s=%s", k, value)

        # Fallback: if this preset is currently active, use current target temperature
        current_preset = get_vtherm_attribute(state, "preset_mode")
        if isinstance(current_preset, str) and current_preset.lower() == key:
            for target_key in ("temperature", "target_temperature", "target_temp"):
                value = get_vtherm_attribute(state, target_key)
                if value is not None:
                    try:
                        return float(value)
                    except (ValueError, TypeError):
                        pass

        return None
