"""State change trigger for maintenance tasks."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers.event import (
    EventStateChangedData,
    async_track_state_change_event,
)

if TYPE_CHECKING:
    from ...sensor import MaintenanceSensor

from .base_trigger import BaseTrigger

_LOGGER = logging.getLogger(__name__)


def _norm_state(value: str | None) -> str | None:
    """Case/whitespace-insensitive state form for from/to comparisons."""
    return value.strip().casefold() if isinstance(value, str) else value


class StateChangeTrigger(BaseTrigger):
    """Trigger that activates after counting state transitions.

    Counts transitions matching from_state -> to_state pattern.
    Triggers when count reaches target_changes.
    """

    def __init__(
        self,
        hass: HomeAssistant,
        entity: MaintenanceSensor,
        trigger_config: dict[str, Any],
    ) -> None:
        """Initialize state change trigger."""
        super().__init__(hass, entity, trigger_config)

        # Case-insensitive matching, like RuntimeTrigger's on_states: the
        # options flow lowercases these on save while the panel keeps the
        # user's casing, and HA states themselves can be capitalized
        # (input_select "Home") — normalizing BOTH sides at compare time is
        # the only variant that works for every surface combination.
        self._from_state: str | None = _norm_state(trigger_config.get("trigger_from_state"))
        self._to_state: str | None = _norm_state(trigger_config.get("trigger_to_state"))
        self._target_changes: int = trigger_config.get("trigger_target_changes", 1)
        # Restore persisted change count from config, default to 0
        self._change_count: int = trigger_config.get("trigger_change_count", 0)
        self._current_value = float(self._change_count)
        self._last_state: str | None = None

    async def async_setup(self) -> None:
        """Set up state change trigger.

        IMPORTANT: The listener is ALWAYS registered, even when the entity does
        not exist yet.  HA fires a state_change event when an entity first
        appears (old_state=None), so the trigger will self-heal automatically.
        """
        state = self.hass.states.get(self.entity_id)
        if state is None:
            _LOGGER.info(
                "Trigger entity %s not yet available — listener registered, waiting for entity to appear",
                self.entity_id,
            )
            # Register listener anyway so we catch the entity appearing
            self._unsub_listener = async_track_state_change_event(self.hass, [self.entity_id], self._handle_state_transition)
            return

        self._last_state = state.state

        # Restore triggered state from persisted change count
        if self._change_count >= self._target_changes:
            # Latch reconciliation: a single-shot state alarm (target_changes
            # == 1) that already left its alert state while we were down is no
            # longer active. Clear it quietly — the recovery transition was
            # never observed, so we must NOT auto-complete for it here (that
            # path only runs on a live off event, guarded against double-count).
            if (
                self._to_state is not None
                and self._target_changes == 1
                and _norm_state(state.state) != self._to_state
            ):
                self._change_count = 0
                self._current_value = 0.0
                if self.hass.is_running:
                    self.hass.async_create_task(self._persist_change_count())
            else:
                self._triggered = True
                self.entity.async_update_trigger_state(
                    is_triggered=True,
                    current_value=float(self._change_count),
                    trigger_entity_id=self.entity_id,
                )

        # Register state change listener (override base: we handle events differently)
        self._unsub_listener = async_track_state_change_event(self.hass, [self.entity_id], self._handle_state_transition)

        _LOGGER.debug(
            "State change trigger setup: %s (target=%d, count=%d, from=%s, to=%s)",
            self.entity_id,
            self._target_changes,
            self._change_count,
            self._from_state,
            self._to_state,
        )

    @callback
    def _handle_state_transition(self, event: Event[EventStateChangedData]) -> None:
        """Handle state transition and count matching changes."""
        old_state = event.data.get("old_state")
        new_state = event.data.get("new_state")

        if new_state is None:
            # Entity removed from state machine
            return

        new_val = new_state.state

        # Entity appeared for the first time (old_state=None)
        if old_state is None:
            _LOGGER.info(
                "Trigger entity %s appeared in state machine (state=%s)",
                self.entity_id,
                new_val,
            )
            self._logged_unavailable = False
            # Capture initial state but don't count as a transition
            if new_val not in ("unavailable", "unknown"):
                self._last_state = new_val
            return

        old_val = old_state.state

        # Handle unavailable/unknown with log-once pattern
        if new_val in ("unavailable", "unknown"):
            if not self._logged_unavailable:
                _LOGGER.warning(
                    "Trigger entity %s became %s",
                    self.entity_id,
                    new_val,
                )
                self._logged_unavailable = True
            return

        # Entity is back to a valid state
        if self._logged_unavailable:
            _LOGGER.info(
                "Trigger entity %s is available again (state=%s)",
                self.entity_id,
                new_val,
            )
            self._logged_unavailable = False

        # Use _last_state as fallback when old_val is unavailable/unknown
        effective_old = old_val
        if old_val in ("unavailable", "unknown") and self._last_state is not None:
            effective_old = self._last_state

        # Check if transition matches pattern
        matches = True
        if self._from_state is not None and _norm_state(effective_old) != self._from_state:
            matches = False
        if self._to_state is not None and _norm_state(new_val) != self._to_state:
            matches = False

        if matches and effective_old != new_val:
            self._change_count += 1
            self._current_value = float(self._change_count)
            # Persist change count to survive restarts
            if self.hass.is_running:
                self.hass.async_create_task(self._persist_change_count())
            _LOGGER.debug(
                "State change counted: %s (%s -> %s) count=%d/%d",
                self.entity_id,
                old_val,
                new_val,
                self._change_count,
                self._target_changes,
            )

            was_triggered = self._triggered
            is_triggered = self._change_count >= self._target_changes
            self._triggered = is_triggered

            if is_triggered and not was_triggered:
                self._on_trigger_activated(float(self._change_count))
            elif not is_triggered and was_triggered:
                self._on_trigger_deactivated(float(self._change_count))

        # Latch recovery: a single-shot state alarm (target_changes == 1 — an
        # adopted problem sensor or an appliance event) clears when the entity
        # leaves its alert state. Reset the counter so the next occurrence can
        # fire again, and run the deactivation path — which auto-completes on
        # recovery when opted in. Multi-count triggers keep accumulating and
        # only reset on manual completion, so they are untouched here.
        elif (
            self._to_state is not None
            and self._target_changes == 1
            and self._triggered
            and _norm_state(new_val) != self._to_state
        ):
            self._change_count = 0
            self._current_value = 0.0
            if self.hass.is_running:
                self.hass.async_create_task(self._persist_change_count())
            self._triggered = False
            self._on_trigger_deactivated(0.0)

        self._last_state = new_val

    def evaluate(self, value: float) -> bool:
        """Evaluate is handled by _handle_state_transition directly."""
        # State change triggers use event-driven evaluation only
        return self._triggered

    @property
    def change_count(self) -> int:
        """Return the current change count."""
        return self._change_count

    def reset_count(self) -> None:
        """Reset the change counter (after maintenance)."""
        self._change_count = 0
        self._current_value = 0.0
        if self.hass.is_running:
            self.hass.async_create_task(self._persist_change_count())
        _LOGGER.debug("State change counter reset: %s", self.entity_id)

    async def _persist_change_count(self) -> None:
        """Persist change count to the Store for survival across restarts."""
        await self._coordinator.async_persist_trigger_runtime(
            self._task_id,
            {"change_count": self._change_count},
            entity_id=self.entity_id,
        )

    def reset(self) -> None:
        """Reset trigger and counter."""
        super().reset()
        self.reset_count()
