"""Internal HTTP client for Agent Berlin SDK."""

import json as json_module
import logging
import random
import time
from typing import Any, Callable, Dict, Optional

import requests

from .config import RetryConfig
from .exceptions import (
    AgentBerlinAPIError,
    AgentBerlinAuthenticationError,
    AgentBerlinConnectionError,
    AgentBerlinNotFoundError,
    AgentBerlinRateLimitError,
    AgentBerlinServerError,
)

__version__ = "0.51.0"
logger = logging.getLogger("agentberlin")


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

    # Status codes that should trigger retry
    RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}

    # Default threshold for proactive token refresh (30 minutes before expiry)
    DEFAULT_REFRESH_THRESHOLD = 1800

    def __init__(
        self,
        token: str,
        base_url: str,
        timeout: int = 30,
        retry_config: Optional[RetryConfig] = None,
        expires_at: Optional[int] = None,
        refresh_threshold: int = DEFAULT_REFRESH_THRESHOLD,
        on_token_refreshed: Optional[Callable[[str, int], None]] = None,
    ) -> None:
        self._token = token
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._retry_config = retry_config or RetryConfig()
        self._expires_at = expires_at
        self._refresh_threshold = refresh_threshold
        self._on_token_refreshed = on_token_refreshed
        self._session = requests.Session()
        self._session.headers.update(
            {
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
                "User-Agent": f"agentberlin-python/{__version__}",
            }
        )

    def _maybe_refresh_token(self) -> None:
        """Refresh token if it expires within the refresh threshold.

        This method proactively refreshes the token before it expires to avoid
        401 errors during requests. If the token is already expired, it raises
        an authentication error immediately so the caller (e.g., MCP) can
        re-authenticate.

        Raises:
            AgentBerlinAuthenticationError: If the token is already expired.
        """
        if self._expires_at is None:
            return

        current_time = time.time()

        # If token is already expired, fail immediately
        if current_time >= self._expires_at:
            raise AgentBerlinAuthenticationError(
                "Token has expired. Please re-authenticate to get a new token."
            )

        # Check if token expires within threshold
        if current_time < (self._expires_at - self._refresh_threshold):
            return

        logger.info("[agentberlin] Token expiring soon, attempting refresh...")

        try:
            response = requests.post(
                f"{self._base_url}/auth/refresh",
                headers={"Authorization": f"Bearer {self._token}"},
                timeout=30,
            )

            if response.ok:
                data = response.json()
                self._token = data["token"]
                self._expires_at = data["expires_at"]
                self._session.headers["Authorization"] = f"Bearer {self._token}"

                logger.info("[agentberlin] Token refreshed successfully")

                if self._on_token_refreshed:
                    self._on_token_refreshed(self._token, self._expires_at)
            else:
                logger.warning(
                    f"[agentberlin] Token refresh failed with status {response.status_code}: {response.text}"
                )
        except requests.exceptions.RequestException as e:
            logger.warning(f"[agentberlin] Token refresh failed: {e}")

    def _log_request(
        self,
        method: str,
        path: str,
        data: Optional[Dict[str, Any]] = None,
    ) -> None:
        """Log an outgoing HTTP request."""
        if data:
            try:
                data_str = json_module.dumps(data)
            except (TypeError, ValueError):
                data_str = str(data)
            logger.info(f"[DEBUG] {method} {path} body={data_str}")
        else:
            logger.info(f"[DEBUG] {method} {path}")

    def _calculate_delay(self, attempt: int, retry_after: Optional[int] = None) -> float:
        """Calculate delay before next retry attempt.

        Args:
            attempt: Current attempt number (0-indexed).
            retry_after: Server-specified retry delay (from Retry-After header).

        Returns:
            Delay in seconds.
        """
        if retry_after is not None:
            # Use server-specified delay, but cap it
            return min(retry_after, self._retry_config.max_delay)

        # Exponential backoff with jitter
        base = self._retry_config.base_delay * (2**attempt)
        jitter = random.uniform(0, self._retry_config.jitter)
        delay = min(base + jitter, self._retry_config.max_delay)
        return delay

    def _should_retry(self, exception: Exception) -> bool:
        """Determine if an exception should trigger a retry.

        Args:
            exception: The exception that was raised.

        Returns:
            True if the request should be retried.
        """
        # Retry on connection/timeout errors
        if isinstance(exception, AgentBerlinConnectionError):
            return True

        # Retry on specific server errors
        if isinstance(exception, AgentBerlinServerError):
            return True

        # Retry on rate limit
        if isinstance(exception, AgentBerlinRateLimitError):
            return True

        # Don't retry on auth, not found, validation, or other client errors
        return False

    def _get_retry_after(self, exception: Exception) -> Optional[int]:
        """Extract Retry-After value from exception if available."""
        if isinstance(exception, AgentBerlinRateLimitError):
            return exception.retry_after
        return None

    def _execute_with_retry(
        self,
        operation: Callable[[], requests.Response],
        operation_name: str,
    ) -> Dict[str, Any]:
        """Execute an HTTP operation with retry logic.

        Args:
            operation: Callable that performs the HTTP request.
            operation_name: Name of the operation for logging.

        Returns:
            Response data as dict.

        Raises:
            AgentBerlinAPIError: On API errors after retries exhausted.
            AgentBerlinConnectionError: On network errors after retries exhausted.
        """
        # Proactively refresh token if it's about to expire
        self._maybe_refresh_token()

        last_exception: Optional[Exception] = None

        for attempt in range(self._retry_config.max_retries + 1):
            try:
                response = operation()
                return self._handle_response(response)

            except requests.exceptions.Timeout:
                last_exception = AgentBerlinConnectionError(
                    f"Request timed out after {self._timeout}s"
                )
            except requests.exceptions.ConnectionError as e:
                last_exception = AgentBerlinConnectionError(f"Connection error: {e}")
            except requests.exceptions.RequestException as e:
                last_exception = AgentBerlinConnectionError(f"Request failed: {e}")
            except (AgentBerlinServerError, AgentBerlinRateLimitError) as e:
                last_exception = e
            except AgentBerlinAPIError:
                # Non-retryable API errors (auth, not found, validation)
                raise

            # Check if we should retry
            if not self._should_retry(last_exception):
                raise last_exception

            # Check if we have retries left
            if attempt >= self._retry_config.max_retries:
                logger.warning(
                    f"[agentberlin] {operation_name} max retries ({self._retry_config.max_retries}) "
                    f"exhausted. Last error: {last_exception}"
                )
                raise last_exception

            # Calculate delay and wait
            retry_after = self._get_retry_after(last_exception)
            delay = self._calculate_delay(attempt, retry_after)

            logger.warning(
                f"[agentberlin] {operation_name} attempt {attempt + 1}/{self._retry_config.max_retries + 1} failed: {last_exception}. "
                f"Retrying in {delay:.1f}s..."
            )
            time.sleep(delay)

        # Should not reach here, but just in case
        raise last_exception  # type: ignore

    def post(
        self,
        path: str,
        json: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make a POST request with retry logic.

        Args:
            path: API endpoint path.
            json: Request body as dict.

        Returns:
            Response data as dict.

        Raises:
            AgentBerlinAPIError: On API errors.
            AgentBerlinConnectionError: On network errors.
        """
        url = f"{self._base_url}{path}"
        self._log_request("POST", path, json)

        def operation() -> requests.Response:
            return self._session.post(url, json=json, timeout=self._timeout)

        return self._execute_with_retry(operation, f"POST {path}")

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

        Args:
            path: API endpoint path.
            params: Query parameters.

        Returns:
            Response data as dict.

        Raises:
            AgentBerlinAPIError: On API errors.
            AgentBerlinConnectionError: On network errors.
        """
        url = f"{self._base_url}{path}"
        self._log_request("GET", path, params)

        def operation() -> requests.Response:
            return self._session.get(url, params=params, timeout=self._timeout)

        return self._execute_with_retry(operation, f"GET {path}")

    def patch(
        self,
        path: str,
        json: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make a PATCH request with retry logic.

        Args:
            path: API endpoint path.
            json: Request body as dict.

        Returns:
            Response data as dict.

        Raises:
            AgentBerlinAPIError: On API errors.
            AgentBerlinConnectionError: On network errors.
        """
        url = f"{self._base_url}{path}"
        self._log_request("PATCH", path, json)

        def operation() -> requests.Response:
            return self._session.patch(url, json=json, timeout=self._timeout)

        return self._execute_with_retry(operation, f"PATCH {path}")

    def upload_file(
        self,
        path: str,
        file_data: bytes,
        filename: str,
        content_type: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Upload a file with retry logic.

        Args:
            path: API endpoint path.
            file_data: Raw bytes of the file.
            filename: Name of the file.
            content_type: MIME type of the file.

        Returns:
            Response data as dict.

        Raises:
            AgentBerlinAPIError: On API errors.
            AgentBerlinConnectionError: On network errors.
        """
        url = f"{self._base_url}{path}"
        files = {"file": (filename, file_data, content_type or "application/octet-stream")}
        headers = {
            "Authorization": f"Bearer {self._token}",
            "User-Agent": f"agentberlin-python/{__version__}",
        }
        logger.info(
            f"[DEBUG] UPLOAD {path} filename={filename} size={len(file_data)} content_type={content_type}"
        )

        def operation() -> requests.Response:
            return requests.post(url, files=files, headers=headers, timeout=self._timeout)

        return self._execute_with_retry(operation, f"UPLOAD {path}")

    def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
        """Handle response and raise appropriate exceptions."""
        # Log response
        response_body = response.text[:1000] if response.text else "(empty)"
        if response.ok:
            logger.info(f"[DEBUG] Response status={response.status_code} body={response_body}")
        else:
            logger.info(f"[DEBUG] Response ERROR status={response.status_code} body={response_body}")

        # Success
        if response.ok:
            if response.content:
                return response.json()  # type: ignore[no-any-return]
            return {}

        # Parse error response
        try:
            error_data = response.json()
            message = error_data.get("message", error_data.get("error", "Unknown error"))
            error_code = error_data.get("code")
        except Exception:
            message = response.text or f"HTTP {response.status_code}"
            error_code = None
            error_data = {}

        # Map status codes to exceptions
        if response.status_code == 401:
            raise AgentBerlinAuthenticationError(message, details=error_data)
        elif response.status_code == 404:
            raise AgentBerlinNotFoundError(
                message,
                status_code=404,
                error_code=error_code,
                details=error_data,
            )
        elif response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            raise AgentBerlinRateLimitError(
                message,
                retry_after=int(retry_after) if retry_after else None,
                details=error_data,
            )
        elif response.status_code >= 500:
            raise AgentBerlinServerError(
                message,
                status_code=response.status_code,
                error_code=error_code,
                details=error_data,
            )
        else:
            raise AgentBerlinAPIError(
                message,
                status_code=response.status_code,
                error_code=error_code,
                details=error_data,
            )
