"""Clore.ai API client implementation."""

import asyncio
import time
from typing import Any, Dict, List, Literal, Optional

import httpx

from clore_ai.exceptions import ERROR_MAP, CloreAPIError
from clore_ai.models import (
    MarketplaceServer,
    MyServer,
    Order,
    ServerConfig,
    SpotMarket,
    Wallet,
)

# Backward-compatible alias
Server = MarketplaceServer


class RateLimiter:
    """Simple rate limiter with exponential backoff."""

    def __init__(self, requests_per_second: float = 1.0, create_order_delay: float = 5.0):
        self.min_interval = 1.0 / requests_per_second
        self.create_order_delay = create_order_delay
        self.last_request = 0.0
        self.last_create_order = 0.0
        self.backoff_until = 0.0

    def wait(self, is_create_order: bool = False) -> None:
        """Block until rate limit allows next request."""
        now = time.time()

        # Handle backoff from rate limit errors
        if now < self.backoff_until:
            time.sleep(self.backoff_until - now)
            now = time.time()

        # Special handling for create_order
        if is_create_order:
            since_last_create = now - self.last_create_order
            if since_last_create < self.create_order_delay:
                time.sleep(self.create_order_delay - since_last_create)
            self.last_create_order = time.time()

        # General rate limiting
        since_last = now - self.last_request
        if since_last < self.min_interval:
            time.sleep(self.min_interval - since_last)

        self.last_request = time.time()

    async def wait_async(self, is_create_order: bool = False) -> None:
        """Async version of wait."""
        now = time.time()

        if now < self.backoff_until:
            await asyncio.sleep(self.backoff_until - now)
            now = time.time()

        if is_create_order:
            since_last_create = now - self.last_create_order
            if since_last_create < self.create_order_delay:
                await asyncio.sleep(self.create_order_delay - since_last_create)
            self.last_create_order = time.time()

        since_last = now - self.last_request
        if since_last < self.min_interval:
            await asyncio.sleep(self.min_interval - since_last)

        self.last_request = time.time()

    def set_backoff(self, seconds: float = 2.0) -> None:
        """Set backoff period after rate limit error."""
        self.backoff_until = time.time() + seconds


class CloreAI:
    """Synchronous Clore.ai API client."""

    BASE_URL = "https://api.clore.ai/v1"

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        timeout: float = 30.0,
        max_retries: int = 3,
    ):
        """
        Initialize Clore.ai client.

        Args:
            api_key: Clore.ai API key (optional for public endpoints)
            base_url: Custom API base URL (default: https://api.clore.ai/v1)
            timeout: Request timeout in seconds
            max_retries: Maximum retry attempts for rate limit errors
        """
        self.api_key = api_key
        self.base_url = base_url or self.BASE_URL
        self.timeout = timeout
        self.max_retries = max_retries
        self.rate_limiter = RateLimiter()
        self.client = httpx.Client(timeout=timeout)

    def __enter__(self) -> "CloreAI":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def close(self) -> None:
        """Close HTTP client."""
        self.client.close()

    def _request(
        self,
        method: str,
        endpoint: str,
        data: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        require_auth: bool = True,
        is_create_order: bool = False,
    ) -> Any:
        """
        Make HTTP request to Clore.ai API with rate limiting and retries.

        Args:
            method: HTTP method (GET, POST)
            endpoint: API endpoint path
            data: Request body (for POST)
            params: Query parameters
            require_auth: Whether authentication is required
            is_create_order: Whether this is a create_order call (special rate limit)

        Returns:
            Response data

        Raises:
            CloreAPIError: On API errors
            AuthError: If auth required but not provided
        """
        if require_auth and not self.api_key:
            from clore_ai.exceptions import AuthError

            raise AuthError("API key required for this endpoint")

        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        headers = {}
        if self.api_key:
            headers["auth"] = self.api_key

        retry_count = 0
        backoff = 1.0

        while retry_count <= self.max_retries:
            # Rate limiting
            self.rate_limiter.wait(is_create_order=is_create_order)

            try:
                if method.upper() == "GET":
                    # Special handling for server_config (GET with body)
                    if endpoint == "server_config" and data:
                        response = self.client.request(
                            "GET", url, headers=headers, json=data, params=params
                        )
                    else:
                        response = self.client.get(url, headers=headers, params=params)
                else:
                    response = self.client.post(url, headers=headers, json=data, params=params)

                response.raise_for_status()
                result = response.json()

                # Handle Clore.ai error codes
                if isinstance(result, dict) and "code" in result:
                    code = result["code"]
                    if code != 0:
                        error_class = ERROR_MAP.get(code, CloreAPIError)
                        message = result.get("message", f"API error code {code}")

                        # Rate limit: apply backoff and retry
                        if code == 5:
                            if retry_count < self.max_retries:
                                self.rate_limiter.set_backoff(backoff)
                                retry_count += 1
                                backoff *= 2  # Exponential backoff
                                continue

                        raise error_class(message, code=code, response=result)

                return result

            except httpx.HTTPStatusError as e:
                if retry_count >= self.max_retries:
                    raise CloreAPIError(f"HTTP {e.response.status_code}: {e.response.text}")
                retry_count += 1
                time.sleep(backoff)
                backoff *= 2

            except httpx.RequestError as e:
                if retry_count >= self.max_retries:
                    raise CloreAPIError(f"Request failed: {str(e)}")
                retry_count += 1
                time.sleep(backoff)
                backoff *= 2

        raise CloreAPIError("Max retries exceeded")

    def wallets(self) -> List[Wallet]:
        """
        Get wallet balances.

        Returns:
            List of wallet objects with balances.

        API response::

            {"wallets": [{"name": "bitcoin", "deposit": "bc1...",
                          "balance": 0.041, "withdrawal_fee": 0.00005}, ...],
             "code": 0}
        """
        result = self._request("GET", "wallets")
        wallets_data = result.get("wallets", [])
        return [Wallet(**w) for w in wallets_data]

    def marketplace(
        self,
        gpu: Optional[str] = None,
        min_gpu_count: Optional[int] = None,
        min_ram_gb: Optional[float] = None,
        max_price_usd: Optional[float] = None,
        available_only: bool = True,
    ) -> List[MarketplaceServer]:
        """
        Get marketplace servers with optional filters.

        Args:
            gpu: Filter by GPU model (substring match in ``specs.gpu`` or ``gpu_array``)
            min_gpu_count: Minimum number of GPUs (from ``gpu_array`` length)
            min_ram_gb: Minimum RAM in GB (from ``specs.ram``)
            max_price_usd: Maximum on-demand price in USD (from ``price.usd.on_demand_usd``)
            available_only: Show only non-rented servers

        Returns:
            List of matching servers
        """
        result = self._request("GET", "marketplace", require_auth=False)
        servers_data = result.get("servers", [])
        servers = [MarketplaceServer(**s) for s in servers_data]

        # Client-side filtering using nested structure accessors
        filtered = servers

        if available_only:
            filtered = [s for s in filtered if s.available]

        if gpu:
            gpu_lower = gpu.lower()

            def _matches_gpu(s: MarketplaceServer) -> bool:
                # Check specs.gpu
                if s.specs and s.specs.gpu and gpu_lower in s.specs.gpu.lower():
                    return True
                # Check flattened gpu_array
                for name in s._flat_gpu_array():
                    if gpu_lower in name.lower():
                        return True
                return False

            filtered = [s for s in filtered if _matches_gpu(s)]

        if min_gpu_count:
            filtered = [s for s in filtered if s.gpu_count >= min_gpu_count]

        if min_ram_gb:
            filtered = [s for s in filtered if s.ram_gb is not None and s.ram_gb >= min_ram_gb]

        if max_price_usd:
            filtered = [
                s for s in filtered
                if s.price_usd is not None and s.price_usd <= max_price_usd
            ]

        return filtered

    def my_servers(self) -> List[MyServer]:
        """
        Get list of your hosted servers.

        Returns:
            List of your servers (``MyServer`` objects).
        """
        result = self._request("GET", "my_servers")
        servers_data = result.get("servers", [])
        return [MyServer(**s) for s in servers_data]

    def server_config(self, server_name: str) -> ServerConfig:
        """
        Get configuration for a specific server.

        Args:
            server_name: Name of the server

        Returns:
            Server configuration
        """
        result = self._request("GET", "server_config", data={"server_name": server_name})
        return ServerConfig(**result.get("config", {}))

    def my_orders(self, include_completed: bool = False) -> List[Order]:
        """
        Get list of your orders.

        Args:
            include_completed: Include completed orders

        Returns:
            List of orders
        """
        params = {"return_completed": str(include_completed).lower()}
        result = self._request("GET", "my_orders", params=params)
        orders_data = result.get("orders", [])
        return [Order(**o) for o in orders_data]

    def spot_marketplace(self, server_id: int) -> SpotMarket:
        """
        Get spot market data for a server.

        Args:
            server_id: Server ID

        Returns:
            SpotMarket object with offers, server info, and currency rates.
        """
        params = {"market": server_id}
        result = self._request("GET", "spot_marketplace", params=params)
        market_data = result.get("market", {})
        return SpotMarket(**market_data)

    def set_server_settings(
        self,
        name: str,
        availability: Optional[bool] = None,
        mrl: Optional[int] = None,
        on_demand: Optional[float] = None,
        spot: Optional[float] = None,
    ) -> Dict[str, Any]:
        """
        Update server settings.

        Args:
            name: Server name
            availability: Server availability status
            mrl: Minimum rental length in hours
            on_demand: On-demand price
            spot: Spot price

        Returns:
            API response
        """
        data: Dict[str, Any] = {"name": name}
        if availability is not None:
            data["availability"] = availability
        if mrl is not None:
            data["mrl"] = mrl
        if on_demand is not None:
            data["on_demand"] = on_demand
        if spot is not None:
            data["spot"] = spot

        return self._request("POST", "set_server_settings", data=data)

    def set_spot_price(self, order_id: int, price: float) -> Dict[str, Any]:
        """
        Update spot price for an order.

        Args:
            order_id: Order ID
            price: New spot price

        Returns:
            API response
        """
        data = {"order_id": order_id, "desired_price": price}
        return self._request("POST", "set_spot_price", data=data)

    def cancel_order(self, order_id: int, issue: Optional[str] = None) -> Dict[str, Any]:
        """
        Cancel an order.

        Args:
            order_id: Order ID to cancel
            issue: Optional cancellation reason

        Returns:
            API response
        """
        data: Dict[str, Any] = {"id": order_id}
        if issue:
            data["issue"] = issue
        return self._request("POST", "cancel_order", data=data)

    def create_order(
        self,
        server_id: int,
        image: str,
        type: Literal["on-demand", "spot"],
        currency: str,
        ssh_password: Optional[str] = None,
        ssh_key: Optional[str] = None,
        ports: Optional[Dict[str, str]] = None,
        env: Optional[Dict[str, str]] = None,
        jupyter_token: Optional[str] = None,
        command: Optional[str] = None,
        spot_price: Optional[float] = None,
        required_price: Optional[float] = None,
        autossh_entrypoint: Optional[str] = None,
    ) -> Order:
        """
        Create a new order.

        Args:
            server_id: Server ID to rent
            image: Docker image (e.g., "cloreai/ubuntu22.04-cuda12")
            type: Order type ("on-demand" or "spot")
            currency: Payment currency (e.g., "bitcoin")
            ssh_password: SSH password
            ssh_key: SSH public key
            ports: Port mappings (e.g., {"22": "tcp", "8888": "http"})
            env: Environment variables
            jupyter_token: Jupyter token
            command: Custom startup command
            spot_price: Spot price (for spot orders)
            required_price: Required price
            autossh_entrypoint: Auto SSH entrypoint

        Returns:
            Created order
        """
        data: Dict[str, Any] = {
            "renting_server": server_id,
            "image": image,
            "type": type,
            "currency": currency,
        }

        if ssh_password:
            data["ssh_password"] = ssh_password
        if ssh_key:
            data["ssh_key"] = ssh_key
        if ports:
            data["ports"] = ports
        if env:
            data["env"] = env
        if jupyter_token:
            data["jupyter_token"] = jupyter_token
        if command:
            data["command"] = command
        if spot_price:
            data["spotprice"] = spot_price
        if required_price:
            data["required_price"] = required_price
        if autossh_entrypoint:
            data["autossh_entrypoint"] = autossh_entrypoint

        result = self._request("POST", "create_order", data=data, is_create_order=True)
        return Order(**result.get("order", {}))


class AsyncCloreAI:
    """Asynchronous Clore.ai API client."""

    BASE_URL = "https://api.clore.ai/v1"

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        timeout: float = 30.0,
        max_retries: int = 3,
    ):
        """
        Initialize async Clore.ai client.

        Args:
            api_key: Clore.ai API key (optional for public endpoints)
            base_url: Custom API base URL
            timeout: Request timeout in seconds
            max_retries: Maximum retry attempts for rate limit errors
        """
        self.api_key = api_key
        self.base_url = base_url or self.BASE_URL
        self.timeout = timeout
        self.max_retries = max_retries
        self.rate_limiter = RateLimiter()
        self.client = httpx.AsyncClient(timeout=timeout)

    async def __aenter__(self) -> "AsyncCloreAI":
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    async def close(self) -> None:
        """Close HTTP client."""
        await self.client.aclose()

    async def _request(
        self,
        method: str,
        endpoint: str,
        data: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        require_auth: bool = True,
        is_create_order: bool = False,
    ) -> Any:
        """Async HTTP request with rate limiting and retries."""
        if require_auth and not self.api_key:
            from clore_ai.exceptions import AuthError

            raise AuthError("API key required for this endpoint")

        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        headers = {}
        if self.api_key:
            headers["auth"] = self.api_key

        retry_count = 0
        backoff = 1.0

        while retry_count <= self.max_retries:
            await self.rate_limiter.wait_async(is_create_order=is_create_order)

            try:
                if method.upper() == "GET":
                    if endpoint == "server_config" and data:
                        response = await self.client.request(
                            "GET", url, headers=headers, json=data, params=params
                        )
                    else:
                        response = await self.client.get(url, headers=headers, params=params)
                else:
                    response = await self.client.post(
                        url, headers=headers, json=data, params=params
                    )

                response.raise_for_status()
                result = response.json()

                if isinstance(result, dict) and "code" in result:
                    code = result["code"]
                    if code != 0:
                        error_class = ERROR_MAP.get(code, CloreAPIError)
                        message = result.get("message", f"API error code {code}")

                        if code == 5:
                            if retry_count < self.max_retries:
                                self.rate_limiter.set_backoff(backoff)
                                retry_count += 1
                                backoff *= 2
                                continue

                        raise error_class(message, code=code, response=result)

                return result

            except httpx.HTTPStatusError as e:
                if retry_count >= self.max_retries:
                    raise CloreAPIError(f"HTTP {e.response.status_code}: {e.response.text}")
                retry_count += 1
                await asyncio.sleep(backoff)
                backoff *= 2

            except httpx.RequestError as e:
                if retry_count >= self.max_retries:
                    raise CloreAPIError(f"Request failed: {str(e)}")
                retry_count += 1
                await asyncio.sleep(backoff)
                backoff *= 2

        raise CloreAPIError("Max retries exceeded")

    async def wallets(self) -> List[Wallet]:
        """Get wallet balances."""
        result = await self._request("GET", "wallets")
        wallets_data = result.get("wallets", [])
        return [Wallet(**w) for w in wallets_data]

    async def marketplace(
        self,
        gpu: Optional[str] = None,
        min_gpu_count: Optional[int] = None,
        min_ram_gb: Optional[float] = None,
        max_price_usd: Optional[float] = None,
        available_only: bool = True,
    ) -> List[MarketplaceServer]:
        """Get marketplace servers with filters."""
        result = await self._request("GET", "marketplace", require_auth=False)
        servers_data = result.get("servers", [])
        servers = [MarketplaceServer(**s) for s in servers_data]

        filtered = servers

        if available_only:
            filtered = [s for s in filtered if s.available]

        if gpu:
            gpu_lower = gpu.lower()

            def _matches_gpu(s: MarketplaceServer) -> bool:
                if s.specs and s.specs.gpu and gpu_lower in s.specs.gpu.lower():
                    return True
                for name in s._flat_gpu_array():
                    if gpu_lower in name.lower():
                        return True
                return False

            filtered = [s for s in filtered if _matches_gpu(s)]

        if min_gpu_count:
            filtered = [s for s in filtered if s.gpu_count >= min_gpu_count]

        if min_ram_gb:
            filtered = [s for s in filtered if s.ram_gb is not None and s.ram_gb >= min_ram_gb]

        if max_price_usd:
            filtered = [
                s for s in filtered
                if s.price_usd is not None and s.price_usd <= max_price_usd
            ]

        return filtered

    async def my_servers(self) -> List[MyServer]:
        """Get your hosted servers."""
        result = await self._request("GET", "my_servers")
        servers_data = result.get("servers", [])
        return [MyServer(**s) for s in servers_data]

    async def server_config(self, server_name: str) -> ServerConfig:
        """Get server configuration."""
        result = await self._request("GET", "server_config", data={"server_name": server_name})
        return ServerConfig(**result.get("config", {}))

    async def my_orders(self, include_completed: bool = False) -> List[Order]:
        """Get your orders."""
        params = {"return_completed": str(include_completed).lower()}
        result = await self._request("GET", "my_orders", params=params)
        orders_data = result.get("orders", [])
        return [Order(**o) for o in orders_data]

    async def spot_marketplace(self, server_id: int) -> SpotMarket:
        """Get spot market data for a server."""
        params = {"market": server_id}
        result = await self._request("GET", "spot_marketplace", params=params)
        market_data = result.get("market", {})
        return SpotMarket(**market_data)

    async def set_server_settings(
        self,
        name: str,
        availability: Optional[bool] = None,
        mrl: Optional[int] = None,
        on_demand: Optional[float] = None,
        spot: Optional[float] = None,
    ) -> Dict[str, Any]:
        """Update server settings."""
        data: Dict[str, Any] = {"name": name}
        if availability is not None:
            data["availability"] = availability
        if mrl is not None:
            data["mrl"] = mrl
        if on_demand is not None:
            data["on_demand"] = on_demand
        if spot is not None:
            data["spot"] = spot

        return await self._request("POST", "set_server_settings", data=data)

    async def set_spot_price(self, order_id: int, price: float) -> Dict[str, Any]:
        """Update spot price."""
        data = {"order_id": order_id, "desired_price": price}
        return await self._request("POST", "set_spot_price", data=data)

    async def cancel_order(self, order_id: int, issue: Optional[str] = None) -> Dict[str, Any]:
        """Cancel order."""
        data: Dict[str, Any] = {"id": order_id}
        if issue:
            data["issue"] = issue
        return await self._request("POST", "cancel_order", data=data)

    async def create_order(
        self,
        server_id: int,
        image: str,
        type: Literal["on-demand", "spot"],
        currency: str,
        ssh_password: Optional[str] = None,
        ssh_key: Optional[str] = None,
        ports: Optional[Dict[str, str]] = None,
        env: Optional[Dict[str, str]] = None,
        jupyter_token: Optional[str] = None,
        command: Optional[str] = None,
        spot_price: Optional[float] = None,
        required_price: Optional[float] = None,
        autossh_entrypoint: Optional[str] = None,
    ) -> Order:
        """Create new order."""
        data: Dict[str, Any] = {
            "renting_server": server_id,
            "image": image,
            "type": type,
            "currency": currency,
        }

        if ssh_password:
            data["ssh_password"] = ssh_password
        if ssh_key:
            data["ssh_key"] = ssh_key
        if ports:
            data["ports"] = ports
        if env:
            data["env"] = env
        if jupyter_token:
            data["jupyter_token"] = jupyter_token
        if command:
            data["command"] = command
        if spot_price:
            data["spotprice"] = spot_price
        if required_price:
            data["required_price"] = required_price
        if autossh_entrypoint:
            data["autossh_entrypoint"] = autossh_entrypoint

        result = await self._request("POST", "create_order", data=data, is_create_order=True)
        return Order(**result.get("order", {}))
