"""
uniWeather Cloud API Client
"""

import httpx
from typing import List, Optional, Union
from datetime import datetime, date
from .models import Device, DataQueryResponse, DeviceInfo
from .exceptions import (
    UniWeatherError,
    AuthenticationError,
    NotFoundError,
    APIError
)


class uniWeatherCloud:
    """
    Async client for uniWeather Cloud API
    
    Example:
        client = uniWeatherCloud(base_url="https://api.uniweather.io")
        await client.connect(token="your-api-key")
        
        devices = await client.my_devices()
        data = await client.data(devices[0], from_date="2025-05-05", to_date="2025-05-07")
    """
    
    def __init__(self, base_url: str = "https://api.uniweather.io"):
        """
        Initialize uniWeather client
        
        Args:
            base_url: Base URL of the uniWeather API server
        """
        self.base_url = base_url.rstrip('/')
        self._token: Optional[str] = None
        self._client: Optional[httpx.AsyncClient] = None
        self._temp_cert_bundle: Optional[str] = None
    
    def _create_combined_cert_bundle(self):
        """
        Create a combined certificate bundle including certifi certs and server-specific intermediate certs.
        This handles servers with incomplete certificate chains.
        Returns path to combined bundle or None if creation fails.
        """
        import tempfile
        import os
        
        try:
            import certifi
            # Create a temporary combined bundle
            combined_fd, combined_path = tempfile.mkstemp(suffix='.pem', prefix='uniweather_certs_')
            
            # Copy certifi bundle
            with open(certifi.where(), 'rb') as src:
                os.write(combined_fd, src.read())
            
            # Add our bundled intermediate certificate for api.uniweather.io
            intermediate_cert_path = os.path.join(os.path.dirname(__file__), 'sectigo_intermediate.pem')
            if os.path.exists(intermediate_cert_path):
                os.write(combined_fd, b'\n')
                with open(intermediate_cert_path, 'rb') as src:
                    os.write(combined_fd, src.read())
            
            os.close(combined_fd)
            return combined_path
        except Exception:
            return None
    
    def _create_ssl_context(self):
        """
        Create SSL context with multiple fallback strategies for robust certificate verification.
        Handles common SSL issues on macOS and other platforms automatically.
        Also handles incomplete certificate chains from servers.
        """
        import ssl
        import sys
        import os
        
        # Try multiple SSL strategies in order of preference
        strategies = []
        
        # Strategy 1: Use combined bundle (certifi + intermediate certs)
        # This handles servers with incomplete certificate chains (like api.uniweather.io)
        combined_bundle = self._create_combined_cert_bundle()
        if combined_bundle:
            try:
                ctx = ssl.create_default_context(cafile=combined_bundle)
                strategies.append(('combined_bundle', ctx, combined_bundle))
            except Exception:
                pass
        
        # Strategy 2: Use certifi alone
        try:
            import certifi
            ca_bundle = certifi.where()
            if os.path.exists(ca_bundle):
                strategies.append(('certifi', ssl.create_default_context(cafile=ca_bundle), None))
        except Exception:
            pass
        
        # Strategy 3: Use httpx's default (let httpx handle it)
        strategies.append(('httpx_default', True, None))
        
        # Strategy 4: Try macOS Python installer certificates
        if sys.platform == 'darwin':
            # Check for OpenSSL default cert file in common Homebrew locations
            homebrew_certs = [
                '/opt/homebrew/etc/openssl@3/cert.pem',
                '/opt/homebrew/etc/openssl@1.1/cert.pem',
                '/usr/local/etc/openssl/cert.pem',
                '/etc/ssl/cert.pem',
            ]
            
            for cert_path in homebrew_certs:
                if os.path.exists(cert_path):
                    try:
                        ctx = ssl.create_default_context(cafile=cert_path)
                        strategies.append((f'homebrew:{cert_path}', ctx, None))
                    except Exception:
                        pass
        
        # Strategy 5: Use system default certificates
        try:
            strategies.append(('system_default', ssl.create_default_context(), None))
        except Exception:
            pass
        
        return strategies
    
    async def connect(self, token: str) -> 'uniWeatherCloud':
        """
        Connect to the API with authentication token
        
        Args:
            token: API authentication token
            
        Returns:
            Self for method chaining
            
        Raises:
            AuthenticationError: If token is invalid
        """
        self._token = token
        
        # Try multiple SSL strategies until one works
        ssl_strategies = self._create_ssl_context()
        last_error = None
        temp_files_to_cleanup = []
        
        for strategy_name, ssl_context, cleanup_path in ssl_strategies:
            if cleanup_path:
                temp_files_to_cleanup.append(cleanup_path)
                
            try:
                self._client = httpx.AsyncClient(
                    base_url=self.base_url,
                    headers={
                        "Authorization": f"Bearer {token}",
                        "User-Agent": "uniWeather-Python-Client/0.1.1"
                    },
                    timeout=30.0,
                    verify=ssl_context
                )
                
                # Test connection
                response = await self._client.get("/devices")
                
                if response.status_code == 401:
                    raise AuthenticationError("Invalid API token") from None
                elif response.status_code == 403:
                    raise AuthenticationError("Access forbidden - check your token") from None
                
                # Connection successful - cleanup temp files from strategies we didn't use
                import os
                for temp_file in temp_files_to_cleanup:
                    if temp_file and temp_file != cleanup_path:  # Keep the one we're using
                        try:
                            os.unlink(temp_file)
                        except Exception:
                            pass
                
                # Store the temp file path for cleanup on close if needed
                self._temp_cert_bundle = cleanup_path
                return self
                
            except (UniWeatherError, AuthenticationError, NotFoundError, APIError):
                # These are API-level errors, not SSL errors - re-raise immediately
                if self._client:
                    await self._client.aclose()
                # Cleanup all temp files
                import os
                for temp_file in temp_files_to_cleanup:
                    try:
                        if temp_file:
                            os.unlink(temp_file)
                    except Exception:
                        pass
                raise
            except httpx.ConnectError as e:
                last_error = e
                error_str = str(e)
                
                # Clean up failed client
                if self._client:
                    await self._client.aclose()
                    self._client = None
                
                # If this is an SSL error, try the next strategy
                if "CERTIFICATE_VERIFY_FAILED" in error_str or "SSL" in error_str:
                    continue
                
                # For non-SSL connection errors, fail immediately and cleanup
                import os
                for temp_file in temp_files_to_cleanup:
                    try:
                        if temp_file:
                            os.unlink(temp_file)
                    except Exception:
                        pass
                        
                if "Name or service not known" in error_str or "nodename nor servname provided" in error_str:
                    raise UniWeatherError(
                        f"Could not resolve hostname: {self.base_url}. Check your internet connection."
                    ) from None
                elif "Connection refused" in error_str:
                    raise UniWeatherError(
                        f"Connection refused by {self.base_url}. Is the server running?"
                    ) from None
                else:
                    raise UniWeatherError(
                        f"Failed to connect to {self.base_url}: {error_str}"
                    ) from None
                    
            except httpx.TimeoutException:
                if self._client:
                    await self._client.aclose()
                    self._client = None
                # Cleanup all temp files
                import os
                for temp_file in temp_files_to_cleanup:
                    try:
                        if temp_file:
                            os.unlink(temp_file)
                    except Exception:
                        pass
                raise UniWeatherError(
                    f"Connection to {self.base_url} timed out. Check your internet connection."
                ) from None
            except Exception as e:
                last_error = e
                if self._client:
                    await self._client.aclose()
                    self._client = None
                # Try next strategy
                continue
        
        # All strategies failed - cleanup temp files
        import os
        for temp_file in temp_files_to_cleanup:
            try:
                if temp_file:
                    os.unlink(temp_file)
            except Exception:
                pass
        
        error_msg = f"SSL certificate verification failed with all strategies. "
        if last_error:
            error_msg += f"Last error: {last_error}. "
        error_msg += "Try: pip install --upgrade certifi"
        raise UniWeatherError(error_msg) from None
    
    async def close(self):
        """Close the HTTP client connection and cleanup resources"""
        if self._client:
            await self._client.aclose()
            self._client = None
        
        # Cleanup temp certificate bundle if it exists
        if self._temp_cert_bundle:
            try:
                import os
                os.unlink(self._temp_cert_bundle)
            except Exception:
                pass
            self._temp_cert_bundle = None
    
    async def __aenter__(self):
        """Support async context manager"""
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Close connection on context exit"""
        await self.close()
    
    def _ensure_connected(self):
        """Ensure client is connected"""
        if not self._client or not self._token:
            raise UniWeatherError("Not connected. Call connect() first.")
    
    def _handle_request_error(self, e: Exception, context: str = "request") -> None:
        """Handle HTTP request errors with clean messages"""
        if isinstance(e, httpx.TimeoutException):
            raise APIError(f"Request timed out during {context}") from None
        elif isinstance(e, httpx.ConnectError):
            raise APIError(f"Connection failed during {context}") from None
        elif isinstance(e, httpx.HTTPStatusError):
            raise APIError(f"HTTP {e.response.status_code} error during {context}") from None
        else:
            raise APIError(f"Error during {context}: {type(e).__name__}") from None
    
    async def my_devices(self, all: bool = False) -> List[Device]:
        """
        Get list of devices accessible to the user
        
        Args:
            all: If True, show all assigned devices (including inactive)
            
        Returns:
            List of Device objects
        """
        self._ensure_connected()
        
        try:
            params = {"all": "true"} if all else {}
            response = await self._client.get("/devices", params=params)
            
            if response.status_code != 200:
                raise APIError(f"Failed to fetch devices: {response.status_code}")
        except Exception as e:
            if isinstance(e, (UniWeatherError, AuthenticationError, NotFoundError, APIError)):
                raise
            self._handle_request_error(e, "fetching devices")
        
        data = response.json()
        device_ids = data.get('devices', [])
        
        devices = []
        for device_id in device_ids:
            device = Device(device_id=device_id)
            devices.append(device)
        
        return devices
    
    async def get_device_info(self, device: Union[str, Device]) -> DeviceInfo:
        """
        Get detailed information about a device (requires admin access)
        
        Args:
            device: Device ID string or Device object
            
        Returns:
            DeviceInfo object
        """
        self._ensure_connected()
        
        device_id = device if isinstance(device, str) else device.device_id
        
        response = await self._client.get(f"/admin/devices/{device_id}")
        
        if response.status_code == 404:
            raise NotFoundError(f"Device '{device_id}' not found")
        elif response.status_code == 403:
            raise AuthenticationError("Admin access required")
        elif response.status_code != 200:
            raise APIError(f"Failed to fetch device info: {response.status_code}")
        
        return DeviceInfo.from_dict(response.json())
    
    async def get_channels(self, device: Union[str, Device]) -> List[str]:
        """
        Get list of available channels for a device
        
        Args:
            device: Device ID string or Device object
            
        Returns:
            List of channel names
        """
        self._ensure_connected()
        
        device_id = device if isinstance(device, str) else device.device_id
        
        try:
            response = await self._client.get(f"/devices/{device_id}/channels")
            
            if response.status_code == 404:
                raise NotFoundError(f"Device '{device_id}' not found")
            elif response.status_code == 403:
                raise AuthenticationError("Access forbidden to this device")
            elif response.status_code != 200:
                raise APIError(f"Failed to fetch channels: {response.status_code}")
        except Exception as e:
            if isinstance(e, (UniWeatherError, AuthenticationError, NotFoundError, APIError)):
                raise
            self._handle_request_error(e, f"fetching channels for device {device_id}")
        
        data = response.json()
        return data.get('channels', [])
    
    def _parse_date(self, date_input: Union[str, date, datetime, float, None]) -> Optional[float]:
        """Convert various date formats to Unix timestamp"""
        if date_input is None:
            return None
        
        if isinstance(date_input, (int, float)):
            return float(date_input)
        
        if isinstance(date_input, str):
            # Try parsing ISO format
            try:
                dt = datetime.fromisoformat(date_input)
                return dt.timestamp()
            except ValueError:
                pass
            
            # Try parsing common date formats
            for fmt in ['%Y-%m-%d', '%Y/%m/%d', '%d.%m.%Y']:
                try:
                    dt = datetime.strptime(date_input, fmt)
                    return dt.timestamp()
                except ValueError:
                    continue
            
            raise ValueError(f"Could not parse date: {date_input}")
        
        if isinstance(date_input, datetime):
            return date_input.timestamp()
        
        if isinstance(date_input, date):
            return datetime.combine(date_input, datetime.min.time()).timestamp()
        
        raise ValueError(f"Unsupported date type: {type(date_input)}")
    
    async def data(
        self,
        device: Union[str, Device],
        channels: Union[str, List[str], None] = None,
        from_date: Union[str, date, datetime, float, None] = None,
        to_date: Union[str, date, datetime, float, None] = None,
        format: str = "dataframe"
    ) -> Union[DataQueryResponse, str]:
        """
        Query data from a device
        
        Args:
            device: Device ID string or Device object
            channels: Channel name, list of channels, or "all" for all channels
            from_date: Start date (string, datetime, or Unix timestamp)
            to_date: End date (string, datetime, or Unix timestamp)
            format: Output format - "dataframe" (default, uses CSV endpoint), "csv" (raw CSV string)
            
        Returns:
            DataQueryResponse object (wrapping parsed CSV data) or CSV string (for csv format)
            
        Example:
            # Get all data for May 2025
            data = await client.data(
                device="device123",
                channels="all",
                from_date="2025-05-01",
                to_date="2025-05-31"
            )
            
            # Convert to pandas DataFrame
            df = data.to_dataframe()
            
            # Or get raw CSV string
            csv = await client.data(device="device123", format="csv")
        """
        self._ensure_connected()
        
        device_id = device if isinstance(device, str) else device.device_id
        
        # Build query parameters - always request CSV format from API
        params = {'format': 'csv'}
        
        if from_date is not None:
            params['start'] = self._parse_date(from_date)
        
        if to_date is not None:
            params['end'] = self._parse_date(to_date)
        
        if channels is not None:
            if channels == "all":
                # Don't pass channels parameter to get all
                pass
            elif isinstance(channels, str):
                params['channels'] = channels
            elif isinstance(channels, list):
                params['channels'] = ','.join(channels)
        
        try:
            response = await self._client.get(f"/data/{device_id}", params=params)
            
            if response.status_code == 404:
                raise NotFoundError(f"Device '{device_id}' not found")
            elif response.status_code == 403:
                raise AuthenticationError("Access forbidden to this device")
            elif response.status_code != 200:
                raise APIError(f"Failed to fetch data: {response.status_code}")
        except Exception as e:
            if isinstance(e, (UniWeatherError, AuthenticationError, NotFoundError, APIError)):
                raise
            self._handle_request_error(e, f"fetching data for device {device_id}")
        
        csv_text = response.text
        
        # If raw CSV requested, return it directly
        if format == "csv":
            return csv_text
        
        # Otherwise, parse CSV and wrap in DataQueryResponse for backward compatibility
        return DataQueryResponse.from_csv(csv_text, device_id)
    
    async def download_csv(
        self,
        device: Union[str, Device],
        filename: str,
        channels: Union[str, List[str], None] = None,
        from_date: Union[str, date, datetime, float, None] = None,
        to_date: Union[str, date, datetime, float, None] = None
    ):
        """
        Download data as CSV file
        
        Args:
            device: Device ID string or Device object
            filename: Output filename for CSV
            channels: Channel name, list of channels, or "all" for all channels
            from_date: Start date
            to_date: End date
        """
        csv_data = await self.data(
            device=device,
            channels=channels,
            from_date=from_date,
            to_date=to_date,
            format="csv"
        )
        
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(csv_data)
    
    async def list_all_devices(self) -> List[DeviceInfo]:
        """
        List all devices in the system (requires admin access)
        
        Returns:
            List of DeviceInfo objects
        """
        self._ensure_connected()
        
        response = await self._client.get("/admin/devices")
        
        if response.status_code == 403:
            raise AuthenticationError("Admin access required")
        elif response.status_code != 200:
            raise APIError(f"Failed to fetch devices: {response.status_code}")
        
        devices_data = response.json()
        return [DeviceInfo.from_dict(d) for d in devices_data]


class uniWeatherCloudSync:
    """
    Synchronous wrapper for uniWeatherCloud
    
    Example:
        with uniWeatherCloudSync() as client:
            client.connect(token="your-api-key")
            devices = client.my_devices()
            data = client.data(devices[0])
    """
    
    def __init__(self, base_url: str = "https://api.uniweather.io"):
        self._async_client = uniWeatherCloud(base_url=base_url)
        self._loop = None
    
    def _run_async(self, coro):
        """Run async coroutine synchronously"""
        import asyncio
        try:
            loop = asyncio.get_event_loop()
        except RuntimeError:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
        return loop.run_until_complete(coro)
    
    def connect(self, token: str) -> 'uniWeatherCloudSync':
        """Connect to the API"""
        self._run_async(self._async_client.connect(token))
        return self
    
    def close(self):
        """Close connection"""
        self._run_async(self._async_client.close())
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
    
    def my_devices(self, all: bool = False) -> List[Device]:
        """Get list of accessible devices"""
        return self._run_async(self._async_client.my_devices(all=all))
    
    def get_channels(self, device: Union[str, Device]) -> List[str]:
        """Get channels for a device"""
        return self._run_async(self._async_client.get_channels(device))
    
    def data(
        self,
        device: Union[str, Device],
        channels: Union[str, List[str], None] = None,
        from_date: Union[str, date, datetime, float, None] = None,
        to_date: Union[str, date, datetime, float, None] = None,
        format: str = "json"
    ) -> Union[DataQueryResponse, str]:
        """Query data from device"""
        return self._run_async(
            self._async_client.data(device, channels, from_date, to_date, format)
        )
    
    def download_csv(
        self,
        device: Union[str, Device],
        filename: str,
        channels: Union[str, List[str], None] = None,
        from_date: Union[str, date, datetime, float, None] = None,
        to_date: Union[str, date, datetime, float, None] = None
    ):
        """Download data as CSV"""
        return self._run_async(
            self._async_client.download_csv(device, filename, channels, from_date, to_date)
        )

