"""Base provider interface with retry logic and caching."""

import asyncio
import hashlib
import json
import time
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any

import httpx
from pydantic import BaseModel

from ..config import Config


class ProviderError(Exception):
    """Base exception for provider errors."""

    pass


class RateLimitError(ProviderError):
    """Rate limit exceeded."""

    def __init__(self, message: str, retry_after: int | None = None):
        super().__init__(message)
        self.retry_after = retry_after


class AuthenticationError(ProviderError):
    """Authentication failed."""

    pass


class CoinNotFoundError(ProviderError):
    """Coin/symbol not found."""

    pass


class ProviderData(BaseModel):
    """Base class for provider response data."""

    provider: str
    symbol: str
    raw_data: dict[str, Any] = {}


class ResponseCache:
    """Simple file-based response cache."""

    def __init__(self, ttl_minutes: int = 5):
        self.ttl_seconds = ttl_minutes * 60
        self.cache_dir = Config.get_data_dir() / "cache"
        self.cache_dir.mkdir(parents=True, exist_ok=True)

    def _get_cache_key(self, provider: str, endpoint: str, params: dict | None) -> str:
        """Generate cache key from request details."""
        key_data = f"{provider}:{endpoint}:{json.dumps(params or {}, sort_keys=True)}"
        return hashlib.md5(key_data.encode()).hexdigest()

    def _get_cache_path(self, key: str) -> Path:
        """Get path for cache file."""
        return self.cache_dir / f"{key}.json"

    def get(self, provider: str, endpoint: str, params: dict | None = None) -> dict | None:
        """Get cached response if valid."""
        key = self._get_cache_key(provider, endpoint, params)
        cache_path = self._get_cache_path(key)

        if not cache_path.exists():
            return None

        try:
            with open(cache_path, "r") as f:
                cached = json.load(f)

            # Check TTL
            if time.time() - cached.get("timestamp", 0) > self.ttl_seconds:
                cache_path.unlink(missing_ok=True)
                return None

            return cached.get("data")
        except (json.JSONDecodeError, IOError):
            return None

    def set(self, provider: str, endpoint: str, params: dict | None, data: dict) -> None:
        """Cache a response."""
        key = self._get_cache_key(provider, endpoint, params)
        cache_path = self._get_cache_path(key)

        try:
            with open(cache_path, "w") as f:
                json.dump({"timestamp": time.time(), "data": data}, f)
        except IOError:
            pass  # Cache write failure is not critical

    def clear(self) -> None:
        """Clear all cached responses."""
        for cache_file in self.cache_dir.glob("*.json"):
            cache_file.unlink(missing_ok=True)


class Provider(ABC):
    """Abstract base class for data providers with retry and caching."""

    name: str = "base"
    base_url: str = ""
    requires_auth: bool = True

    # Retry configuration
    max_retries: int = 3
    base_delay: float = 1.0  # Base delay in seconds
    max_delay: float = 30.0  # Maximum delay in seconds

    def __init__(self, api_key: str | None = None, timeout: float = 30.0, cache_ttl: int = 5):
        """Initialize provider with optional API key."""
        self.api_key = api_key
        self.timeout = timeout
        self._client: httpx.AsyncClient | None = None
        self._cache = ResponseCache(ttl_minutes=cache_ttl)

    @property
    def client(self) -> httpx.AsyncClient:
        """Get or create HTTP client."""
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                base_url=self.base_url,
                timeout=self.timeout,
                headers=self._get_headers(),
            )
        return self._client

    def _get_headers(self) -> dict[str, str]:
        """Get request headers including authentication."""
        headers = {
            "Accept": "application/json",
            "User-Agent": "FOMO-Guard/0.1.0",
        }
        if self.api_key:
            headers.update(self._get_auth_headers())
        return headers

    def _get_auth_headers(self) -> dict[str, str]:
        """Get authentication headers. Override in subclasses."""
        return {}

    async def _request_with_retry(
        self,
        method: str,
        endpoint: str,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        use_cache: bool = True,
    ) -> dict[str, Any]:
        """Make an HTTP request with retry logic and caching."""
        # Check cache first (only for GET requests)
        if method == "GET" and use_cache:
            cached = self._cache.get(self.name, endpoint, params)
            if cached is not None:
                return cached

        last_exception: Exception | None = None

        for attempt in range(self.max_retries):
            try:
                response = await self.client.request(
                    method=method,
                    url=endpoint,
                    params=params,
                    json=json_body,
                )

                # Handle rate limiting
                if response.status_code == 429:
                    retry_after = response.headers.get("Retry-After")
                    retry_seconds = int(retry_after) if retry_after else self._get_backoff_delay(attempt)

                    if attempt < self.max_retries - 1:
                        await asyncio.sleep(min(retry_seconds, self.max_delay))
                        continue
                    raise RateLimitError(
                        f"Rate limit exceeded for {self.name}",
                        retry_after=retry_seconds
                    )

                # Handle auth errors (no retry)
                if response.status_code in (401, 403):
                    raise AuthenticationError(f"Authentication failed for {self.name}")

                # Handle not found
                if response.status_code == 404:
                    raise CoinNotFoundError(f"Resource not found on {self.name}")

                response.raise_for_status()
                data = response.json()

                # Cache successful GET responses
                if method == "GET" and use_cache:
                    self._cache.set(self.name, endpoint, params, data)

                return data

            except httpx.HTTPStatusError as e:
                last_exception = ProviderError(f"{self.name} request failed: {e}")
                if attempt < self.max_retries - 1:
                    await asyncio.sleep(self._get_backoff_delay(attempt))
                    continue

            except httpx.RequestError as e:
                last_exception = ProviderError(f"{self.name} connection error: {e}")
                if attempt < self.max_retries - 1:
                    await asyncio.sleep(self._get_backoff_delay(attempt))
                    continue

        raise last_exception or ProviderError(f"{self.name} request failed after {self.max_retries} retries")

    def _get_backoff_delay(self, attempt: int) -> float:
        """Calculate exponential backoff delay."""
        delay = self.base_delay * (2 ** attempt)
        return min(delay, self.max_delay)

    async def get(
        self, endpoint: str, params: dict[str, Any] | None = None, use_cache: bool = True
    ) -> dict[str, Any]:
        """Make a GET request with retry and caching."""
        return await self._request_with_retry("GET", endpoint, params=params, use_cache=use_cache)

    async def post(
        self,
        endpoint: str,
        json: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Make a POST request with retry."""
        return await self._request_with_retry("POST", endpoint, params=params, json_body=json, use_cache=False)

    @abstractmethod
    async def fetch_data(self, symbol: str) -> ProviderData:
        """Fetch data for a given symbol. Must be implemented by subclasses."""
        pass

    def is_available(self) -> bool:
        """Check if this provider is available (has required credentials)."""
        if not self.requires_auth:
            return True
        return self.api_key is not None

    async def close(self) -> None:
        """Close the HTTP client."""
        if self._client and not self._client.is_closed:
            await self._client.aclose()
            self._client = None

    async def __aenter__(self) -> "Provider":
        """Async context manager entry."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """Async context manager exit."""
        await self.close()
