"""Self-healing: automatic strategy restart, feed reconnection, circuit breaker."""

from __future__ import annotations

import logging
import threading
import time
from collections import deque
from dataclasses import dataclass
from enum import Enum
from typing import Any

logger = logging.getLogger("horizon.fund.recovery")


class RecoveryAction(str, Enum):
    """Types of recovery actions."""
    RESTART_STRATEGY = "restart_strategy"
    RECONNECT_FEED = "reconnect_feed"


@dataclass(frozen=True)
class RecoveryEvent:
    """A recorded recovery attempt."""
    action: RecoveryAction
    target: str
    timestamp: float
    success: bool
    attempt: int
    error: str | None = None


class RecoveryManager:
    """Monitors and recovers from strategy failures and stale feeds.

    Features:
    - Automatic strategy restart with exponential backoff
    - Circuit breaker: permanently fail after max retries
    - Stale feed detection and restart
    - Full event audit trail
    """

    def __init__(
        self,
        max_retries: int = 3,
        cooldown_secs: float = 60.0,
        max_cooldown_secs: float = 300.0,
        stale_feed_threshold_secs: float = 300.0,
    ) -> None:
        self._max_retries = max_retries
        self._base_cooldown = cooldown_secs
        self._max_cooldown = max_cooldown_secs
        self._stale_threshold = stale_feed_threshold_secs
        self._retry_counts: dict[str, int] = {}
        self._last_attempt: dict[str, float] = {}
        self._permanently_failed: set[str] = set()
        self._history: deque[RecoveryEvent] = deque(maxlen=10_000)
        self._lock = threading.Lock()

    def check_strategies(self, controller: Any) -> list[RecoveryEvent]:
        """Check for failed strategies and attempt recovery.

        Args:
            controller: StrategyController instance.

        Returns:
            List of RecoveryEvents for this check cycle.
        """
        events: list[RecoveryEvent] = []
        failed_names = controller.failed_strategies()
        now = time.time()

        for name in failed_names:
            with self._lock:
                if name in self._permanently_failed:
                    continue

                # Cooldown check with exponential backoff
                attempt = self._retry_counts.get(name, 0)
                last = self._last_attempt.get(name, 0.0)
                cooldown = min(
                    self._base_cooldown * (2 ** attempt),
                    self._max_cooldown,
                )
                if now - last < cooldown:
                    continue

                # Circuit breaker
                if attempt >= self._max_retries:
                    self._permanently_failed.add(name)
                    event = RecoveryEvent(
                        action=RecoveryAction.RESTART_STRATEGY,
                        target=name,
                        timestamp=now,
                        success=False,
                        attempt=attempt,
                        error=f"Circuit breaker: {attempt} failures, permanently disabled",
                    )
                    self._history.append(event)
                    events.append(event)
                    logger.error(
                        "Strategy '%s' permanently failed after %d attempts",
                        name, attempt,
                    )
                    continue

                self._retry_counts[name] = attempt + 1
                self._last_attempt[name] = now

            # Attempt restart
            event = self._restart_strategy(controller, name, attempt + 1, now)
            with self._lock:
                self._history.append(event)
            events.append(event)

            if event.success:
                with self._lock:
                    self._retry_counts.pop(name, None)
                    self._last_attempt.pop(name, None)

        return events

    def check_feeds(
        self,
        engines: dict[str, Any],
        controller: Any = None,
    ) -> list[RecoveryEvent]:
        """Check for stale feeds and attempt reconnection.

        Args:
            engines: Mapping of strategy_name -> Engine.
            controller: Optional StrategyController for config access.

        Returns:
            List of RecoveryEvents.
        """
        events: list[RecoveryEvent] = []
        now = time.time()

        for name, engine in engines.items():
            try:
                # Get market slugs from controller if available
                market_slugs: list[str] = []
                if controller is not None:
                    handle = getattr(controller, "_strategies", {}).get(name)
                    if handle is not None:
                        market_slugs = getattr(handle.config, "markets", [])

                for slug in market_slugs:
                    try:
                        snap = engine.feed_snapshot(slug)
                        if snap is None:
                            continue
                        age = now - getattr(snap, "timestamp", now)
                        if age > self._stale_threshold:
                            event = RecoveryEvent(
                                action=RecoveryAction.RECONNECT_FEED,
                                target=f"{name}/{slug}",
                                timestamp=now,
                                success=False,
                                attempt=1,
                                error=f"Feed stale for {age:.0f}s (threshold={self._stale_threshold:.0f}s)",
                            )
                            with self._lock:
                                self._history.append(event)
                            events.append(event)
                            logger.warning(
                                "Stale feed detected: %s/%s (%.0fs old)",
                                name, slug, age,
                            )
                    except Exception:
                        pass
            except Exception:
                pass

        return events

    def is_permanently_failed(self, name: str) -> bool:
        """Check if a strategy has been permanently disabled."""
        with self._lock:
            return name in self._permanently_failed

    def reset(self, name: str) -> None:
        """Reset recovery state for a strategy (re-enable after permanent failure)."""
        with self._lock:
            self._permanently_failed.discard(name)
            self._retry_counts.pop(name, None)
            self._last_attempt.pop(name, None)
        logger.info("Recovery state reset for '%s'", name)

    def event_history(self, limit: int = 50) -> list[RecoveryEvent]:
        """Return recent recovery events."""
        with self._lock:
            return list(self._history)[-limit:]

    def stats(self) -> dict[str, Any]:
        """Return recovery statistics."""
        with self._lock:
            total = len(self._history)
            successes = sum(1 for e in self._history if e.success)
            return {
                "total_attempts": total,
                "successes": successes,
                "permanently_failed": list(self._permanently_failed),
                "pending_retries": dict(self._retry_counts),
            }

    @staticmethod
    def _restart_strategy(
        controller: Any,
        name: str,
        attempt: int,
        timestamp: float,
    ) -> RecoveryEvent:
        """Attempt to restart a failed strategy using its saved config."""
        try:
            # Access the saved handle for config and capital under controller lock
            with controller._lock:
                handle = controller._strategies.get(name)
                if handle is None:
                    return RecoveryEvent(
                        action=RecoveryAction.RESTART_STRATEGY,
                        target=name,
                        timestamp=timestamp,
                        success=False,
                        attempt=attempt,
                        error="Strategy handle not found",
                    )

                config = handle.config
                capital = handle.allocated_capital

            # Remove the failed strategy (acquires its own lock internally)
            controller.remove(name)

            # Redeploy with the same config and capital
            controller.deploy(config, capital)

            logger.info(
                "Strategy '%s' restarted successfully (attempt %d)",
                name, attempt,
            )
            return RecoveryEvent(
                action=RecoveryAction.RESTART_STRATEGY,
                target=name,
                timestamp=timestamp,
                success=True,
                attempt=attempt,
            )

        except Exception as e:
            logger.warning(
                "Strategy '%s' restart failed (attempt %d): %s",
                name, attempt, e,
            )
            return RecoveryEvent(
                action=RecoveryAction.RESTART_STRATEGY,
                target=name,
                timestamp=timestamp,
                success=False,
                attempt=attempt,
                error=str(e),
            )
