"""
Fix Loader - Fetches active fixes from the API.

Handles:
- Initial loading of fixes on startup
- Periodic refresh in background
- Error handling and fallback to cache
"""

from __future__ import annotations

import asyncio
import logging
import threading
import time
from typing import Any, Callable, List, Optional

from risicare.runtime.cache import FixCache
from risicare.runtime.config import ActiveFix, FixRuntimeConfig

logger = logging.getLogger(__name__)


class FixLoader:
    """
    Loads active fixes from the Risicare API.

    Features:
    - Async and sync loading support
    - Background refresh thread
    - Error handling with exponential backoff
    - Cache integration
    """

    def __init__(
        self,
        config: Optional[FixRuntimeConfig] = None,
        cache: Optional[FixCache] = None,
    ) -> None:
        """
        Initialize the loader.

        Args:
            config: Runtime configuration.
            cache: Optional fix cache.
        """
        self._config = config or FixRuntimeConfig.from_env()
        self._cache = cache or FixCache(self._config)
        self._http_client: Optional[Any] = None  # httpx.Client (lazy import)
        self._async_client: Optional[Any] = None  # httpx.AsyncClient (lazy import)
        self._refresh_thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()
        self._last_load_time: Optional[float] = None
        self._consecutive_failures = 0
        self._on_load_callbacks: List[Callable[[List[ActiveFix]], None]] = []

    @property
    def last_load_time(self) -> Optional[float]:
        """Get timestamp of last successful load."""
        return self._last_load_time

    def start(self) -> None:
        """
        Start the loader.

        Performs initial load and starts background refresh if enabled.
        """
        if not self._config.enabled:
            logger.debug("Fix runtime disabled, skipping loader start")
            return

        if not self._config.api_key:
            logger.warning("No API key configured, fixes will not be loaded")
            return

        # Initial load (synchronous)
        try:
            self.load_sync()
        except Exception as e:
            logger.warning(f"Initial fix load failed: {e}")

        # Start background refresh
        if self._config.auto_refresh:
            self._start_refresh_thread()

    def stop(self) -> None:
        """
        Stop the loader.

        Stops background refresh and cleans up resources.
        """
        self._stop_event.set()

        if self._refresh_thread and self._refresh_thread.is_alive():
            self._refresh_thread.join(timeout=5.0)

        if self._http_client:
            self._http_client.close()
            self._http_client = None

        if self._async_client:
            try:
                # Attempt to close async client if there's a running loop
                loop = asyncio.get_running_loop()
                loop.create_task(self._async_client.aclose())
            except RuntimeError:
                # No running loop — schedule close or let GC handle it
                pass
            self._async_client = None

    def load_sync(self) -> List[ActiveFix]:
        """
        Load fixes synchronously.

        Returns:
            List of active fixes.
        """
        if not self._config.api_key or not self._config.project_id:
            return []

        try:
            client = self._get_http_client()
            response = client.get(
                f"{self._config.api_endpoint}/v1/fixes/active",
                params={"project_id": self._config.project_id},
                headers=self._get_headers(),
                timeout=self._config.timeout_ms / 1000,
            )
            response.raise_for_status()

            fixes = self._parse_response(response.json())
            self._on_load_success(fixes)
            return fixes

        except Exception as e:
            self._on_load_failure(e)
            raise

    async def load_async(self) -> List[ActiveFix]:
        """
        Load fixes asynchronously.

        Returns:
            List of active fixes.
        """
        if not self._config.api_key or not self._config.project_id:
            return []

        try:
            client = await self._get_async_client()
            response = await client.get(
                f"{self._config.api_endpoint}/v1/fixes/active",
                params={"project_id": self._config.project_id},
                headers=self._get_headers(),
                timeout=self._config.timeout_ms / 1000,
            )
            response.raise_for_status()

            fixes = self._parse_response(response.json())
            self._on_load_success(fixes)
            return fixes

        except Exception as e:
            self._on_load_failure(e)
            raise

    def on_load(self, callback: Callable[[List[ActiveFix]], None]) -> None:
        """
        Register a callback for when fixes are loaded.

        Args:
            callback: Function called with list of fixes.
        """
        self._on_load_callbacks.append(callback)

    def get_cached(self) -> List[ActiveFix]:
        """
        Get currently cached fixes.

        Returns:
            List of cached fixes.
        """
        return self._cache.get_all()

    def _get_http_client(self) -> Any:
        """Get or create synchronous HTTP client."""
        if self._http_client is None:
            import httpx
            self._http_client = httpx.Client()
        return self._http_client

    async def _get_async_client(self) -> Any:
        """Get or create asynchronous HTTP client."""
        if self._async_client is None:
            import httpx
            self._async_client = httpx.AsyncClient()
        return self._async_client

    def _get_headers(self) -> dict:
        """Get API request headers."""
        return {
            "Authorization": f"Bearer {self._config.api_key}",
            "Content-Type": "application/json",
            "X-Risicare-SDK-Version": "0.1.0",
        }

    def _parse_response(self, data: dict) -> List[ActiveFix]:
        """
        Parse API response into ActiveFix objects.

        Args:
            data: API response data.

        Returns:
            List of ActiveFix objects.
        """
        fixes = []
        items = data.get("fixes", []) or data.get("data", []) or []

        for item in items:
            try:
                fix = ActiveFix(
                    fix_id=item["id"],
                    deployment_id=item.get("deployment_id", ""),
                    error_code=item["error_code"],
                    fix_type=item["fix_type"],
                    config=item.get("config", {}),
                    traffic_percentage=item.get("traffic_percentage", 100),
                    version=item.get("version", 1),
                )
                fixes.append(fix)
            except (KeyError, TypeError) as e:
                logger.warning(f"Failed to parse fix: {e}")
                continue

        return fixes

    def _on_load_success(self, fixes: List[ActiveFix]) -> None:
        """Handle successful load."""
        self._last_load_time = time.time()
        self._consecutive_failures = 0

        # Update cache
        self._cache.set_all(fixes)

        # Notify callbacks
        for callback in self._on_load_callbacks:
            try:
                callback(fixes)
            except Exception as e:
                logger.warning(f"Load callback error: {e}")

        if self._config.debug:
            logger.info(f"Loaded {len(fixes)} active fixes")

    def _on_load_failure(self, error: Exception) -> None:
        """Handle load failure."""
        self._consecutive_failures += 1
        logger.warning(f"Fix load failed (attempt {self._consecutive_failures}): {error}")

    def _start_refresh_thread(self) -> None:
        """Start background refresh thread."""
        self._stop_event.clear()
        self._refresh_thread = threading.Thread(
            target=self._refresh_loop,
            name="risicare-fix-refresh",
            daemon=True,
        )
        self._refresh_thread.start()
        logger.debug("Started fix refresh thread")

    def _refresh_loop(self) -> None:
        """Background refresh loop."""
        while not self._stop_event.is_set():
            # Calculate sleep time with exponential backoff on failures
            base_interval = self._config.refresh_interval_seconds
            backoff_factor = min(2 ** self._consecutive_failures, 60)  # Max 60x
            interval = base_interval * backoff_factor

            # Wait for interval or stop event
            if self._stop_event.wait(timeout=interval):
                break

            # Refresh fixes
            try:
                self.load_sync()
            except Exception as e:
                # Error already logged in load_sync
                pass

        logger.debug("Fix refresh thread stopped")
