import logging
import time
from typing import Dict, List, Optional, Tuple
import requests
from requests.exceptions import (
    RequestException,
    Timeout,
    ConnectionError,
    HTTPError,
)
from functools import wraps
from django.core.cache import cache
from django.utils import timezone
from datetime import timedelta

logger = logging.getLogger(__name__)


class APIError(Exception):
    """Custom exception for API-related errors."""
    pass


class RetryExhausted(APIError):
    """Raised when maximum retries exceeded."""
    pass


def exponential_backoff(max_retries: int = 3, base_delay: int = 2):
    """
    Decorator for exponential backoff retry logic.
    
    Args:
        max_retries: Maximum number of retry attempts
        base_delay: Base delay in seconds (2^attempt * base_delay)
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except (Timeout, ConnectionError) as e:
                    last_exception = e
                    if attempt < max_retries - 1:
                        delay = base_delay * (2 ** attempt)
                        logger.warning(
                            f"Attempt {attempt + 1} failed: {str(e)}. "
                            f"Retrying in {delay}s..."
                        )
                        time.sleep(delay)
                    else:
                        logger.error(f"All {max_retries} attempts failed")
                except HTTPError as e:
                    if e.response.status_code >= 500:
                        last_exception = e
                        if attempt < max_retries - 1:
                            delay = base_delay * (2 ** attempt)
                            logger.warning(
                                f"Server error (attempt {attempt + 1}): {str(e)}. "
                                f"Retrying in {delay}s..."
                            )
                            time.sleep(delay)
                        else:
                            logger.error(f"Server error after {max_retries} attempts")
                    else:
                        raise  # Don't retry client errors
            
            if last_exception:
                raise RetryExhausted(
                    f"Failed after {max_retries} attempts: {str(last_exception)}"
                )
        
        return wrapper
    return decorator


class ExternalAPIService:
    """
    Service for handling external API integration.
    Provides methods for authenticating, fetching, and caching API responses.
    """

    def __init__(self, config):
        """
        Initialize the API service.
        
        Args:
            config: SyncConfiguration instance
        """
        self.config = config
        self.base_url = config.base_url.rstrip("/")
        self.endpoint = config.api_endpoint.lstrip("/")
        self.auth_type = config.auth_type
        self.auth_key = config.auth_key
        self.timeout = config.timeout_seconds
        self.max_retries = config.max_retries
        self.cache_enabled = config.cache_enabled
        self.cache_ttl = config.cache_ttl_seconds
        
        # Setup logging
        self.logger = logger
        self.logger.info(f"Initialized ExternalAPIService for {config.source_name}")

    def _get_headers(self) -> Dict[str, str]:
        """
        Build request headers with authentication.
        
        Returns:
            Dictionary of headers
        """
        headers = {
            "Content-Type": "application/json",
            "User-Agent": "University-Management-System/1.0",
        }

        if self.auth_type == "api_key":
            headers["X-API-Key"] = self.auth_key
        elif self.auth_type == "bearer_token":
            headers["Authorization"] = f"Bearer {self.auth_key}"
        elif self.auth_type == "basic_auth":
            import base64
            credentials = base64.b64encode(self.auth_key.encode()).decode()
            headers["Authorization"] = f"Basic {credentials}"

        return headers

    def _build_url(self, path: str = "", **params) -> str:
        """
        Build complete URL for API endpoint.
        
        Args:
            path: Additional path to append
            **params: Query parameters
        
        Returns:
            Complete URL
        """
        url = f"{self.base_url}/{self.endpoint}"
        if path:
            url = f"{url.rstrip('/')}/{path.lstrip('/')}"
        
        if params:
            from urllib.parse import urlencode
            query_string = urlencode(params)
            url = f"{url}?{query_string}"
        
        return url

    @exponential_backoff(max_retries=3, base_delay=2)
    def _make_request(
        self,
        method: str = "GET",
        path: str = "",
        data: Optional[Dict] = None,
        **params
    ) -> Dict:
        """
        Make HTTP request to external API with retry logic.
        
        Args:
            method: HTTP method (GET, POST, etc.)
            path: Additional path
            data: Request body for POST/PATCH/PUT
            **params: Query parameters
        
        Returns:
            JSON response as dictionary
        
        Raises:
            APIError: If request fails
            RetryExhausted: If max retries exceeded
        """
        url = self._build_url(path, **params)
        headers = self._get_headers()

        self.logger.debug(f"Making {method} request to {url}")

        try:
            if method == "GET":
                response = requests.get(url, headers=headers, timeout=self.timeout)
            elif method == "POST":
                response = requests.post(
                    url, headers=headers, json=data, timeout=self.timeout
                )
            elif method == "PATCH":
                response = requests.patch(
                    url, headers=headers, json=data, timeout=self.timeout
                )
            elif method == "DELETE":
                response = requests.delete(url, headers=headers, timeout=self.timeout)
            else:
                raise ValueError(f"Unsupported HTTP method: {method}")

            response.raise_for_status()
            
            self.logger.debug(f"Request successful. Status: {response.status_code}")
            return response.json()

        except requests.exceptions.JSONDecodeError as e:
            self.logger.error(f"Invalid JSON response: {str(e)}")
            raise APIError(f"Invalid JSON response from API: {str(e)}")
        except HTTPError as e:
            self.logger.error(f"HTTP Error {e.response.status_code}: {str(e)}")
            raise
        except RequestException as e:
            self.logger.error(f"Request failed: {str(e)}")
            raise

    def _get_cache_key(self, method: str, path: str, **params) -> str:
        """Generate cache key for API request."""
        import hashlib
        key_parts = [self.config.source_name, method, path]
        if params:
            from urllib.parse import urlencode
            key_parts.append(urlencode(sorted(params.items())))
        
        cache_key = "|".join(key_parts)
        return hashlib.md5(cache_key.encode()).hexdigest()

    def _get_from_cache(self, cache_key: str) -> Optional[Dict]:
        """Get response from cache if enabled and valid."""
        if not self.cache_enabled:
            return None
        
        cached_data = cache.get(cache_key)
        if cached_data:
            self.logger.info(f"Cache hit for {cache_key}")
            return cached_data
        
        return None

    def _set_in_cache(self, cache_key: str, data: Dict) -> None:
        """Store response in cache."""
        if self.cache_enabled:
            cache.set(cache_key, data, self.cache_ttl)
            self.logger.debug(f"Cached response for {cache_key} (TTL: {self.cache_ttl}s)")

    def fetch_organizations(
        self,
        limit: Optional[int] = None,
        offset: int = 0,
        use_cache: bool = True
    ) -> Dict[str, any]:
        """
        Fetch organizations from external API.
        
        Args:
            limit: Maximum number of organizations to fetch
            offset: Offset for pagination
            use_cache: Whether to use cached response if available
        
        Returns:
            API response with organizations data
        
        Raises:
            APIError: If fetch fails
            RetryExhausted: If max retries exceeded
        """
        params = {}
        if limit:
            params["limit"] = limit
        if offset:
            params["offset"] = offset

        cache_key = self._get_cache_key("GET", "", **params)

        # Check cache first
        if use_cache:
            cached_response = self._get_from_cache(cache_key)
            if cached_response:
                return cached_response

        # Make request
        response = self._make_request("GET", **params)

        # Cache the response
        self._set_in_cache(cache_key, response)

        return response

    def fetch_organization_by_id(self, external_id: str) -> Dict:
        """
        Fetch a specific organization by external ID.
        
        Args:
            external_id: External organization ID
        
        Returns:
            Organization data
        
        Raises:
            APIError: If fetch fails
        """
        return self._make_request("GET", path=external_id)

    def fetch_paginated(
        self,
        batch_size: int = 100,
        max_records: Optional[int] = None
    ) -> List[Dict]:
        """
        Fetch all organizations using pagination.
        
        Args:
            batch_size: Records per request
            max_records: Maximum total records to fetch
        
        Yields:
            Individual organization records
        
        Raises:
            APIError: If fetch fails
        """
        offset = 0
        total_fetched = 0

        while True:
            self.logger.info(f"Fetching batch at offset {offset}")
            
            response = self.fetch_organizations(
                limit=batch_size,
                offset=offset,
                use_cache=False  # Don't cache paginated requests
            )

            # Handle different response formats
            if isinstance(response, dict):
                # Format: {"data": [...], "total": X}
                records = response.get("data", [])
                total = response.get("total")
            elif isinstance(response, list):
                # Format: [...]
                records = response
                total = len(response)
            else:
                raise APIError(f"Unexpected API response format: {type(response)}")

            if not records:
                self.logger.info("No more records to fetch")
                break

            for record in records:
                yield record
                total_fetched += 1

                if max_records and total_fetched >= max_records:
                    self.logger.info(f"Reached max_records limit: {max_records}")
                    return

            # Check if we got all records
            if total and offset + batch_size >= total:
                self.logger.info("Fetched all available records")
                break

            offset += batch_size

    def test_connection(self) -> bool:
        """
        Test API connection and authentication.
        
        Returns:
            True if connection successful
        
        Raises:
            APIError: If connection fails
        """
        self.logger.info(f"Testing connection to {self.config.source_name}")
        try:
            response = self.fetch_organizations(limit=1, use_cache=False)
            self.logger.info("Connection test successful")
            return True
        except Exception as e:
            self.logger.error(f"Connection test failed: {str(e)}")
            raise APIError(f"Connection test failed: {str(e)}")

    def clear_cache(self) -> None:
        """Clear all cached responses for this source."""
        key_pattern = f"{self.config.source_name}*"
        cache.delete_pattern(key_pattern)
        self.logger.info(f"Cleared cache for {self.config.source_name}")

    def get_cache_stats(self) -> Dict[str, any]:
        """Get caching statistics."""
        return {
            "cache_enabled": self.cache_enabled,
            "cache_ttl": self.cache_ttl,
            "source": self.config.source_name
        }
