"""HTTP client for ZenOTC SDK."""

from __future__ import annotations

import asyncio
import hashlib
import hmac
import json
import logging
import time
from typing import Any, Dict, Optional, TypeVar, Union

import httpx
from pydantic import BaseModel

from zenotc.config import Config
from zenotc.exceptions import (
    AuthenticationError,
    ConnectionError,
    RateLimitError,
    ServerError,
    TimeoutError,
    ValidationError,
    ZenOTCError,
)

T = TypeVar("T", bound=BaseModel)
logger = logging.getLogger(__name__)


class HTTPClient:
    """HTTP client with authentication, retry logic, and error handling."""

    def __init__(self, config: Config) -> None:
        self._config = config
        self._client: Optional[httpx.AsyncClient] = None

    def _get_client(self) -> httpx.AsyncClient:
        """Get or create the HTTP client."""
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                base_url=self._config.effective_base_url,
                timeout=httpx.Timeout(self._config.timeout),
                headers=self._get_default_headers(),
            )
        return self._client

    def _get_default_headers(self) -> Dict[str, str]:
        """Get default headers for all requests."""
        return {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": "ZenOTC-Python-SDK/0.1.0",
            "X-SDK-Client": "python",
        }

    def _sign_request(
        self,
        method: str,
        path: str,
        timestamp: str,
        body: str = "",
    ) -> str:
        """Generate HMAC signature for request authentication.

        The signature scheme is:
        1. Client computes: HMAC-SHA256(SHA256(secret), message)
        2. Server verifies using stored SHA256(secret) as HMAC key

        This matches the backend's verification in api-key.service.ts
        """
        message = f"{timestamp}{method.upper()}{path}{body}"
        # Hash the secret first - server stores SHA256(secret) as secretHash
        hmac_key = hashlib.sha256(
            self._config.api_secret.encode("utf-8")
        ).hexdigest()
        signature = hmac.new(
            hmac_key.encode("utf-8"),
            message.encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()
        return signature

    def _get_auth_headers(
        self,
        method: str,
        path: str,
        body: str = "",
    ) -> Dict[str, str]:
        """Get authentication headers for a request."""
        timestamp = str(int(time.time() * 1000))
        signature = self._sign_request(method, path, timestamp, body)

        return {
            "X-API-Key": self._config.api_key,
            "X-API-Timestamp": timestamp,
            "X-API-Signature": signature,
        }

    def _handle_error_response(self, response: httpx.Response) -> None:
        """Handle error responses from the API."""
        status_code = response.status_code

        try:
            data = response.json()
            message = data.get("message", "Unknown error")
            error_code = data.get("errorCode") or data.get("error")
            details = data.get("details", {})
        except Exception:
            message = response.text or "Unknown error"
            error_code = None
            details = {}

        if status_code == 401:
            raise AuthenticationError(
                message=message,
                status_code=status_code,
                error_code=error_code,
                details=details,
            )
        elif status_code == 429:
            retry_after = response.headers.get("Retry-After")
            raise RateLimitError(
                message=message,
                retry_after=float(retry_after) if retry_after else None,
                status_code=status_code,
                error_code=error_code,
                details=details,
            )
        elif status_code == 400:
            raise ValidationError(
                message=message,
                field_errors=details.get("fieldErrors"),
                status_code=status_code,
                error_code=error_code,
                details=details,
            )
        elif status_code >= 500:
            raise ServerError(
                message=message,
                status_code=status_code,
                error_code=error_code,
                details=details,
            )
        else:
            raise ZenOTCError(
                message=message,
                status_code=status_code,
                error_code=error_code,
                details=details,
            )

    def _is_retryable_error(self, error: Exception) -> bool:
        """Check if an error is retryable."""
        if isinstance(error, (ConnectionError, TimeoutError)):
            return True
        if isinstance(error, ServerError) and error.status_code in (502, 503, 504):
            return True
        if isinstance(error, RateLimitError):
            return True
        return False

    def _serialize_body(
        self, json_data: Optional[Union[Dict[str, Any], BaseModel]]
    ) -> tuple[str, Optional[Dict[str, Any]]]:
        """Serialize request body consistently for signature and request.

        Returns:
            Tuple of (serialized body string for signature, json payload for request)
        """
        if json_data is None:
            return "", None

        if isinstance(json_data, BaseModel):
            json_payload = json_data.model_dump(mode="json", exclude_none=True)
        else:
            json_payload = json_data

        # Use consistent serialization for signature computation
        # sort_keys ensures deterministic ordering for signature verification
        body = json.dumps(json_payload, separators=(",", ":"), sort_keys=True)
        return body, json_payload

    async def request(
        self,
        method: str,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        json_data: Optional[Union[Dict[str, Any], BaseModel]] = None,
    ) -> Dict[str, Any]:
        """Make an authenticated HTTP request with retry logic."""
        client = self._get_client()

        # Serialize body consistently
        body, json_payload = self._serialize_body(json_data)

        last_error: Optional[Exception] = None
        max_retries = self._config.max_retries

        for attempt in range(max_retries + 1):
            # Generate fresh auth headers for each attempt (timestamp must be current)
            auth_headers = self._get_auth_headers(method, path, body)

            try:
                # Send the sorted JSON to ensure signature matches
                response = await client.request(
                    method=method,
                    url=path,
                    params=params,
                    content=body if body else None,
                    headers=auth_headers,
                )

                if not response.is_success:
                    self._handle_error_response(response)

                if response.status_code == 204:
                    return {}

                return response.json()

            except httpx.ConnectError as e:
                last_error = ConnectionError(f"Failed to connect: {e}")
            except httpx.TimeoutException as e:
                last_error = TimeoutError(f"Request timed out: {e}")
            except (RateLimitError, ServerError, ConnectionError, TimeoutError) as e:
                last_error = e

            # Check if we should retry
            if last_error and self._is_retryable_error(last_error):
                if attempt < max_retries:
                    # Calculate backoff delay
                    if isinstance(last_error, RateLimitError) and last_error.retry_after:
                        delay = last_error.retry_after
                    else:
                        # Exponential backoff: 1s, 2s, 4s...
                        delay = min(2 ** attempt, 30)

                    logger.warning(
                        f"Request failed (attempt {attempt + 1}/{max_retries + 1}), "
                        f"retrying in {delay}s: {last_error}"
                    )
                    await asyncio.sleep(delay)
                    continue

            # Non-retryable error or max retries exceeded
            raise last_error

        # Should not reach here, but raise last error if we do
        if last_error:
            raise last_error
        raise ZenOTCError("Request failed with unknown error")

    async def get(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make a GET request."""
        return await self.request("GET", path, params=params)

    async def post(
        self,
        path: str,
        json_data: Optional[Union[Dict[str, Any], BaseModel]] = None,
        params: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make a POST request."""
        return await self.request("POST", path, params=params, json_data=json_data)

    async def put(
        self,
        path: str,
        json_data: Optional[Union[Dict[str, Any], BaseModel]] = None,
        params: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make a PUT request."""
        return await self.request("PUT", path, params=params, json_data=json_data)

    async def patch(
        self,
        path: str,
        json_data: Optional[Union[Dict[str, Any], BaseModel]] = None,
        params: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make a PATCH request."""
        return await self.request("PATCH", path, params=params, json_data=json_data)

    async def delete(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        json_data: Optional[Union[Dict[str, Any], BaseModel]] = None,
    ) -> Dict[str, Any]:
        """Make a DELETE request."""
        return await self.request("DELETE", path, params=params, json_data=json_data)

    async def close(self) -> None:
        """Close the HTTP client."""
        if self._client is not None:
            await self._client.aclose()
            self._client = None
