"""
FletX - Advanced Async/Sync HTTP Client for API requests
"""
import json
import asyncio
import logging
import threading
from pathlib import Path
from typing import (
    Any, Dict, Optional, Union, AsyncIterator, List, Callable, BinaryIO, Iterator
)
from dataclasses import dataclass, field
from time import monotonic
from functools import wraps
import aiohttp
import requests
from aiohttp import ClientTimeout, ClientResponse, ClientSession
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from fletx.utils.exceptions import (
    NetworkError, RateLimitError, APIError
)

logger = logging.getLogger("fletx.http")


####
##      FILE INFO
#####
@dataclass
class FileInfo:
    """Information about an uploaded/downloaded file"""

    filename: str
    size: int
    content_type: str
    field_name: Optional[str] = None


####
##      HTTP RESPONSE
#####
@dataclass
class HTTPResponse:
    """Structured HTTP response container"""

    status: int
    headers: Dict[str, str]
    data: Union[Dict[str, Any], str, bytes]
    elapsed: float
    url: str
    files: List[FileInfo] = field(default_factory=list)
    cookies: Dict[str, str] = field(default_factory=dict)
    
    @property
    def ok(self) -> bool:
        """Check if response is successful (2xx status)"""

        return 200 <= self.status < 300
    
    @property
    def is_json(self) -> bool:
        """Check if response contains JSON data"""

        return isinstance(self.data, (dict, list))
    
    def json(self) -> Union[Dict[str, Any], List[Any]]:
        """Get JSON data from response"""

        if not self.is_json:
            raise ValueError("Response does not contain JSON data")
        return self.data
    
    def text(self) -> str:
        """Get text data from response"""

        # Bytes data
        if isinstance(self.data, bytes):
            return self.data.decode('utf-8')
        
        # Str 
        elif isinstance(self.data, str):
            return self.data
        
        # Json data
        elif isinstance(self.data, dict) and 'raw_response' in self.data:
            return self.data['raw_response']
        return str(self.data)


####
##      UPLOAD PROGRESS
#####
@dataclass
class UploadProgress:
    """Progress information for file uploads"""

    uploaded: int
    total: int
    percentage: float
    speed: float  # bytes per second
    filename: str


####
##      DOWNLOAD PROGRESS
#####
@dataclass
class DownloadProgress:
    """Progress information for file downloads"""

    downloaded: int
    total: int
    percentage: float
    speed: float  # bytes per second
    filename: str


####
##      FORM DATA CLASS
#####
class FormData(aiohttp.FormData):
    """Enhanced FormData with file support"""
    
    def add_file(
        self, 
        field_name: str, 
        file_path: Union[str, Path], 
        filename: Optional[str] = None, 
        content_type: Optional[str] = None
    ):
        """Add a file to the form data"""

        file_path = Path(file_path)
        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")
        
        filename = filename or file_path.name
        with open(file_path, 'rb') as f:
            content = f.read()
        
        self.add_field(
            field_name, 
            content, 
            filename = filename, 
            content_type = content_type
        )


####
##      MIDDLEWARE SYSTEM
#####
class Middleware:
    """Base middleware class"""
    
    async def before_request(
        self, 
        method: str, 
        url: str, **kwargs
    ) -> Dict[str, Any]:
        """Called before each request"""

        return kwargs
    
    async def after_response(
        self, 
        response: HTTPResponse
    ) -> HTTPResponse:
        """Called after each response"""

        return response
    
    async def on_error(
        self, 
        error: Exception
    ) -> Optional[Exception]:
        """Called when an error occurs. Return None to suppress the error."""

        return error


####
##      AUTHENTICATION MIDDLEWARE
#####
class AuthMiddleware(Middleware):
    """Authentication middleware"""
    
    def __init__(self, token: str, auth_type: str = "Bearer"):
        self.token = token
        self.auth_type = auth_type
    
    async def before_request(
        self, 
        method: str, 
        url: str, **kwargs
    ) -> Dict[str, Any]:
        """Authentication middleware."""

        headers = kwargs.get('headers', {})
        headers['Authorization'] = f"{self.auth_type} {self.token}"
        kwargs['headers'] = headers

        return kwargs


####
##      LOGGING MIDDLEWARE
#####
class LoggingMiddleware(Middleware):
    """Request/Response logging middleware"""
    
    def __init__(self, log_level: int = logging.INFO):
        self.log_level = log_level
    
    async def before_request(
        self, 
        method: str, 
        url: str, **kwargs
    ) -> Dict[str, Any]:
        
        logger.log(self.log_level, f"[REQUEST] {method} {url}")
        return kwargs
    
    async def after_response(
        self, 
        response: HTTPResponse
    ) -> HTTPResponse:
        
        logger.log(
            self.log_level, 
            f"[RESPONSE] {response.status} {response.url} ({response.elapsed:.2f}s)"
        )
        return response


####
##      MAIN HTTP CLIENT CLASS
#####
class HTTPClient:
    """Advanced asynchronous/synchronous HTTP client with enhanced features"""
    
    def __init__(
        self,
        base_url: str = "",
        default_headers: Optional[Dict[str, str]] = None,
        timeout: int = 30,
        max_retries: int = 3,
        retry_delay: float = 1.0,
        debug: bool = False,
        proxy: Optional[str] = None,
        pool_size: int = 100,
        verify_ssl: bool = True,
        follow_redirects: bool = True,
        max_redirects: int = 10,
        cookies: Optional[Dict[str, str]] = None,
        sync_mode: bool = False
    ):
        """
        Initialize the HTTP client with advanced configuration.
        
        Args:
            base_url: Base API endpoint
            default_headers: Default headers for all requests
            timeout: Request timeout in seconds
            max_retries: Maximum retry attempts for failed requests
            retry_delay: Initial delay between retries in seconds
            debug: Enable debug logging
            proxy: Proxy server URL
            pool_size: Connection pool size
            verify_ssl: Verify SSL certificates
            follow_redirects: Follow HTTP redirects
            max_redirects: Maximum number of redirects to follow
            cookies: Default cookies
            sync_mode: Use synchronous mode by default
        """

        self.base_url = base_url.rstrip('/') if base_url else ""
        self.default_headers = default_headers or {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "User-Agent": "FletX-HTTPClient/1.0"
        }
        self.timeout = timeout
        self.max_retries = max_retries
        self.retry_delay = retry_delay
        self.debug = debug
        self.proxy = proxy
        self.pool_size = pool_size
        self.verify_ssl = verify_ssl
        self.follow_redirects = follow_redirects
        self.max_redirects = max_redirects
        self.default_cookies = cookies or {}
        self.sync_mode = sync_mode
        
        # Async components
        self._session: Optional[ClientSession] = None
        self.connector: Optional[aiohttp.TCPConnector] = None
        
        # Sync components
        self._sync_session: Optional[requests.Session] = None
        
        # Middleware
        self.middlewares: List[Middleware] = []
        
        # Progress callbacks
        self.upload_progress_callback: Optional[Callable[[UploadProgress], None]] = None
        self.download_progress_callback: Optional[Callable[[DownloadProgress], None]] = None
        
        # Rate limiting
        self.rate_limit_per_second: Optional[float] = None
        self._last_request_time: float = 0
        self._request_lock = asyncio.Lock()
        self._sync_request_lock = threading.Lock()

    def add_middleware(self, middleware: Middleware) -> 'HTTPClient':
        """Add middleware to the client"""

        self.middlewares.append(middleware)
        return self

    def set_auth(self, token: str, auth_type: str = "Bearer") -> 'HTTPClient':
        """Set authentication token"""

        self.add_middleware(AuthMiddleware(token, auth_type))
        return self

    def set_rate_limit(self, requests_per_second: float) -> 'HTTPClient':
        """Set rate limiting"""

        self.rate_limit_per_second = requests_per_second
        return self

    def set_upload_progress_callback(
        self, 
        callback: Callable[[UploadProgress], None]
    ) -> 'HTTPClient':
        """Set upload progress callback"""

        self.upload_progress_callback = callback
        return self

    def set_download_progress_callback(
        self, 
        callback: Callable[[DownloadProgress], None]
    ) -> 'HTTPClient':
        """Set download progress callback"""

        self.download_progress_callback = callback
        return self

    async def __aenter__(self) -> 'HTTPClient':

        await self.start_session()
        return self

    async def __aexit__(self, *exc) -> None:

        await self.close_session()

    def __enter__(self) -> 'HTTPClient':

        self.start_sync_session()
        return self

    def __exit__(self, *exc) -> None:

        self.close_sync_session()

    async def start_session(self) -> None:
        """Initialize the async client session"""

        if self._session is None or self._session.closed:
            if self.connector is None:
                self.connector = aiohttp.TCPConnector(
                    limit = self.pool_size,
                    force_close = False,
                    enable_cleanup_closed = True,
                    verify_ssl = self.verify_ssl
                )
            
            timeout = ClientTimeout(total=self.timeout)
            self._session = ClientSession(
                connector = self.connector,
                timeout = timeout,
                headers = self.default_headers,
                cookies = self.default_cookies
            )

    def start_sync_session(self) -> None:
        """Initialize the sync client session"""

        if self._sync_session is None:
            self._sync_session = requests.Session()
            self._sync_session.headers.update(self.default_headers)
            self._sync_session.cookies.update(self.default_cookies)
            
            # Configure retries for sync session
            retry_strategy = Retry(
                total = self.max_retries,
                backoff_factor = self.retry_delay,
                status_forcelist = [429, 500, 502, 503, 504]
            )
            adapter = HTTPAdapter(max_retries=retry_strategy, pool_maxsize=self.pool_size)
            self._sync_session.mount("http://", adapter)
            self._sync_session.mount("https://", adapter)

    async def close_session(self) -> None:
        """Close the async client session"""

        if self._session and not self._session.closed:
            await self._session.close()
            self._session = None

    def close_sync_session(self) -> None:
        """Close the sync client session"""

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

    async def _apply_middlewares_before(
        self, 
        method: str, 
        url: str, 
        **kwargs
    ) -> Dict[str, Any]:
        """Apply middlewares before request"""

        for middleware in self.middlewares:
            kwargs = await middleware.before_request(method, url, **kwargs)
        return kwargs

    async def _apply_middlewares_after(
        self, 
        response: HTTPResponse
    ) -> HTTPResponse:
        """Apply middlewares after response"""

        for middleware in self.middlewares:
            response = await middleware.after_response(response)
        return response

    async def _apply_middlewares_error(
        self, error: Exception
    ) -> Optional[Exception]:
        """Apply middlewares on error"""

        for middleware in self.middlewares:
            error = await middleware.on_error(error)
            if error is None:
                break
        return error

    async def _rate_limit_check(self) -> None:
        """Check and enforce rate limiting"""

        if self.rate_limit_per_second is None:
            return
        
        async with self._request_lock:
            current_time = monotonic()
            time_since_last = current_time - self._last_request_time
            min_interval = 1.0 / self.rate_limit_per_second
            
            if time_since_last < min_interval:
                await asyncio.sleep(min_interval - time_since_last)
            
            self._last_request_time = monotonic()

    def _sync_rate_limit_check(self) -> None:
        """Synchronous rate limiting check"""

        if self.rate_limit_per_second is None:
            return
        
        with self._sync_request_lock:
            import time
            current_time = time.monotonic()
            time_since_last = current_time - self._last_request_time
            min_interval = 1.0 / self.rate_limit_per_second
            
            if time_since_last < min_interval:
                time.sleep(min_interval - time_since_last)
            
            self._last_request_time = time.monotonic()

    def _build_url(self, endpoint: str) -> str:
        """Build full URL from endpoint"""

        if endpoint.startswith(('http://', 'https://')):
            return endpoint
        
        if self.base_url:
            return f"{self.base_url}/{endpoint.lstrip('/')}"
        return endpoint

    async def _process_files_async(
        self, 
        files: Dict[str, Any], 
        data: Optional[Dict] = None, 
        json_data: Optional[Dict] = None
    ) -> aiohttp.FormData:
        """Process files for async upload"""

        form_data = aiohttp.FormData()
        
        # Add regular form fields
        if data and isinstance(data, dict):
            for key, value in data.items():
                form_data.add_field(key, str(value))
        
        # Add JSON data as form field if needed
        if json_data:
            form_data.add_field('json_payload', json.dumps(json_data))
        
        # Add files
        for field_name, file_info in files.items():

            # Provided file is a path
            if isinstance(file_info, (str, Path)):
                # File path
                file_path = Path(file_info)
                if not file_path.exists():
                    raise FileNotFoundError(f"File not found: {file_path}")
                
                # Read file content
                with open(file_path, 'rb') as f:
                    content = f.read()
                
                form_data.add_field(
                    field_name,
                    content,
                    filename = file_path.name
                )
            
            # Content file as tuple
            elif isinstance(file_info, tuple):

                if len(file_info) == 2:
                    filename, content = file_info
                    form_data.add_field(
                        field_name, 
                        content, 
                        filename = filename
                    )

                elif len(file_info) == 3:
                    filename, content, content_type = file_info
                    form_data.add_field(
                        field_name, 
                        content, 
                        filename = filename, 
                        content_type = content_type
                    )
            
            # File-like object
            elif hasattr(file_info, 'read'):
                content = file_info.read()
                filename = getattr(file_info, 'name', f'{field_name}_file')
                form_data.add_field(field_name, content, filename = filename)

            else:
                form_data.add_field(field_name, file_info)
        
        return form_data

    def _process_files_sync(
        self, files: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Process files for sync upload"""

        processed_files = {}
        
        for field_name, file_info in files.items():

            # File path
            if isinstance(file_info, (str, Path)):
                file_path = Path(file_info)
                if not file_path.exists():
                    raise FileNotFoundError(f"File not found: {file_path}")
                
                processed_files[field_name] = open(file_path, 'rb')
            
            # File Tuple (filename, content, [content_type])
            elif isinstance(file_info, tuple):
                if len(file_info) == 2:
                    filename, content = file_info

                    if isinstance(content, (str, bytes)):
                        processed_files[field_name] = (filename, content)

                elif len(file_info) == 3:
                    filename, content, content_type = file_info
                    processed_files[field_name] = (filename, content, content_type)

            # File-like object
            elif hasattr(file_info, 'read'):
                processed_files[field_name] = file_info

            else:
                processed_files[field_name] = file_info
        
        return processed_files

    async def _request_async(
        self,
        method: str,
        endpoint: str,
        headers: Optional[Dict[str, str]] = None,
        params: Optional[Dict[str, Any]] = None,
        data: Optional[Union[Dict[str, Any], str]] = None,
        json_data: Optional[Union[Dict[str, Any], List[Any]]] = None,
        files: Optional[Dict[str, Any]] = None,
        **kwargs
    ) -> HTTPResponse:
        """Execute async HTTP request"""

        url = self._build_url(endpoint)
        merged_headers = {**self.default_headers, **(headers or {})}
        
        # Apply middlewares
        request_kwargs = await self._apply_middlewares_before(
            method, url, headers=merged_headers, params=params, 
            data=data, json=json_data, files=files, **kwargs
        )
        
        # Rate limiting
        await self._rate_limit_check()
        
        # Handle file uploads
        if files:
            data = await self._process_files_async(files, data, json_data)
            merged_headers.pop('Content-Type', None)
            json_data = None
        
        start_time = monotonic()
        last_exception = None
        
        for attempt in range(self.max_retries + 1):
            try:
                if not self._session or self._session.closed:
                    await self.start_session()

                async with self._session.request(
                    metho = method,
                    url = url,
                    headers = merged_headers,
                    params = params,
                    data = data,
                    json = json_data,
                    proxy = self.proxy,
                    allow_redirects = self.follow_redirects,
                    max_redirects = self.max_redirects,
                    **kwargs
                ) as response:
                    elapsed = monotonic() - start_time
                    
                    # Process response content
                    content_type = response.headers.get('Content-Type', '')

                    # Json data
                    if 'application/json' in content_type:
                        response_data = await response.json()
                    
                    # Text data
                    elif 'text/' in content_type:
                        response_data = await response.text()
                    
                    # Byte data (probably a file)
                    else:
                        response_data = await response.read()

                    if self.debug:
                        logger.debug(f"Response ({response.status}) in {elapsed:.2f}s")

                    # Handle error responses
                    if response.status == 429:

                        error = RateLimitError(
                            message="Rate limit exceeded",
                            status_code=response.status,
                            raw_response=response_data,
                            headers=dict(response.headers)
                        )
                        error = await self._apply_middlewares_error(error)
                        if error:
                            raise error
                    
                    http_response = HTTPResponse(
                        status = response.status,
                        headers = dict(response.headers),
                        data = response_data,
                        elapsed = elapsed,
                        url = str(response.url),
                        cookies = dict(response.cookies)
                    )
                    
                    # Apply after middlewares
                    http_response = await self._apply_middlewares_after(http_response)
                    
                    return http_response

            except Exception as e:
                last_exception = e
                error = await self._apply_middlewares_error(e)
                
                if error is None:
                    # Error was handled by middleware
                    continue
                
                if attempt == self.max_retries:
                    if isinstance(e, (aiohttp.ClientError, aiohttp.ClientPayloadError)):
                        raise NetworkError(
                            message=f"Network error: {str(e)}",
                            original_exception=e
                        ) from e
                    raise e
                
                retry_wait = self.retry_delay * (2 ** attempt)
                if self.debug:
                    logger.warning(
                        f"Attempt {attempt + 1} failed. Retrying in {retry_wait:.1f}s. Error: {str(e)}"
                    )
                await asyncio.sleep(retry_wait)

    def _request_sync(
        self,
        method: str,
        endpoint: str,
        headers: Optional[Dict[str, str]] = None,
        params: Optional[Dict[str, Any]] = None,
        data: Optional[Union[Dict[str, Any], str]] = None,
        json_data: Optional[Union[Dict[str, Any], List[Any]]] = None,
        files: Optional[Dict[str, Any]] = None,
        **kwargs
    ) -> HTTPResponse:
        """Execute sync HTTP request"""

        url = self._build_url(endpoint)
        merged_headers = {**self.default_headers, **(headers or {})}
        
        # Rate limiting
        self._sync_rate_limit_check()
        
        # Handle file uploads
        if files:
            files = self._process_files_sync(files)
            merged_headers.pop('Content-Type', None)
        
        if not self._sync_session:
            self.start_sync_session()
        
        start_time = monotonic()
        
        try:
            response = self._sync_session.request(
                method = method,
                url = url,
                headers = merged_headers,
                params = params,
                data = data,
                json = json_data,
                files = files,
                proxies = {'http': self.proxy, 'https': self.proxy} if self.proxy else None,
                verify = self.verify_ssl,
                allow_redirects = self.follow_redirects,
                timeout = self.timeout,
                **kwargs
            )
            
            elapsed = monotonic() - start_time
            
            # Process response content
            content_type = response.headers.get('Content-Type', '')

            # Json data
            if 'application/json' in content_type:
                try:
                    response_data = response.json()
                except json.JSONDecodeError:
                    response_data = {"raw_response": response.text}

            # Text data
            elif 'text/' in content_type:
                response_data = response.text

            else:
                response_data = response.content
            
            return HTTPResponse(
                status = response.status_code,
                headers = dict(response.headers),
                data = response_data,
                elapsed = elapsed,
                url = response.url,
                cookies = dict(response.cookies)
            )
            
        except requests.RequestException as e:
            raise NetworkError(
                message = f"Network error: {str(e)}",
                original_exception = e
            ) from e
        
        finally:
            # Close file handles if we opened them
            if files:
                for file_obj in files.values():
                    if hasattr(file_obj, 'close'):
                        file_obj.close()

    # Unified request method that chooses async or sync
    def request(
        self,
        method: str,
        endpoint: str,
        headers: Optional[Dict[str, str]] = None,
        params: Optional[Dict[str, Any]] = None,
        data: Optional[Union[Dict[str, Any], str]] = None,
        json_data: Optional[Union[Dict[str, Any], List[Any]]] = None,
        files: Optional[Dict[str, Any]] = None,
        sync: Optional[bool] = None,
        **kwargs
    ) -> Union[HTTPResponse, asyncio.Future[HTTPResponse]]:
        """
        Universal request method that can work both sync and async
        
        Args:
            sync: If True, force synchronous execution. If False, force async. 
                  If None, use the client's default mode.
        """

        use_sync = sync if sync is not None else self.sync_mode
        
        if use_sync:
            return self._request_sync(
                method, 
                endpoint, 
                headers, 
                params, 
                data, 
                json_data, 
                files, 
                **kwargs
            )
        
        else:
            return self._request_async(
                method, 
                endpoint, 
                headers, 
                params, 
                data, 
                json_data, 
                files, 
                **kwargs
            )

    # Convenience methods
    def get(
        self, 
        endpoint: str, 
        **kwargs
    ) -> Union[HTTPResponse, asyncio.Future[HTTPResponse]]:
        """Perform GET request"""

        return self.request("GET", endpoint, **kwargs)

    def post(
        self, 
        endpoint: str, 
        **kwargs
    ) -> Union[HTTPResponse, asyncio.Future[HTTPResponse]]:
        """Perform POST request"""

        return self.request("POST", endpoint, **kwargs)

    def put(
        self, 
        endpoint: str, 
        **kwargs
    ) -> Union[HTTPResponse, asyncio.Future[HTTPResponse]]:
        """Perform PUT request"""

        return self.request("PUT", endpoint, **kwargs)

    def delete(
        self, 
        endpoint: str, 
        **kwargs
    ) -> Union[HTTPResponse, asyncio.Future[HTTPResponse]]:
        """Perform DELETE request"""

        return self.request("DELETE", endpoint, **kwargs)

    def patch(
        self, 
        endpoint: str, 
        **kwargs
    ) -> Union[HTTPResponse, asyncio.Future[HTTPResponse]]:
        """Perform PATCH request"""

        return self.request("PATCH", endpoint, **kwargs)

    # File-specific methods
    async def upload_file(
        self,
        endpoint: str,
        file_path: Union[str, Path],
        field_name: str = "file",
        additional_data: Optional[Dict[str, Any]] = None,
        **kwargs
    ) -> HTTPResponse:
        """Upload a single file"""

        files = {field_name: file_path}
        return await self._request_async(
            "POST", 
            endpoint, 
            files = files, 
            data = additional_data, 
            **kwargs
        )

    async def download_file(
        self,
        endpoint: str,
        file_path: Union[str, Path],
        **kwargs
    ) -> HTTPResponse:
        """Download a file"""

        response = await self._request_async("GET", endpoint, **kwargs)
        
        if response.ok and isinstance(response.data, bytes):
            file_path = Path(file_path)
            file_path.parent.mkdir(parents=True, exist_ok=True)
            
            with open(file_path, 'wb') as f:
                f.write(response.data)
        
        return response

    async def stream_download(
        self,
        endpoint: str,
        file_path: Union[str, Path],
        chunk_size: int = 8192,
        **kwargs
    ) -> HTTPResponse:
        """Stream download a large file with progress tracking"""

        url = self._build_url(endpoint)
        file_path = Path(file_path)
        file_path.parent.mkdir(parents=True, exist_ok=True)
        
        if not self._session or self._session.closed:
            await self.start_session()
        
        start_time = monotonic()
        downloaded = 0
        
        async with self._session.get(url, **kwargs) as response:
            total_size = int(response.headers.get('Content-Length', 0))
            
            with open(file_path, 'wb') as f:
                async for chunk in response.content.iter_chunked(chunk_size):
                    f.write(chunk)
                    downloaded += len(chunk)
                    
                    if self.download_progress_callback and total_size > 0:
                        elapsed = monotonic() - start_time
                        speed = downloaded / elapsed if elapsed > 0 else 0
                        percentage = (downloaded / total_size) * 100
                        
                        progress = DownloadProgress(
                            downloaded = downloaded,
                            total = total_size,
                            percentage = percentage,
                            speed = speed,
                            filename = file_path.name
                        )
                        self.download_progress_callback(progress)
            
            elapsed = monotonic() - start_time

            return HTTPResponse(
                status = response.status,
                headers = dict(response.headers),
                data = f"Downloaded {downloaded} bytes to {file_path}",
                elapsed = elapsed,
                url = str(response.url)
            )