# -*- coding: utf-8 -*-
# pylint: disable=broad-exception-caught

"""Underlying state manager for versatile_thermostat.

This module provides `UnderlyingStateManager` which listens to state change
events for a list of entity_ids and stores the last known `State` for each
entity.
"""
from typing import List, Optional, Callable, Any
import logging
from vtherm_api.log_collector import get_vtherm_logger

from homeassistant.core import HomeAssistant, State
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.helpers.event import async_track_state_change_event

_LOGGER = get_vtherm_logger(__name__)


class UnknownEntity(Exception):
    """Raised when an entity_id is not managed by the manager."""


class UnderlyingStateManager:
    """Manage states of underlying entities.

    Accepts a list of `entity_id` strings to monitor, initializes an
    internal array of states (`State | None`) and stores the last received
    state for each entity when state change events occur.

    Optionally accepts `on_change` callback with signature
    `async def on_change(entity_id: str, new_state: Optional[State])` that
    will be scheduled when a state change is received.
    """

    def __init__(self, hass: HomeAssistant, on_change: Optional[Callable[[str, Optional[State], Optional[State]], Any]] = None) -> None:
        """Create the manager without any monitored entity IDs.

        Entities can be added later with `add_underlying_entities`.
        """
        self._hass: HomeAssistant = hass
        self._entity_ids: List[str] = []
        self._on_change = on_change

        # no states initially; entities added at runtime
        self._states: List[Optional[State]] = []

        # no listener registered until entities are added
        self._remove_callback: Optional[Callable[[], None]] = None

    async def _state_changed(self, event) -> None:
        """Internal callback invoked on each state change event."""
        new_state: Optional[State] = event.data.get("new_state", None)
        old_state: Optional[State] = event.data.get("old_state", None)
        # Retrieve the entity_id from the event (sometimes in data or via new_state)
        entity_id = event.data.get("entity_id") if event.data.get("entity_id") else (
            new_state.entity_id if new_state else None
        )
        _LOGGER.debug(
            "UnderlyingStateManager - State change event received: %s for entity_id: %s",
            new_state,
            entity_id,
        )
        if entity_id is None:
            return

        if not self._set_state(entity_id, new_state):
            return

        if self._on_change:
            try:
                self._hass.async_create_task(self._on_change(entity_id, new_state, old_state))
            except Exception as err:  # pragma: no cover - defensive
                _LOGGER.exception("Error %s scheduling on_change for %s", err, entity_id)

    def get_state(self, entity_id: str) -> Optional[State]:
        """Return the last known `State` for `entity_id`, or `None` if unknown."""
        idx = self._index_of(entity_id)
        if idx is None:
            state = self._hass.states.get(entity_id)
            if state is not None:
                _LOGGER.debug(
                    "UnderlyingStateManager - Requested state for unknown entity_id: %s, found in HA",
                    entity_id,
                )
                return state
            _LOGGER.error("UnderlyingStateManager - Requested state for unknown entity_id: %s", entity_id)
            return None
        return self._states[idx]

    def _set_state(self, entity_id: str, state: Optional[State]) -> bool:
        """Set the cached state for an entity and schedule `on_change` if present.
        Return True if the state was updated, False if ignored.
        States that are `None`, `STATE_UNAVAILABLE`, or `STATE_UNKNOWN` are ignored
        """
        idx = self._index_of(entity_id)
        if idx is None:
            raise UnknownEntity(f"Entity ID {entity_id} is not managed by UnderlyingStateManager")

        # if state is None or state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN, None]:
        #     _LOGGER.debug("UnderlyingStateManager - Ignoring state change to unavailable/unknown for entity_id: %s", entity_id)
        #     return False

        self._states[idx] = state
        return True

    @property
    def is_all_states_initialized(self) -> bool:
        """Return True if all monitored states are initialized and available.

        A state is considered initialized if it is not None and its
        `.state` is not `STATE_UNAVAILABLE` or `STATE_UNKNOWN`.
        """
        for st in self._states:
            if st is None or st.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
                return False
        return len(self._states) == len(self._entity_ids)

    def get_uninitialized_entities(self) -> List[str]:
        """Return a list of entity_ids that are not initialized."""
        uninitialized = []
        for entity_id, state in zip(self._entity_ids, self._states):
            if state is None or state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
                uninitialized.append(entity_id)
        return uninitialized

    def _index_of(self, entity_id: str) -> Optional[int]:
        """Return the index of `entity_id` in the monitored list, or None."""
        try:
            return self._entity_ids.index(entity_id)
        except ValueError:
            return None

    def add_underlying_entities(self, entity_ids: List[str]) -> None:
        """Add multiple entity_ids to the monitored list at runtime.

        Existing entities are ignored. The state change listener is (re)
        registered for the updated entity list. If any added entity has an
        initial state and `on_change` is set, the callback is scheduled for
        that entity.
        """
        added = []
        for entity_id in entity_ids:
            if entity_id in self._entity_ids:
                continue
            self._entity_ids.append(entity_id)
            state = self._hass.states.get(entity_id)
            # should always add an initial state even if None
            self._states.append(state)
            if state and state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN, None]:
                # but don't notify if not a real state
                added.append(entity_id)

        # re-register the state change listener for the new set of entity_ids
        if self._remove_callback:
            try:
                self._remove_callback()
            except Exception:
                _LOGGER.exception("Error removing previous callback while adding %s", added)

        self._remove_callback = async_track_state_change_event(
            self._hass, self._entity_ids, self._state_changed
        )

        # schedule on_change for each newly added entity if initial state exists
        if self._on_change:
            for eid in added:
                st = self._states[self._entity_ids.index(eid)]
                if st is not None:
                    try:
                        self._hass.async_create_task(self._on_change(eid, st, None))
                    except Exception as error:  # pragma: no cover - defensive
                        _LOGGER.exception("Error scheduling on_change for %s. error is %s", eid, error)

    def stop(self) -> None:
        """Stop listening to state changes and remove the callback."""
        if self._remove_callback:
            try:
                self._remove_callback()
            except Exception:
                pass
            self._remove_callback = None
