"""Automations."""

import re
import copy
import logging

from homeassistant.core import (
    HomeAssistant,
    callback,
)
from homeassistant.const import (
    CONF_TYPE,
    ATTR_SERVICE,
    ATTR_ENTITY_ID,
    CONF_SERVICE_DATA,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.template import Template, is_template_string
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.translation import async_get_translations
from homeassistant.components.binary_sensor.device_condition import (
    ENTITY_CONDITIONS,
)

from . import const
from .helpers import (
    friendly_name_for_entity_id,
)
from .sensors import (
    STATE_OPEN,
    STATE_CLOSED,
    STATE_UNAVAILABLE,
)
from .alarm_control_panel import AlarmoBaseEntity

_LOGGER = logging.getLogger(__name__)

EVENT_ARM_FAILURE = "arm_failure"


def validate_area(trigger, area_id, hass):
    """Validate area for trigger."""
    if const.ATTR_AREA not in trigger:
        return False
    elif trigger[const.ATTR_AREA]:
        return trigger[const.ATTR_AREA] == area_id
    elif area_id and hass.data[const.DOMAIN].get("master"):
        return False
    elif len(hass.data[const.DOMAIN]["areas"]) == 1:
        return True
    else:
        return area_id is None


def validate_modes(trigger, mode):
    """Validate modes for trigger."""
    if const.ATTR_MODES not in trigger:
        return False
    elif not trigger[const.ATTR_MODES]:
        return True
    else:
        return mode in trigger[const.ATTR_MODES]


def validate_trigger(trigger, to_state, from_state=None):
    """Validate trigger condition."""
    if const.ATTR_EVENT not in trigger:
        return False
    elif trigger[const.ATTR_EVENT] == "untriggered" and from_state == "triggered":
        return True
    elif trigger[const.ATTR_EVENT] == to_state:
        return True
    else:
        return False


class AutomationHandler:
    """Handle automations."""

    def __init__(self, hass: HomeAssistant):
        """Initialize automation handler."""
        self.hass = hass
        self._config = None
        self._subscriptions = []
        self._sensorTranslationCache = {}
        self._alarmTranslationCache = {}
        self._sensorTranslationLang = None
        self._alarmTranslationLang = None

        def async_update_config():
            """Automation config updated, reload the configuration."""
            self._config = self.hass.data[const.DOMAIN][
                "coordinator"
            ].store.async_get_automations()

        self._subscriptions.append(
            async_dispatcher_connect(
                hass, "alarmo_automations_updated", async_update_config
            )
        )
        async_update_config()

        @callback
        async def async_alarm_state_changed(
            area_id: str, old_state: str, new_state: str
        ):
            if not old_state:
                # ignore automations at startup/restoring
                return

            if area_id:
                alarm_entity = self.hass.data[const.DOMAIN]["areas"][area_id]
            else:
                alarm_entity = self.hass.data[const.DOMAIN]["master"]

            if not alarm_entity:
                return

            _LOGGER.debug(
                "state of %s is updated from %s to %s",
                alarm_entity.entity_id,
                old_state,
                new_state,
            )

            if new_state in const.ARM_MODES:
                # we don't distinguish between armed modes for automations
                #   they are handled separately
                new_state = "armed"

            for automation_id, config in self._config.items():
                if not config[const.ATTR_ENABLED]:
                    continue
                for trigger in config[const.ATTR_TRIGGERS]:
                    if (
                        validate_area(trigger, area_id, self.hass)
                        and validate_modes(trigger, alarm_entity._arm_mode)
                        and validate_trigger(trigger, new_state, old_state)
                    ):
                        await self.async_execute_automation(automation_id, alarm_entity)

        self._subscriptions.append(
            async_dispatcher_connect(
                self.hass, "alarmo_state_updated", async_alarm_state_changed
            )
        )

        @callback
        async def async_handle_event(event: str, area_id: str, args: dict = {}):
            if event != const.EVENT_FAILED_TO_ARM:
                return
            if area_id:
                alarm_entity = self.hass.data[const.DOMAIN]["areas"][area_id]
            else:
                alarm_entity = self.hass.data[const.DOMAIN]["master"]

            _LOGGER.debug(
                "%s has failed to arm",
                alarm_entity.entity_id,
            )

            for automation_id, config in self._config.items():
                if not config[const.ATTR_ENABLED]:
                    continue
                for trigger in config[const.ATTR_TRIGGERS]:
                    if (
                        validate_area(trigger, area_id, self.hass)
                        and validate_modes(trigger, alarm_entity._arm_mode)
                        and validate_trigger(trigger, EVENT_ARM_FAILURE)
                    ):
                        await self.async_execute_automation(automation_id, alarm_entity)

        self._subscriptions.append(
            async_dispatcher_connect(self.hass, "alarmo_event", async_handle_event)
        )

    def __del__(self):
        """Prepare for removal."""
        while len(self._subscriptions):
            self._subscriptions.pop()()

    async def async_execute_automation(
        self, automation_id: str, alarm_entity: AlarmoBaseEntity
    ):
        """Execute the specified automation."""
        # automation is a dict of AutomationEntry
        _LOGGER.debug(
            "Executing automation %s",
            automation_id,
        )

        actions = self._config[automation_id][const.ATTR_ACTIONS]
        for action in actions:
            try:
                service_data = copy.copy(action[CONF_SERVICE_DATA])

                if action.get(ATTR_ENTITY_ID):
                    service_data[ATTR_ENTITY_ID] = action[ATTR_ENTITY_ID]

                if self._config[automation_id][CONF_TYPE] == const.ATTR_NOTIFICATION:
                    # recursively process all service_data (supports deep nesting)
                    service_data = await self._process_service_data(
                        service_data,
                        alarm_entity,
                    )

                domain, service = action[ATTR_SERVICE].split(".")

                await self.hass.async_create_task(
                    self.hass.services.async_call(
                        domain,
                        service,
                        service_data,
                        blocking=False,
                        context={},
                    )
                )
            except HomeAssistantError as e:
                _LOGGER.error(
                    "Execution of action %s failed, reason: %s",
                    automation_id,
                    e,
                )

    async def _process_service_data(
        self, obj, alarm_entity, depth: int = 0, max_depth: int = 10
    ):
        """Recursively process service_data replacing wildcards and templates safely."""
        if depth > max_depth:
            return obj

        # Handle strings
        if isinstance(obj, str):
            return await self.replace_wildcards_in_string(obj, alarm_entity)

        # Handle dictionaries
        if isinstance(obj, dict):
            processed = {}
            for k, v in obj.items():
                processed[k] = await self._process_service_data(
                    v,
                    alarm_entity,
                    depth + 1,
                    max_depth,
                )
            return processed

        # Handle lists
        if isinstance(obj, list):
            return [
                await self._process_service_data(
                    item,
                    alarm_entity,
                    depth + 1,
                    max_depth,
                )
                for item in obj
            ]
        # Other types (int, float, bool, None, etc.)
        return obj

    def get_automations_by_area(self, area_id: str):
        """Get automations for specified area."""
        result = []
        for automation_id, config in self._config.items():
            if any(
                el[const.ATTR_AREA] == area_id for el in config[const.ATTR_TRIGGERS]
            ):
                result.append(automation_id)

        return result

    async def replace_wildcards_in_string(
        self, input: str, alarm_entity: AlarmoBaseEntity
    ):
        """Look for wildcards in string and replace them with content."""
        # process wildcard '{{open_sensors}}'
        res = re.search(r"{{open_sensors(\|lang=([^}]+))?(\|format=short)?}}", input)
        if res:
            lang = res.group(2) if res.group(2) else "en"
            names_only = True if res.group(3) else False

            open_sensors = ""
            if alarm_entity.open_sensors:
                parts = []
                for entity_id, status in alarm_entity.open_sensors.items():
                    if names_only:
                        parts.append(friendly_name_for_entity_id(entity_id, self.hass))
                    else:
                        parts.append(
                            await self.async_get_open_sensor_string(
                                entity_id, status, lang
                            )
                        )
                open_sensors = ", ".join(parts)
            input = input.replace(res.group(0), open_sensors)

        # process wildcard '{{bypassed_sensors}}'
        if "{{bypassed_sensors}}" in input:
            bypassed_sensors = ""
            if alarm_entity.bypassed_sensors and len(alarm_entity.bypassed_sensors):
                parts = []
                for entity_id in alarm_entity.bypassed_sensors:
                    name = friendly_name_for_entity_id(entity_id, self.hass)
                    parts.append(name)
                bypassed_sensors = ", ".join(parts)
            input = input.replace("{{bypassed_sensors}}", bypassed_sensors)

        # process wildcard '{{arm_mode}}'
        res = re.search(r"{{arm_mode(\|lang=([^}]+))?}}", input)
        if res:
            lang = res.group(2) if res.group(2) else "en"
            arm_mode = await self.async_get_arm_mode_string(alarm_entity.arm_mode, lang)

            input = input.replace(res.group(0), arm_mode)

        # process wildcard '{{changed_by}}'
        if "{{changed_by}}" in input:
            changed_by = alarm_entity.changed_by if alarm_entity.changed_by else ""
            input = input.replace("{{changed_by}}", changed_by)

        # process wildcard '{{delay}}'
        if "{{delay}}" in input:
            delay = str(alarm_entity.delay) if alarm_entity.delay else ""
            input = input.replace("{{delay}}", delay)

        # process HA templates
        if is_template_string(input):
            input = Template(input, self.hass).async_render()
            _LOGGER.debug("Processed HA template, result:(%s) %s", type(input), input)

        return input

    async def async_get_open_sensor_string(
        self, entity_id: str, state: str, language: str
    ):
        """Get translation for sensor states."""
        if self._sensorTranslationCache and self._sensorTranslationLang == language:
            translations = self._sensorTranslationCache
        else:
            translations = await async_get_translations(
                self.hass, language, "device_automation", ["binary_sensor"]
            )

            self._sensorTranslationCache = translations
            self._sensorTranslationLang = language

        entity = self.hass.states.get(entity_id)

        device_type = (
            entity.attributes["device_class"]
            if entity and "device_class" in entity.attributes
            else None
        )

        if state == STATE_OPEN:
            translation_key = (
                f"component.binary_sensor.device_automation.condition_type.{ENTITY_CONDITIONS[device_type][0]['type']}"
                if device_type in ENTITY_CONDITIONS
                else None
            )
            if translation_key and translation_key in translations:
                string = translations[translation_key]
            else:
                string = "{entity_name} is open"
        elif state == STATE_CLOSED:
            translation_key = (
                f"component.binary_sensor.device_automation.condition_type.{ENTITY_CONDITIONS[device_type][1]['type']}"
                if device_type in ENTITY_CONDITIONS
                else None
            )
            if translation_key and translation_key in translations:
                string = translations[translation_key]
            else:
                string = "{entity_name} is closed"

        elif state == STATE_UNAVAILABLE:
            string = "{entity_name} is unavailable"

        else:
            string = "{entity_name} is unknown"

        name = friendly_name_for_entity_id(entity_id, self.hass)
        string = string.replace("{entity_name}", name)

        return string

    async def async_get_arm_mode_string(self, arm_mode: str, language: str):
        """Get translation for alarm arm mode."""
        if self._alarmTranslationCache and self._alarmTranslationLang == language:
            translations = self._alarmTranslationCache
        else:
            translations = await async_get_translations(
                self.hass, language, "entity_component", ["alarm_control_panel"]
            )

            self._alarmTranslationCache = translations
            self._alarmTranslationLang = language

        translation_key = (
            f"component.alarm_control_panel.entity_component._.state.{arm_mode}"
            if arm_mode
            else None
        )

        if translation_key and translation_key in translations:
            return translations[translation_key]
        elif arm_mode:
            return " ".join(w.capitalize() for w in arm_mode.split("_"))
        else:
            return ""
