"""
Async Client Support for Connectors
Non-blocking operations with asyncio and aiohttp
"""

import asyncio
import aiohttp
import json
from typing import Dict, List, Any, Optional, Union, Callable
from dataclasses import dataclass
from .exceptions import (
    NetworkError, TimeoutError, AuthenticationError,
    RateLimitError, APIError
)
from .logging_config import get_logger
from .cache import MultiLevelCache, cached, get_global_cache
from .retry import async_retry
from .rate_limiter import RateLimiter, RateLimitConfig

logger = get_logger(__name__)


@dataclass
class AsyncClientConfig:
    """Configuration for async HTTP client"""
    
    timeout: float = 30.0
    max_retries: int = 3
    retry_delay: float = 1.0
    max_connections: int = 100
    connection_timeout: float = 10.0
    read_timeout: float = 30.0
    
    # Rate limiting
    rate_limit_requests: Optional[int] = None
    rate_limit_period: Optional[float] = None
    
    # SSL settings
    verify_ssl: bool = True
    ssl_context: Optional[Any] = None
    
    # Headers
    default_headers: Dict[str, str] = None
    
    def __post_init__(self):
        """Set default values"""
        if self.default_headers is None:
            self.default_headers = {
                'User-Agent': 'connectors-async-client/1.0',
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }


class AsyncHTTPClient:
    """Async HTTP client with retry, rate limiting, and caching"""
    
    def __init__(
        self,
        config: AsyncClientConfig,
        base_url: Optional[str] = None,
        auth_headers: Optional[Dict[str, str]] = None,
        cache: Optional[MultiLevelCache] = None
    ):
        self.config = config
        self.base_url = base_url
        self.auth_headers = auth_headers or {}
        self.cache = cache
        
        # Rate limiter
        if config.rate_limit_requests and config.rate_limit_period:
            rate_config = RateLimitConfig(
                requests_per_period=config.rate_limit_requests,
                period_seconds=config.rate_limit_period
            )
            self.rate_limiter = RateLimiter(rate_config)
        else:
            self.rate_limiter = None
        
        # Session will be created when needed
        self._session: Optional[aiohttp.ClientSession] = None
        self._session_lock = asyncio.Lock()
    
    async def _get_session(self) -> aiohttp.ClientSession:
        """Get or create HTTP session"""
        if self._session is None or self._session.closed:
            async with self._session_lock:
                if self._session is None or self._session.closed:
                    timeout = aiohttp.ClientTimeout(
                        total=self.config.timeout,
                        connect=self.config.connection_timeout,
                        sock_read=self.config.read_timeout
                    )
                    
                    connector = aiohttp.TCPConnector(
                        limit=self.config.max_connections,
                        verify_ssl=self.config.verify_ssl,
                        ssl_context=self.config.ssl_context
                    )
                    
                    headers = {**self.config.default_headers, **self.auth_headers}
                    
                    self._session = aiohttp.ClientSession(
                        timeout=timeout,
                        connector=connector,
                        headers=headers
                    )
        
        return self._session
    
    async def close(self) -> None:
        """Close the HTTP session"""
        if self._session and not self._session.closed:
            await self._session.close()
    
    @async_retry(
        max_attempts=3,
        base_delay=1.0,
        retry_on_exceptions=[NetworkError, TimeoutError, aiohttp.ClientError]
    )
    async def request(
        self,
        method: str,
        url: str,
        **kwargs
    ) -> Union[Dict[str, Any], str, bytes]:
        """Make async HTTP request with retry and rate limiting"""
        
        # Apply rate limiting
        if self.rate_limiter:
            await self._apply_rate_limit()
        
        # Build full URL
        if self.base_url and not url.startswith('http'):
            url = f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"
        
        session = await self._get_session()
        
        try:
            async with session.request(method, url, **kwargs) as response:
                # Handle API errors
                if response.status >= 400:
                    await handle_api_error_async(response, f"{method} {url}")
                
                # Process response
                content_type = response.headers.get('Content-Type', '')
                
                if 'application/json' in content_type:
                    return await response.json()
                elif 'text/' in content_type:
                    return await response.text()
                else:
                    return await response.read()
        
        except asyncio.TimeoutError as e:
            raise TimeoutError(f"Request timeout for {method} {url}") from e
        except aiohttp.ClientError as e:
            raise NetworkError(f"Network error for {method} {url}: {e}") from e
    
    async def _apply_rate_limit(self) -> None:
        """Apply rate limiting"""
        if self.rate_limiter:
            status = self.rate_limiter.check_limit()
            
            if not status['is_allowed']:
                wait_time = status['wait_time']
                if wait_time > 0:
                    logger.info(f"Rate limiting: waiting {wait_time:.2f}s")
                    await asyncio.sleep(wait_time)
            
            # Record the request
            self.rate_limiter.acquire()
    
    async def get(self, url: str, params: Optional[Dict] = None, **kwargs) -> Dict[str, Any]:
        """Make GET request"""
        return await self.request('GET', url, params=params, **kwargs)
    
    async def post(self, url: str, data: Optional[Dict] = None, json_data: Optional[Dict] = None, **kwargs) -> Dict[str, Any]:
        """Make POST request"""
        if json_data:
            kwargs['json'] = json_data
        elif data:
            kwargs['data'] = data
        
        return await self.request('POST', url, **kwargs)
    
    async def put(self, url: str, data: Optional[Dict] = None, json_data: Optional[Dict] = None, **kwargs) -> Dict[str, Any]:
        """Make PUT request"""
        if json_data:
            kwargs['json'] = json_data
        elif data:
            kwargs['data'] = data
        
        return await self.request('PUT', url, **kwargs)
    
    async def delete(self, url: str, **kwargs) -> Dict[str, Any]:
        """Make DELETE request"""
        return await self.request('DELETE', url, **kwargs)
    
    @cached(get_global_cache(), ttl=300)
    async def cached_get(self, url: str, params: Optional[Dict] = None, **kwargs) -> Dict[str, Any]:
        """Make cached GET request"""
        return await self.get(url, params, **kwargs)


async def handle_api_error_async(response: aiohttp.ClientResponse, operation: str = "API call") -> None:
    """Handle async API response errors appropriately"""
    status_code = response.status
    
    try:
        response_data = await response.json()
    except (json.JSONDecodeError, aiohttp.ContentTypeError):
        response_data = {}
    
    if status_code == 401:
        raise AuthenticationError(
            f"Authentication failed for {operation}",
            error_code="AUTH_401",
            status_code=status_code,
            response_data=response_data
        )
    elif status_code == 403:
        from .exceptions import PermissionError
        raise PermissionError(
            f"Permission denied for {operation}",
            error_code="PERM_403",
            status_code=status_code,
            response_data=response_data
        )
    elif status_code == 404:
        from .exceptions import ResourceNotFoundError
        raise ResourceNotFoundError(
            f"Resource not found for {operation}",
            error_code="NOTFOUND_404",
            status_code=status_code,
            response_data=response_data
        )
    elif status_code == 429:
        retry_after = response.headers.get('Retry-After')
        raise RateLimitError(
            f"Rate limit exceeded for {operation}",
            error_code="RATE_429",
            status_code=status_code,
            retry_after=int(retry_after) if retry_after else None,
            response_data=response_data
        )
    elif status_code >= 500:
        from .exceptions import ServiceUnavailableError
        raise ServiceUnavailableError(
            f"Service unavailable for {operation}",
            error_code="SERVICE_5XX",
            status_code=status_code,
            response_data=response_data
        )
    else:
        raise APIError(
            f"API error for {operation}",
            error_code=f"API_{status_code}",
            status_code=status_code,
            response_data=response_data
        )


class AsyncBatchProcessor:
    """Batch processor for async operations"""
    
    def __init__(
        self,
        batch_size: int = 10,
        max_concurrent: int = 5,
        delay_between_batches: float = 0.1
    ):
        self.batch_size = batch_size
        self.max_concurrent = max_concurrent
        self.delay_between_batches = delay_between_batches
        self.semaphore = asyncio.Semaphore(max_concurrent)
    
    async def process_batch(
        self,
        items: List[Any],
        process_func: Callable[[Any], Any],
        progress_callback: Optional[Callable[[int, int], None]] = None
    ) -> List[Any]:
        """Process items in batches with concurrency control"""
        results = []
        total_items = len(items)
        
        async def process_item(item: Any) -> Any:
            async with self.semaphore:
                return await process_func(item)
        
        # Process in batches
        for i in range(0, total_items, self.batch_size):
            batch = items[i:i + self.batch_size]
            
            # Process batch concurrently
            batch_tasks = [process_item(item) for item in batch]
            batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
            
            # Handle exceptions in results
            for j, result in enumerate(batch_results):
                if isinstance(result, Exception):
                    logger.error(f"Error processing item {i + j}: {result}")
                    results.append(None)
                else:
                    results.append(result)
            
            # Report progress
            if progress_callback:
                progress_callback(min(i + self.batch_size, total_items), total_items)
            
            # Delay between batches
            if i + self.batch_size < total_items:
                await asyncio.sleep(self.delay_between_batches)
        
        return results
    
    async def process_all(
        self,
        items: List[Any],
        process_func: Callable[[Any], Any],
        progress_callback: Optional[Callable[[int, int], None]] = None
    ) -> List[Any]:
        """Process all items with full concurrency"""
        async def process_item(item: Any) -> Any:
            async with self.semaphore:
                return await process_func(item)
        
        tasks = [process_item(item) for item in items]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # Handle exceptions
        processed_results = []
        for i, result in enumerate(results):
            if isinstance(result, Exception):
                logger.error(f"Error processing item {i}: {result}")
                processed_results.append(None)
            else:
                processed_results.append(result)
        
        return processed_results


class AsyncConnectionPool:
    """Connection pool for async HTTP clients"""
    
    def __init__(
        self,
        max_connections: int = 100,
        base_config: Optional[AsyncClientConfig] = None
    ):
        self.max_connections = max_connections
        self.base_config = base_config or AsyncClientConfig()
        self._clients: Dict[str, AsyncHTTPClient] = {}
        self._lock = asyncio.Lock()
    
    async def get_client(
        self,
        base_url: str,
        auth_headers: Optional[Dict[str, str]] = None,
        config_override: Optional[AsyncClientConfig] = None
    ) -> AsyncHTTPClient:
        """Get or create client for base URL"""
        client_key = f"{base_url}:{hash(str(auth_headers))}"
        
        async with self._lock:
            if client_key not in self._clients:
                config = config_override or self.base_config
                client = AsyncHTTPClient(config, base_url, auth_headers)
                self._clients[client_key] = client
            
            return self._clients[client_key]
    
    async def close_all(self) -> None:
        """Close all clients in the pool"""
        close_tasks = [client.close() for client in self._clients.values()]
        await asyncio.gather(*close_tasks, return_exceptions=True)
        self._clients.clear()
    
    async def __aenter__(self):
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close_all()


# Global async client pool
_global_pool: Optional[AsyncConnectionPool] = None


def get_async_pool(max_connections: int = 100) -> AsyncConnectionPool:
    """Get global async connection pool"""
    global _global_pool
    if _global_pool is None:
        _global_pool = AsyncConnectionPool(max_connections)
    return _global_pool


async def create_async_client(
    base_url: str,
    auth_headers: Optional[Dict[str, str]] = None,
    config: Optional[AsyncClientConfig] = None
) -> AsyncHTTPClient:
    """Create async HTTP client"""
    pool = get_async_pool()
    return await pool.get_client(base_url, auth_headers, config)


# Context manager for async operations
class AsyncConnectorContext:
    """Context manager for async connector operations"""
    
    def __init__(self, client: AsyncHTTPClient):
        self.client = client
    
    async def __aenter__(self) -> AsyncHTTPClient:
        return self.client
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.client.close()
