"""Spook - Your homie."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from homeassistant.components.lovelace import DOMAIN
from homeassistant.components.lovelace.const import ConfigNotFound
from homeassistant.const import (
    EVENT_COMPONENT_LOADED,
    EVENT_LOVELACE_UPDATED,
    EVENT_STATE_CHANGED,
)
from homeassistant.core import Event, callback
from homeassistant.helpers import entity_registry as er

from ....const import LOGGER
from ....entity_filtering import (
    async_filter_known_entity_ids,
    async_get_all_entity_ids,
    split_comma_separated_entity_ids,
)
from ....repairs import AbstractSpookRepair

if TYPE_CHECKING:
    from collections.abc import Mapping

    from homeassistant.components.lovelace.dashboard import (
        LovelaceStorage,
        LovelaceYAML,
    )


class SpookRepair(AbstractSpookRepair):
    """Spook repair tries to find unknown referenced entity in dashboards."""

    domain = DOMAIN
    repair = "lovelace_unknown_entity_references"
    inspect_events = {
        EVENT_COMPONENT_LOADED,
        EVENT_LOVELACE_UPDATED,
        er.EVENT_ENTITY_REGISTRY_UPDATED,
    }
    inspect_config_entry_changed = True
    inspect_on_reload = True
    automatically_clean_up_issues = True

    _dashboards: dict[str, LovelaceStorage | LovelaceYAML]

    async def async_activate(self) -> None:
        """Handle the activating a repair."""
        self._dashboards = self.hass.data["lovelace"].dashboards
        await super().async_activate()

        @callback
        def _state_entity_changed(event_data: Mapping[str, Any]) -> bool:
            """Return if a state entity was added or removed."""
            return (
                event_data.get("old_state") is None
                or event_data.get("new_state") is None
            )

        @callback
        def _async_call_inspect_debouncer(_: Event) -> None:
            """Trigger an inspection when a state entity is added or removed."""
            self.inspect_debouncer.async_schedule_call()

        self._event_subs.add(
            self.hass.bus.async_listen(
                EVENT_STATE_CHANGED,
                _async_call_inspect_debouncer,
                event_filter=_state_entity_changed,
            ),
        )

    async def async_inspect(self) -> None:
        """Trigger a inspection."""
        LOGGER.debug("Spook is inspecting: %s", self.repair)

        known_entity_ids = async_get_all_entity_ids(self.hass, include_all_none=True)

        # Loop over all dashboards and check if there are unknown entities
        # referenced in the dashboards.
        for dashboard in self._dashboards.values():
            url_path = dashboard.url_path or "lovelace"
            self.possible_issue_ids.add(url_path)
            try:
                config = await dashboard.async_load(force=False)
            except ConfigNotFound:
                LOGGER.debug("Config for dashboard %s not found, skipping", url_path)
                continue

            extracted_entities = self.__async_extract_entities(config)
            if unknown_entities := async_filter_known_entity_ids(
                self.hass,
                entity_ids=set(extracted_entities.keys()),
                known_entity_ids=known_entity_ids,
            ):
                # Get the view path of the first unknown entity (by view order)
                first_view_path = next(
                    path
                    for entity_id, path in extracted_entities.items()
                    if entity_id in unknown_entities
                )
                title = "Overview"
                if dashboard.config:
                    title = dashboard.config.get("title", url_path)
                self.async_create_issue(
                    issue_id=url_path,
                    translation_placeholders={
                        "entities": "\n".join(
                            f"- `{entity_id}`" for entity_id in sorted(unknown_entities)
                        ),
                        "dashboard": title,
                        "edit": f"/{url_path}/{first_view_path}?edit=1",
                    },
                )
                LOGGER.debug(
                    (
                        "Spook found unknown entities in dashboard %s "
                        "and created an issue for it; Entities: %s"
                    ),
                    title,
                    ", ".join(unknown_entities),
                )

    @callback
    def __async_extract_entities(self, config: dict[str, Any]) -> dict[str, int | str]:
        """Extract entities from a dashboard config."""
        entities: dict[str, int | str] = {}
        if isinstance(config, dict) and (views := config.get("views")):
            for view_index, view in enumerate(views):
                if not isinstance(view, dict):
                    continue
                view_path: int | str = view.get("path") or view_index
                for entity_id_raw in self.__async_extract_entities_from_view(view):
                    for entity_id in split_comma_separated_entity_ids(entity_id_raw):
                        if entity_id not in entities:
                            entities[entity_id] = view_path
        return entities

    @callback
    def __async_extract_entities_from_view(self, config: dict[Any]) -> set[str]:
        """Extract entities from a view config."""
        entities = set()
        if badges := config.get("badges"):
            for badge in badges:
                entities.update(self.__async_extract_entities_from_badge(badge))
        if cards := config.get("cards"):
            for card in cards:
                entities.update(self.__async_extract_entities_from_card(card))
        if sections := config.get("sections"):
            for section in sections:
                entities.update(self.__async_extract_entities_from_section(section))
        return entities

    @callback
    def __async_extract_entities_from_section(self, config: dict[Any]) -> set[str]:
        """Extract entities from a section config."""
        entities = set()
        if cards := config.get("cards"):
            for card in cards:
                entities.update(self.__async_extract_entities_from_card(card))
        return entities

    @callback
    def __async_extract_common(self, config: dict[str, Any] | str) -> set[str]:
        """Extract entities from common dashboard config."""
        entities: set[str] = set()

        if not isinstance(config, dict):
            return entities

        for key in ("camera_image", "entity", "entities", "entity_id"):
            if entity := config.get(key):
                if isinstance(entity, str):
                    entities.add(entity)
                if isinstance(entity, list):
                    for item in entity:
                        if isinstance(item, str):
                            entities.add(item)
                        elif (
                            isinstance(item, dict)
                            and "entity" in item
                            and isinstance(item["entity"], str)
                        ):
                            entities.add(item["entity"])
                if (
                    isinstance(entity, dict)
                    and "entity" in entity
                    and isinstance(entity["entity"], str)
                ):
                    entities.add(entity["entity"])
        return entities

    @callback
    def __async_extract_entities_from_badge(
        self,
        config: dict[str, Any] | str,
    ) -> set[str]:
        """Extract entities from a dashboard badge config."""
        if isinstance(config, str):
            return {config}
        if isinstance(config, dict):
            if (entity_id := config.get("entity")) and isinstance(entity_id, str):
                return {config["entity"]}
            if (entities := config.get("entities")) and isinstance(entities, list):
                extracted = []
                for entity in entities:
                    if isinstance(entity, str):
                        extracted.append(entity)
                    elif (
                        isinstance(entity, dict)
                        and "entity" in entity
                        and isinstance(entity["entity"], str)
                    ):
                        extracted.append(entity["entity"])
                return set(extracted)
        return set()

    @callback
    # pylint: disable-next=too-many-branches
    def __async_extract_entities_from_card(  # noqa: C901, PLR0912
        self,
        config: dict[str, Any],
    ) -> set[str]:
        """Extract entities from a dashboard card config."""
        if not isinstance(config, dict):
            return set()

        entities = self.__async_extract_common(config)
        entities.update(self.__async_extract_entities_from_actions(config))

        if condition := config.get("condition"):
            entities.update(
                self.__async_extract_entities_from_condition(condition),
            )

        if card := config.get("card"):
            entities.update(self.__async_extract_entities_from_card(card))

        if cards := config.get("cards"):
            for card in cards:
                entities.update(self.__async_extract_entities_from_card(card))

        for key in ("header", "footer"):
            if header_footer := config.get(key):
                entities.update(
                    self.__async_extract_entities_from_header_footer(header_footer),
                )

        if elements := config.get("elements"):
            for element in elements:
                entities.update(self.__async_extract_entities_from_element(element))

        # Mushroom
        if chips := config.get("chips"):
            for chip in chips:
                entities.update(self.__async_extract_entities_from_mushroom_chip(chip))

        # Heading card
        if badges := config.get("badges"):
            for badge in badges:
                if isinstance(badge, dict):
                    entities.update(self.__async_extract_common(badge))
                    entities.update(self.__async_extract_entities_from_actions(badge))

        visibility = config.get("visibility")
        if isinstance(visibility, list):
            for condition in visibility:
                if isinstance(condition, dict):
                    entities.update(
                        self.__async_extract_entities_from_condition(condition)
                    )

        return entities

    @callback
    def __async_extract_entities_from_actions(self, config: dict[str, Any]) -> set[str]:
        """Extract entities from a dashboard config containing actions."""
        entities = set()
        for key in (
            "tap_action",
            "hold_action",
            "double_tap_action",
            "subtitle_tap_action",
        ):
            if isinstance(config, dict) and (action := config.get(key)):
                entities.update(self.__async_extract_entities_from_action(action))
        return entities

    @callback
    def __async_extract_entities_from_action(self, config: dict[str, Any]) -> set[str]:
        """Extract entities from a dashboard action config."""
        entities = set()
        for key in ("service_data", "target"):
            if (
                isinstance(config, dict)
                and (target := config.get(key))
                and isinstance(target, dict)
                and (entity_id := target.get("entity_id"))
            ):
                if isinstance(entity_id, str):
                    entities.add(entity_id)
                if isinstance(entity_id, list):
                    entities.update(entity_id)
        return entities

    @callback
    def __async_extract_entities_from_condition(
        self,
        config: dict[str, Any],
    ) -> set[str]:
        """Extract entities from a dashboard element config."""
        if "entity" in config and isinstance(config["entity"], str):
            return {config["entity"]}
        return set()

    @callback
    def __async_extract_entities_from_element(self, config: dict[str, Any]) -> set[str]:
        """Extract entities from a dashboard element config."""
        if not isinstance(config, dict):
            return set()

        entities = self.__async_extract_common(config)
        entities.update(self.__async_extract_entities_from_actions(config))
        entities.update(self.__async_extract_entities_from_action(config))

        if conditions := config.get("conditions"):
            for condition in conditions:
                entities.update(self.__async_extract_entities_from_condition(condition))

        if elements := config.get("elements"):
            for element in elements:
                entities.update(self.__async_extract_entities_from_element(element))

        visibility = config.get("visibility")
        if isinstance(visibility, list):
            for condition in visibility:
                if isinstance(condition, dict):
                    entities.update(
                        self.__async_extract_entities_from_condition(condition)
                    )

        return entities

    @callback
    def __async_extract_entities_from_header_footer(
        self,
        config: dict[str, Any],
    ) -> set[str]:
        """Extract entities from a dashboard haeder footer config."""
        entities = self.__async_extract_common(config)
        entities.update(self.__async_extract_entities_from_actions(config))
        return entities

    @callback
    def __async_extract_entities_from_mushroom_chip(
        self,
        config: dict[str, Any],
    ) -> set[str]:
        """Extract entities from mushroom chips."""
        if not isinstance(config, dict):
            return set()
        entities = self.__async_extract_common(config)
        if chip := config.get("chip"):
            entities.update(
                self.__async_extract_entities_from_mushroom_chip(chip),
            )
        if conditions := config.get("conditions"):
            for condition in conditions:
                entities.update(self.__async_extract_entities_from_condition(condition))
        return entities
