"""Automation engine — background loop that evaluates conditions and activates scenes."""

import asyncio
import re
from datetime import datetime, timezone
from typing import Dict, Optional, Set

from wled_controller.core.automations.platform_detector import PlatformDetector
from wled_controller.storage.automation import (
    AlwaysCondition,
    ApplicationCondition,
    Automation,
    Condition,
    DisplayStateCondition,
    HomeAssistantCondition,
    MQTTCondition,
    StartupCondition,
    SystemIdleCondition,
    TimeOfDayCondition,
    WebhookCondition,
)
from wled_controller.storage.automation_store import AutomationStore
from wled_controller.storage.scene_preset import ScenePreset
from wled_controller.utils import get_logger

logger = get_logger(__name__)


class AutomationEngine:
    """Evaluates automation conditions and activates/deactivates scene presets."""

    def __init__(
        self,
        automation_store: AutomationStore,
        processor_manager,
        poll_interval: float = 1.0,
        mqtt_service=None,
        scene_preset_store=None,
        target_store=None,
        device_store=None,
        ha_manager=None,
    ):
        self._store = automation_store
        self._manager = processor_manager
        self._poll_interval = poll_interval
        self._detector = PlatformDetector()
        self._mqtt_service = mqtt_service
        self._scene_preset_store = scene_preset_store
        self._target_store = target_store
        self._device_store = device_store
        self._ha_manager = ha_manager
        self._task: Optional[asyncio.Task] = None
        self._eval_lock = asyncio.Lock()

        # Runtime state (not persisted)
        # automation_id → True when automation is currently active
        self._active_automations: Dict[str, bool] = {}
        # automation_id → snapshot captured before activation (for "revert" mode)
        self._pre_activation_snapshots: Dict[str, ScenePreset] = {}
        # automation_id → datetime of last activation / deactivation
        self._last_activated: Dict[str, datetime] = {}
        self._last_deactivated: Dict[str, datetime] = {}
        # webhook_token → bool (volatile state set by webhook calls)
        self._webhook_states: Dict[str, bool] = {}

    async def start(self) -> None:
        if self._task is not None:
            return
        self._task = asyncio.create_task(self._poll_loop())
        logger.info("Automation engine started")

    async def stop(self) -> None:
        if self._task is None:
            return

        self._task.cancel()
        try:
            await self._task
        except asyncio.CancelledError:
            logger.debug("Automation engine task cancelled")
            pass
        self._task = None

        # Deactivate all automations
        for automation_id in list(self._active_automations.keys()):
            await self._deactivate_automation(automation_id)

        logger.info("Automation engine stopped")

    async def _poll_loop(self) -> None:
        try:
            while True:
                try:
                    await self._evaluate_all()
                except Exception as e:
                    logger.error(f"Automation evaluation error: {e}", exc_info=True)
                await asyncio.sleep(self._poll_interval)
        except asyncio.CancelledError:
            logger.debug("Automation poll loop cancelled")
            pass

    async def _evaluate_all(self) -> None:
        async with self._eval_lock:
            await self._evaluate_all_locked()

    def _detect_all_sync(
        self,
        needs_running: bool,
        needs_topmost: bool,
        needs_fullscreen: bool,
        needs_idle: bool,
        needs_display_state: bool,
    ) -> tuple:
        """Run all platform detection in a single thread call.

        Batching detection calls into one executor submission reduces
        event-loop wake-ups, minimising asyncio.sleep() jitter in
        latency-sensitive processing loops.
        """
        running_procs = self._detector._get_running_processes_sync() if needs_running else set()
        if needs_topmost:
            topmost_proc, topmost_fullscreen = self._detector._get_topmost_process_sync()
        else:
            topmost_proc, topmost_fullscreen = None, False
        fullscreen_procs = (
            self._detector._get_fullscreen_processes_sync() if needs_fullscreen else set()
        )
        idle_seconds = self._detector._get_idle_seconds_sync() if needs_idle else None
        display_state = (
            self._detector._get_display_power_state_sync() if needs_display_state else None
        )
        return (
            running_procs,
            topmost_proc,
            topmost_fullscreen,
            fullscreen_procs,
            idle_seconds,
            display_state,
        )

    async def _evaluate_all_locked(self) -> None:
        automations = self._store.get_all_automations()
        if not automations:
            # No automations — deactivate any stale state
            for aid in list(self._active_automations.keys()):
                await self._deactivate_automation(aid)
            return

        # Determine which detection methods are actually needed
        match_types_used: set = set()
        needs_idle = False
        needs_display_state = False
        for a in automations:
            if a.enabled:
                for c in a.conditions:
                    if isinstance(c, ApplicationCondition):
                        match_types_used.add(c.match_type)
                    elif isinstance(c, SystemIdleCondition):
                        needs_idle = True
                    elif isinstance(c, DisplayStateCondition):
                        needs_display_state = True

        needs_running = "running" in match_types_used
        needs_topmost = bool(match_types_used & {"topmost", "topmost_fullscreen"})
        needs_fullscreen = "fullscreen" in match_types_used

        # Single executor call for all platform detection
        loop = asyncio.get_event_loop()
        (
            running_procs,
            topmost_proc,
            topmost_fullscreen,
            fullscreen_procs,
            idle_seconds,
            display_state,
        ) = await loop.run_in_executor(
            None,
            self._detect_all_sync,
            needs_running,
            needs_topmost,
            needs_fullscreen,
            needs_idle,
            needs_display_state,
        )

        active_automation_ids = set()

        for automation in automations:
            should_be_active = automation.enabled and (
                len(automation.conditions) == 0
                or self._evaluate_conditions(
                    automation,
                    running_procs,
                    topmost_proc,
                    topmost_fullscreen,
                    fullscreen_procs,
                    idle_seconds,
                    display_state,
                )
            )

            is_active = automation.id in self._active_automations

            if should_be_active and not is_active:
                await self._activate_automation(automation)
                active_automation_ids.add(automation.id)
            elif should_be_active and is_active:
                active_automation_ids.add(automation.id)
            elif not should_be_active and is_active:
                await self._deactivate_automation(automation.id)

        # Deactivate automations that were removed from store while active
        for aid in list(self._active_automations.keys()):
            if aid not in active_automation_ids:
                await self._deactivate_automation(aid)

    def _evaluate_conditions(
        self,
        automation: Automation,
        running_procs: Set[str],
        topmost_proc: Optional[str],
        topmost_fullscreen: bool,
        fullscreen_procs: Set[str],
        idle_seconds: Optional[float],
        display_state: Optional[str],
    ) -> bool:
        results = [
            self._evaluate_condition(
                c,
                running_procs,
                topmost_proc,
                topmost_fullscreen,
                fullscreen_procs,
                idle_seconds,
                display_state,
            )
            for c in automation.conditions
        ]

        if automation.condition_logic == "and":
            return all(results)
        return any(results)  # "or" is default

    def _evaluate_condition(
        self,
        condition: Condition,
        running_procs: Set[str],
        topmost_proc: Optional[str],
        topmost_fullscreen: bool,
        fullscreen_procs: Set[str],
        idle_seconds: Optional[float],
        display_state: Optional[str],
    ) -> bool:
        dispatch = {
            AlwaysCondition: lambda c: True,
            StartupCondition: lambda c: True,
            ApplicationCondition: lambda c: self._evaluate_app_condition(
                c, running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs
            ),
            TimeOfDayCondition: lambda c: self._evaluate_time_of_day(c),
            SystemIdleCondition: lambda c: self._evaluate_idle(c, idle_seconds),
            DisplayStateCondition: lambda c: self._evaluate_display_state(c, display_state),
            MQTTCondition: lambda c: self._evaluate_mqtt(c),
            WebhookCondition: lambda c: self._webhook_states.get(c.token, False),
            HomeAssistantCondition: lambda c: self._evaluate_home_assistant(c),
        }
        handler = dispatch.get(type(condition))
        if handler is None:
            return False
        return handler(condition)

    @staticmethod
    def _evaluate_time_of_day(condition: TimeOfDayCondition) -> bool:
        now = datetime.now()
        current = now.hour * 60 + now.minute
        parts_s = condition.start_time.split(":")
        parts_e = condition.end_time.split(":")
        start = int(parts_s[0]) * 60 + int(parts_s[1])
        end = int(parts_e[0]) * 60 + int(parts_e[1])
        if start <= end:
            return start <= current <= end
        # Overnight range (e.g. 22:00 → 06:00)
        return current >= start or current <= end

    @staticmethod
    def _evaluate_idle(condition: SystemIdleCondition, idle_seconds: Optional[float]) -> bool:
        if idle_seconds is None:
            return False
        is_idle = idle_seconds >= (condition.idle_minutes * 60)
        return is_idle if condition.when_idle else not is_idle

    @staticmethod
    def _evaluate_display_state(
        condition: DisplayStateCondition, display_state: Optional[str]
    ) -> bool:
        if display_state is None:
            return False
        return display_state == condition.state

    def _evaluate_mqtt(self, condition: MQTTCondition) -> bool:
        if self._mqtt_service is None or not self._mqtt_service.is_connected:
            return False
        value = self._mqtt_service.get_last_value(condition.topic)
        if value is None:
            return False
        matchers = {
            "exact": lambda: value == condition.payload,
            "contains": lambda: condition.payload in value,
            "regex": lambda: bool(re.search(condition.payload, value)),
        }
        matcher = matchers.get(condition.match_mode)
        if matcher is None:
            return False
        try:
            return matcher()
        except re.error as e:
            logger.debug("MQTT condition regex error: %s", e)
            return False

    def _evaluate_home_assistant(self, condition: HomeAssistantCondition) -> bool:
        if self._ha_manager is None:
            return False
        entity_state = self._ha_manager.get_state(condition.ha_source_id, condition.entity_id)
        if entity_state is None:
            return False
        value = entity_state.state
        matchers = {
            "exact": lambda: value == condition.state,
            "contains": lambda: condition.state in value,
            "regex": lambda: bool(re.search(condition.state, value)),
        }
        matcher = matchers.get(condition.match_mode)
        if matcher is None:
            return False
        try:
            return matcher()
        except re.error as e:
            logger.debug("HA condition regex error: %s", e)
            return False

    def _evaluate_app_condition(
        self,
        condition: ApplicationCondition,
        running_procs: Set[str],
        topmost_proc: Optional[str],
        topmost_fullscreen: bool,
        fullscreen_procs: Set[str],
    ) -> bool:
        if not condition.apps:
            return False

        apps_lower = [a.lower() for a in condition.apps]

        match_handlers = {
            "fullscreen": lambda: any(app in fullscreen_procs for app in apps_lower),
            "topmost_fullscreen": lambda: (
                topmost_proc is not None
                and topmost_fullscreen
                and any(app == topmost_proc for app in apps_lower)
            ),
            "topmost": lambda: (
                topmost_proc is not None and any(app == topmost_proc for app in apps_lower)
            ),
        }
        handler = match_handlers.get(condition.match_type)
        if handler is not None:
            return handler()
        # Default: "running"
        return any(app in running_procs for app in apps_lower)

    async def _activate_automation(self, automation: Automation) -> None:
        if not automation.scene_preset_id:
            # No scene configured — just mark active (conditions matched but nothing to do)
            self._active_automations[automation.id] = True
            self._last_activated[automation.id] = datetime.now(timezone.utc)
            self._fire_event(automation.id, "activated")
            logger.info(f"Automation '{automation.name}' activated (no scene configured)")
            return

        if not self._scene_preset_store or not self._target_store or not self._device_store:
            logger.warning(f"Automation '{automation.name}' matched but scene stores not available")
            return

        # Load the scene preset
        try:
            preset = self._scene_preset_store.get_preset(automation.scene_preset_id)
        except ValueError:
            logger.warning(
                f"Automation '{automation.name}': scene preset {automation.scene_preset_id} not found"
            )
            return

        # For "revert" mode, capture current state before activating
        if automation.deactivation_mode == "revert":
            from wled_controller.core.scenes.scene_activator import capture_current_snapshot

            targets = capture_current_snapshot(self._target_store, self._manager)
            self._pre_activation_snapshots[automation.id] = ScenePreset(
                id=f"_revert_{automation.id}",
                name=f"Pre-activation snapshot for {automation.name}",
                targets=targets,
            )

        # Apply the scene
        from wled_controller.core.scenes.scene_activator import apply_scene_state

        status, errors = await apply_scene_state(
            preset,
            self._target_store,
            self._manager,
        )

        self._active_automations[automation.id] = True
        self._last_activated[automation.id] = datetime.now(timezone.utc)
        self._fire_event(automation.id, "activated")

        if errors:
            logger.warning(f"Automation '{automation.name}' activated with errors: {errors}")
        else:
            logger.info(f"Automation '{automation.name}' activated (scene '{preset.name}' applied)")

    async def _deactivate_automation(self, automation_id: str) -> None:
        was_active = self._active_automations.pop(automation_id, False)
        if not was_active:
            return

        # Look up the automation for deactivation settings
        try:
            automation = self._store.get_automation(automation_id)
        except ValueError:
            automation = None

        deactivation_mode = automation.deactivation_mode if automation else "none"

        if deactivation_mode == "revert":
            await self._deactivate_revert(automation_id)
        elif deactivation_mode == "fallback_scene":
            await self._deactivate_fallback(automation_id, automation)
        else:
            logger.info(f"Automation {automation_id} deactivated")

        self._last_deactivated[automation_id] = datetime.now(timezone.utc)
        self._fire_event(automation_id, "deactivated")
        # Clean up any leftover snapshot
        self._pre_activation_snapshots.pop(automation_id, None)

    async def _deactivate_revert(self, automation_id: str) -> None:
        """Revert to pre-activation snapshot."""
        snapshot = self._pre_activation_snapshots.pop(automation_id, None)
        if snapshot and self._target_store:
            from wled_controller.core.scenes.scene_activator import apply_scene_state

            status, errors = await apply_scene_state(
                snapshot,
                self._target_store,
                self._manager,
            )
            if errors:
                logger.warning(f"Automation {automation_id} revert errors: {errors}")
            else:
                logger.info(f"Automation {automation_id} deactivated (reverted to previous state)")
        else:
            logger.warning(f"Automation {automation_id}: no snapshot available for revert")

    async def _deactivate_fallback(self, automation_id: str, automation) -> None:
        """Activate fallback scene on deactivation."""
        fallback_id = automation.deactivation_scene_preset_id if automation else None
        if fallback_id and self._scene_preset_store and self._target_store:
            try:
                fallback = self._scene_preset_store.get_preset(fallback_id)
                from wled_controller.core.scenes.scene_activator import apply_scene_state

                status, errors = await apply_scene_state(
                    fallback,
                    self._target_store,
                    self._manager,
                )
                if errors:
                    logger.warning(f"Automation {automation_id} fallback errors: {errors}")
                else:
                    logger.info(
                        f"Automation {automation_id} deactivated (fallback scene '{fallback.name}' applied)"
                    )
            except ValueError:
                logger.warning(
                    f"Automation {automation_id}: fallback scene {fallback_id} not found"
                )
        else:
            logger.info(f"Automation {automation_id} deactivated (no fallback scene configured)")

    def _fire_event(self, automation_id: str, action: str) -> None:
        try:
            self._manager.fire_event(
                {
                    "type": "automation_state_changed",
                    "automation_id": automation_id,
                    "action": action,
                }
            )
        except Exception as e:
            logger.error("Automation action failed: %s", e, exc_info=True)

    # ===== Public query methods (used by API) =====

    def get_automation_state(self, automation_id: str) -> dict:
        """Get runtime state of a single automation."""
        is_active = automation_id in self._active_automations
        return {
            "is_active": is_active,
            "last_activated_at": self._last_activated.get(automation_id),
            "last_deactivated_at": self._last_deactivated.get(automation_id),
        }

    def get_all_automation_states(self) -> Dict[str, dict]:
        """Get runtime states of all automations."""
        result = {}
        for automation in self._store.get_all_automations():
            result[automation.id] = self.get_automation_state(automation.id)
        return result

    async def trigger_evaluate(self) -> None:
        """Run a single evaluation cycle immediately (used after enabling an automation)."""
        try:
            await self._evaluate_all()
        except Exception as e:
            logger.error(f"Immediate automation evaluation error: {e}", exc_info=True)

    async def set_webhook_state(self, token: str, active: bool) -> None:
        """Set webhook condition state and trigger immediate evaluation."""
        self._webhook_states[token] = active
        await self.trigger_evaluate()

    def get_webhook_state(self, token: str) -> bool:
        """Read current webhook state (False if never called)."""
        return self._webhook_states.get(token, False)

    async def deactivate_if_active(self, automation_id: str) -> None:
        """Deactivate an automation immediately (used when disabling/deleting)."""
        if automation_id in self._active_automations:
            await self._deactivate_automation(automation_id)
