"""Main SubSystem API Handler Module.

This module provides a comprehensive interface for interacting with the JL Main SubSystem API.
It includes HTTP client management, automatic retry logic, error handling, and both tenant-specific
and tenantless API operations.

Key Features:
    - Async HTTP client with connection management
    - Automatic retry with exponential backoff
    - Thread-local instance caching for performance
    - Support for all HTTP methods (GET, POST, PUT, DELETE, PATCH)
    - Tenant-specific and tenantless API endpoints
    - Comprehensive error handling and logging
    - Context manager support for proper resource cleanup

Example:
    Basic usage:
        api = MainSubSysApiManager.get_instance(tenant_id, config)
        
        # Make API calls
        response = await api.base_method_async("customers", RequestType.GET)
        if response.status == 200:
            data = await response.json()
            
        # Tenantless API call
        response = await api.base_method_tenantless_async("health", RequestType.GET)
"""

import aiohttp
from aiohttp import ClientResponse
import logging
from enum import Enum
from typing import Dict, Any, Optional, List
from threading import local
from functools import lru_cache
from functools import wraps
import asyncio
from datetime import datetime, timedelta, timezone
from collections import defaultdict
import json
from .appconfig_handler import Configurations


class RequestType(Enum):
    """Enumeration for HTTP request methods.
    
    This enum defines the supported HTTP methods for API operations,
    providing type safety and consistency across the application.
    """
    GET = "GET"        # Retrieve data from the server
    POST = "POST"      # Create new resources
    PUT = "PUT"        # Update existing resources (full update)
    DELETE = "DELETE"  # Remove resources
    PATCH = "PATCH"    # Partial update of existing resources


class ApiError(Exception):
    """Custom exception class for API-related errors.
    
    This exception is raised when API operations fail, providing
    detailed error information including HTTP status codes when available.
    
    Attributes:
        message (str): Human-readable error message.
        status_code (Optional[int]): HTTP status code if available.
    """
    def __init__(self, message: str, status_code: Optional[int] = None):
        """Initialize the ApiError with message and optional status code.
        
        Args:
            message (str): Error message describing what went wrong.
            status_code (Optional[int]): HTTP status code if available.
        """
        self.message = message
        self.status_code = status_code
        super().__init__(self.message)

def retry_with_backoff(retries=3, backoff_in_seconds=1):
    """Decorator that implements retry logic with exponential backoff.
    
    This decorator automatically retries failed API operations with increasing
    delay between attempts. It catches transient network errors and connection
    issues that might be resolved with a retry.
    
    Args:
        retries (int): Maximum number of retry attempts. Defaults to 3.
        backoff_in_seconds (int): Base delay in seconds, doubled with each retry. Defaults to 1.
        
    Returns:
        Callable: Decorator function that wraps the target method.
        
    Example:
        @retry_with_backoff(retries=5, backoff_in_seconds=2)
        async def api_call(self):
            # This will retry up to 5 times with delays: 2s, 4s, 8s, 16s, 32s
            pass
    """
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            max_retries = retries
            for attempt in range(max_retries):
                try:
                    return await func(*args, **kwargs)
                except (
                    aiohttp.ClientError,
                    aiohttp.ServerTimeoutError,
                    aiohttp.ConnectionTimeoutError,
                    ConnectionResetError,
                    asyncio.TimeoutError
                ) as e:
                    if attempt == max_retries - 1:
                        raise ApiError(f"Max retries reached: {str(e)}")
                    wait_time = (backoff_in_seconds * 2 ** attempt)
                    logging.warning(
                        f"Attempt {attempt + 1} failed, retrying in {wait_time}s: {str(e)}"
                    )
                    await asyncio.sleep(wait_time)
            return None
        return wrapper
    return decorator

class ApiCallLimiter:
    """Enforces API call limits per tenant using sliding window algorithm.
    
    Tracks and limits API calls for live and test tenants independently based on
    configuration. Uses sliding window for accurate rate limiting.
    """
    
    def __init__(self, config: Configurations):
        """Initialize limiter with configuration from Azure App Config."""
        self.config = config
        self.call_history: Dict[str, List[datetime]] = defaultdict(list)
        self._lock = asyncio.Lock()
        self._load_config()
    
    def _load_config(self):
        """Load API call limits from Azure App Config 'ApiCallLimits' JSON key."""
        try:

            config_json = json.loads(self.config.ApiCallLimits)
        except json.JSONDecodeError as e:
            logging.error(f"Invalid JSON in ApiCallLimits config: {e}")
            logging.error(f"Config value: {self.config.ApiCallLimits}")
            raise ValueError(f"ApiCallLimits configuration contains invalid JSON: {e}")
        
        
        client_config = config_json['ClientConfig']
        self.live_enabled = client_config['Enabled']
        self.live_max_calls = int(client_config['MaxCalls'])
        self.live_time_value = int(client_config['TimeValue'])
        
       
        test_config = config_json['TestClientConfig']
        self.test_enabled = test_config['Enabled']
        self.test_max_calls = int(test_config['MaxCalls'])
        self.test_time_value = int(test_config['TimeValue'])
        
       
        self.test_tenant_ids = set(tid.lower() for tid in test_config['TenantIds'])
    
    def _normalize_tenant(self, tenant_id: str) -> str:
        """Normalize tenant ID to lowercase for case-insensitive comparison.
        
        Args:
            tenant_id (str): The tenant ID to normalize.
            
        Returns:
            str: Normalized tenant ID (lowercase, stripped of whitespace).
        """
        return (tenant_id or "").strip().lower()
    
    def _is_test_tenant(self, tenant_id: str) -> bool:
        """Check if tenant is in test tenant list (case-insensitive)."""
        return self._normalize_tenant(tenant_id) in self.test_tenant_ids
    
    def _get_time_window_in_minutes(self, is_test: bool) -> timedelta:
        """Get time window (timedelta) for test or live tenant in minutes."""
        time_value = self.test_time_value if is_test else self.live_time_value
        return timedelta(minutes=time_value)
    
    async def check_call_limit(self, tenant_id: str) -> None:
        """Check and enforce API call limits for tenant using sliding window algorithm.
        
        Raises ApiCallLimitExceeded if tenant exceeds their configured limit.
        """
        # Normalize tenant_id for case-insensitive tracking
        tenant_id_normalized = self._normalize_tenant(tenant_id)
        
        is_test_tenant = tenant_id_normalized in self.test_tenant_ids
        
        # Apply config based on tenant type
        if is_test_tenant:
            
            if not self.test_enabled:
                return
            use_test_config = True
        else:
            # Tenant is NOT in test list - use live config
            if not self.live_enabled:
                return
            use_test_config = False
        
        # Apply the limits
        async with self._lock:
            now = datetime.now(timezone.utc)
            time_window = self._get_time_window_in_minutes(use_test_config)
            max_calls = self.test_max_calls if use_test_config else self.live_max_calls
            
            calls = self.call_history[tenant_id_normalized]
            
            cutoff_time = now - time_window
            self.call_history[tenant_id_normalized] = [
                call_time for call_time in calls if call_time > cutoff_time
            ]
            
            # Check if limit exceeded
            current_calls = len(self.call_history[tenant_id_normalized])
            if current_calls >= max_calls:
                # Calculate retry_after (seconds until oldest call expires)
                oldest_call = min(self.call_history[tenant_id_normalized])
                retry_after = int((oldest_call + time_window - now).total_seconds())
                
                raise Exception(
                    f"Rate limit exceeded ({current_calls}/{max_calls} calls). Retry after {retry_after}s. "
                    f"Tenant ID: {tenant_id}, Retry after: {retry_after}s"
                )
            
            self.call_history[tenant_id_normalized].append(now)


def api_call_limit(func):
    """Decorator to enforce API call limits before method execution."""
    @wraps(func)
    async def wrapper(self, *args, **kwargs):
        tenant_id = kwargs.get('tenant_id') or self.tenant_id
        
        if hasattr(self, '_call_limiter') and self._call_limiter:
            await self._call_limiter.check_call_limit(tenant_id)
        
        return await func(self, *args, **kwargs)
    
    return wrapper


class MainSubSysApiManager:
    """Main SubSystem API Manager for handling HTTP operations.
    
    This class provides a centralized interface for making HTTP requests to the
    JL Main SubSystem API. It includes automatic retry logic, connection management,
    and support for both tenant-specific and tenantless operations.
    
    The class implements thread-local instance caching to ensure efficient resource
    usage while maintaining thread safety in concurrent environments.
    
    Attributes:
        tenant_id (str): Tenant identifier for API operations.
        header (str): Header key name for authentication.
        url (str): Base URL for the Main SubSystem API.
        header_key (str): Authentication header value.
        client (Optional[aiohttp.ClientSession]): HTTP client session.
        _closed (bool): Flag indicating if the client has been closed.
        
    Class Attributes:
        _thread_local (threading.local): Thread-local storage for instances.
    """

    _thread_local = local()
    
    
    def __init__(self, tenant_id: str, config: Configurations):
        """Initialize the MainSubSysApiManager with tenant and configuration details.
        
        Args:
            tenant_id (str): Tenant identifier for multi-tenancy support.
            config (Configurations): Configuration object containing API settings.
        """
        self.tenant_id = tenant_id
        self.header: str = "jl-x-header-token-key"
        self.url: str = config.MainSubSysAutomationAPI
        self.header_key: str = config.MainSubSysAutomationAPIHeader
        self.client = None
        self._closed = False
        self._call_limiter = ApiCallLimiter(config)

    async def _ensure_client(self):
        """Ensure that the HTTP client session is available and not closed.
        
        This method creates a new client session if one doesn't exist or if the
        current session has been closed.
        """
        if not self.client or self.client.closed:
            self.client = aiohttp.ClientSession()

    @classmethod
    @lru_cache(maxsize=100)
    def get_instance(cls, tenant_id: str, config: Configurations) -> 'MainSubSysApiManager':
        """Get or create a cached instance for the specified tenant.
        
        This method implements thread-local caching to ensure that each tenant
        gets a dedicated API manager instance while maintaining thread safety.
        
        Args:
            tenant_id (str): Tenant identifier.
            config (Configurations): Configuration object.
            
        Returns:
            MainSubSysApiManager: Cached or new instance for the tenant.
        """
        if not hasattr(cls._thread_local, 'instances'):
            cls._thread_local.instances = {}
        
        if tenant_id not in cls._thread_local.instances:
            cls._thread_local.instances[tenant_id] = cls(tenant_id, config)
        
        return cls._thread_local.instances[tenant_id]

    @retry_with_backoff()
    @api_call_limit
    async def base_method_async(
        self, 
        api_url: str, 
        method: RequestType,
        payload: Dict[str, Any] = None,
        params: Dict[str, Any] = None,  
        tenant_id: str = None
    ) -> ClientResponse:
        """Make tenant-specific API request and return raw ClientResponse.
        
        This method constructs and executes HTTP requests to tenant-specific API endpoints
        with automatic retry logic and error handling. The raw response is returned
        for flexible handling by the caller.
        
        Args:
            api_url (str): API endpoint path (without base URL).
            method (RequestType): HTTP method to use for the request.
            payload (Optional[Dict[str, Any]]): JSON payload for POST/PUT requests.
            params (Optional[Dict[str, Any]]): Query parameters for the request.
            tenant_id (Optional[str]): Override tenant ID (uses instance default if None).
            
        Returns:
            ClientResponse: Raw aiohttp response object for flexible handling.
            
        Raises:
            ApiError: If the API request fails after all retry attempts.
            
        Example:
            response = await api.base_method_async("customers", RequestType.GET)
            if response.status == 200:
                data = await response.json()
            else:
                error_text = await response.text()
                logging.error(f"API error: {error_text}")
        """
        await self._ensure_client()
        tenant_id = tenant_id or self.tenant_id

        headers = {
            "Content-Type": "application/json",
            self.header: self.header_key
        }
        
        url = f"{self.url}/tenancy/{tenant_id}/{api_url}"
        
        try:
            return await self.client.request(
                method.value,
                url=url,
                headers=headers,
                json=payload,
                params=params
            )
                
        except aiohttp.ClientError as e:
            logging.error(f"API error: {str(e)}")
            raise ApiError(str(e))

    @retry_with_backoff()
    async def base_method_tenantless_async(
        self, 
        api_url: str, 
        method: RequestType,
        payload: Dict[str, Any] = None,
        params: Dict[str, Any] = None
    ) -> ClientResponse:
        """Make tenantless API request and return raw ClientResponse.
        
        This method constructs and executes HTTP requests to tenantless API endpoints
        (typically for system-wide operations) with automatic retry logic and error handling.
        
        Args:
            api_url (str): API endpoint path (without base URL).
            method (RequestType): HTTP method to use for the request.
            payload (Optional[Dict[str, Any]]): JSON payload for POST/PUT requests.
            params (Optional[Dict[str, Any]]): Query parameters for the request.
            
        Returns:
            ClientResponse: Raw aiohttp response object for flexible handling.
            
        Raises:
            ApiError: If the API request fails after all retry attempts.
            
        Example:
            response = await api.base_method_tenantless_async("health", RequestType.GET)
            health_status = await response.json()
        """
        await self._ensure_client()

        headers = {
            "Content-Type": "application/json",
            self.header: self.header_key
        }
        
        url = f"{self.url}/tenantless/UK/{api_url}"
        
        try:
            return await self.client.request(
                method.value,
                url=url,
                headers=headers,
                json=payload,
                params=params
            )
                
        except aiohttp.ClientError as e:
            logging.error(f"API error: {str(e)}")
            raise ApiError(str(e))
        
    async def close(self):
        """Close the HTTP client session and clean up resources.
        
        This method ensures proper cleanup of the aiohttp ClientSession
        to prevent resource leaks and connection warnings.
        """
        if self.client and not self.client.closed:
            await self.client.close()
            self._closed = True
            self.client = None

    async def __aenter__(self):
        """Async context manager entry.
        
        Returns:
            MainSubSysApiManager: Self reference for context manager usage.
            
        Example:
            async with MainSubSysApiManager.get_instance(tenant_id, config) as api:
                response = await api.base_method_async("endpoint", RequestType.GET)
        """
        await self._ensure_client()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit with automatic cleanup.
        
        Args:
            exc_type: Exception type if an exception occurred.
            exc_val: Exception value if an exception occurred.
            exc_tb: Exception traceback if an exception occurred.
        """
        await self.close()

    def __del__(self):
        """Ensure resources are cleaned up when the object is destroyed.
        
        This method schedules cleanup as an asyncio task to handle cases
        where explicit cleanup wasn't performed.
        """
        if self.client and not self.client.closed:
            asyncio.create_task(self.close())

    @classmethod
    async def cleanup_all(cls):
        """Clean up all cached instances and their resources.
        
        This method should be called when shutting down the application
        to ensure all API manager instances are properly cleaned up.
        
        Example:
            # During application shutdown
            await MainSubSysApiManager.cleanup_all()
        """
        if hasattr(cls._thread_local, 'instances'):
            for instance in cls._thread_local.instances.values():
                await instance.close()