"""Auto-restart mixin for ProcessorManager.

Handles crash detection, exponential backoff, and automatic restart
of failed target processors.

Extracted from processor_manager.py to keep files under 800 lines.
"""

import asyncio
import time
from dataclasses import dataclass
from typing import Optional

from wled_controller.utils import get_logger

logger = get_logger(__name__)

# Auto-restart constants
RESTART_MAX_ATTEMPTS = 5        # max restarts within the window
RESTART_WINDOW_SEC = 300        # 5 minutes — reset counter after stable period
RESTART_BACKOFF_BASE = 2.0      # initial backoff seconds
RESTART_BACKOFF_MAX = 30.0      # cap backoff at 30s


@dataclass
class RestartState:
    """Per-target auto-restart tracking."""
    attempts: int = 0
    first_crash_time: float = 0.0
    last_crash_time: float = 0.0
    restart_task: Optional[asyncio.Task] = None
    enabled: bool = True  # disabled on manual stop


class AutoRestartMixin:
    """Mixin providing auto-restart logic for crashed target processors.

    Requires the host class to have:
        _processors: Dict[str, TargetProcessor]
        _restart_states: Dict[str, RestartState]
        fire_event(event: dict) -> None
        start_processing(target_id: str) -> coroutine
    """

    def _on_task_done(self, target_id: str, task: asyncio.Task) -> None:
        """Task done callback — detects crashes and schedules auto-restart."""
        # Ignore graceful cancellation (manual stop)
        if task.cancelled():
            return

        exc = task.exception()
        if exc is None:
            return  # Clean exit (shouldn't happen, but harmless)

        rs = self._restart_states.get(target_id)
        if not rs or not rs.enabled:
            return  # Auto-restart disabled (manual stop was called)

        now = time.monotonic()

        # Reset counter if previous crash window expired
        if rs.first_crash_time and (now - rs.first_crash_time) > RESTART_WINDOW_SEC:
            rs.attempts = 0
            rs.first_crash_time = 0.0

        rs.attempts += 1
        rs.last_crash_time = now
        if not rs.first_crash_time:
            rs.first_crash_time = now

        if rs.attempts > RESTART_MAX_ATTEMPTS:
            logger.error(
                f"[AUTO-RESTART] Target {target_id} crashed {rs.attempts} times "
                f"in {now - rs.first_crash_time:.0f}s — giving up"
            )
            self.fire_event({
                "type": "state_change",
                "target_id": target_id,
                "processing": False,
                "crashed": True,
                "auto_restart_exhausted": True,
            })
            return

        backoff = min(
            RESTART_BACKOFF_BASE * (2 ** (rs.attempts - 1)),
            RESTART_BACKOFF_MAX,
        )
        logger.warning(
            f"[AUTO-RESTART] Target {target_id} crashed (attempt {rs.attempts}/"
            f"{RESTART_MAX_ATTEMPTS}), restarting in {backoff:.1f}s"
        )

        self.fire_event({
            "type": "state_change",
            "target_id": target_id,
            "processing": False,
            "crashed": True,
            "auto_restart_in": backoff,
            "auto_restart_attempt": rs.attempts,
        })

        # Schedule the restart (runs in the event loop)
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            logger.error(f"[AUTO-RESTART] No running event loop for {target_id}")
            return

        rs.restart_task = loop.create_task(self._auto_restart(target_id, backoff))

    async def _auto_restart(self, target_id: str, delay: float) -> None:
        """Wait for backoff delay, then restart the target processor."""
        try:
            await asyncio.sleep(delay)
        except asyncio.CancelledError:
            logger.info(f"[AUTO-RESTART] Restart cancelled for {target_id}")
            return

        rs = self._restart_states.get(target_id)
        if not rs or not rs.enabled:
            logger.info(f"[AUTO-RESTART] Restart aborted for {target_id} (disabled)")
            return

        proc = self._processors.get(target_id)
        if proc is None:
            logger.warning(f"[AUTO-RESTART] Target {target_id} no longer registered")
            return
        if proc.is_running:
            logger.info(f"[AUTO-RESTART] Target {target_id} already running, skipping")
            return

        logger.info(f"[AUTO-RESTART] Restarting target {target_id} (attempt {rs.attempts})")
        try:
            await self.start_processing(target_id)
        except Exception as e:
            logger.error(f"[AUTO-RESTART] Failed to restart {target_id}: {e}")
            self.fire_event({
                "type": "state_change",
                "target_id": target_id,
                "processing": False,
                "crashed": True,
                "auto_restart_error": str(e),
            })
