"""Base HTTP client with retry and error handling.

This module provides a base HTTP client class with consistent
behavior for retries, rate limiting, and error handling.
"""

from __future__ import annotations

import contextlib
import logging
import random
import time
from dataclasses import dataclass
from email.utils import parsedate_to_datetime
from typing import Any, Callable, Dict, Iterable, Optional, Tuple

logger = logging.getLogger(__name__)


@dataclass
class RetryConfig:
    """Configuration for HTTP retry behavior.

    Attributes:
        max_attempts: Maximum number of retry attempts (including initial)
        base_delay: Base delay in seconds for exponential backoff
        max_delay: Maximum delay cap in seconds
        jitter: Whether to add random jitter to delays
        retry_on: HTTP status codes to retry on
    """
    max_attempts: int = 4
    base_delay: float = 1.0
    max_delay: float = 30.0
    jitter: bool = True
    retry_on: Tuple[int, ...] = (429, 500, 502, 503, 504)


class HTTPError(Exception):
    """Base exception for HTTP client errors."""


class AuthenticationError(HTTPError):
    """Raised for authentication/authorization failures (401, 403)."""


class RateLimitError(HTTPError):
    """Raised for rate limit responses (429).

    Attributes:
        retry_after: Suggested retry delay in seconds, if provided
    """

    def __init__(self, message: str, retry_after: Optional[float] = None):
        super().__init__(message)
        self.retry_after = retry_after


class TransientError(HTTPError):
    """Raised for retryable server-side failures (5xx)."""


class BaseHTTPClient:
    """Base HTTP client with automatic retries and error handling.

    Provides consistent HTTP behavior:
    - Automatic retries with exponential backoff and jitter
    - Respect for Retry-After on 429s
    - Error classification (auth, rate limit, transient)
    - Structured logging for observability

    This class requires the `requests` library. Subclasses can
    override `_get_session()` to provide custom session configuration.

    Args:
        service_name: Name used in log messages
        default_retry_config: Default retry configuration
        session: Optional pre-configured requests.Session

    Example:
        >>> client = BaseHTTPClient(service_name="myapi")
        >>> response = client._get("https://api.example.com/data")
    """

    def __init__(
        self,
        service_name: str = "http",
        default_retry_config: Optional[RetryConfig] = None,
        session: Optional[Any] = None,
    ) -> None:
        self.service_name = service_name
        self.default_retry_config = default_retry_config or RetryConfig()
        self._session = session
        self._session_created = False
        self.correlation_id: Optional[str] = None

    def _get_session(self) -> Any:
        """Get or create HTTP session."""
        if self._session is None:
            try:
                import requests
                self._session = requests.Session()
                self._session_created = True
            except ImportError:
                raise HTTPError(
                    "requests library not installed. Run: pip install requests"
                )
        return self._session

    def _request(
        self,
        method: str,
        url: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json: Optional[Dict[str, Any]] = None,
        data: Optional[Any] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: float = 30.0,
        retry_config: Optional[RetryConfig] = None,
        auth: Optional[Tuple[str, str]] = None,
        allow_statuses: Tuple[int, ...] = (),
    ) -> Any:
        """Make an HTTP request with automatic retries.

        Args:
            method: HTTP method (GET, POST, etc.)
            url: Request URL
            params: Query parameters
            json: JSON body (will be serialized)
            data: Raw body data
            headers: Request headers
            timeout: Request timeout in seconds
            retry_config: Override default retry configuration
            auth: Basic auth tuple (username, password)
            allow_statuses: Additional status codes to accept without error

        Returns:
            requests.Response object

        Raises:
            AuthenticationError: For 401/403 responses
            RateLimitError: For 429 responses after retries exhausted
            TransientError: For 5xx responses after retries exhausted
            HTTPError: For other error responses
        """
        session = self._get_session()
        cfg = retry_config or self.default_retry_config
        attempt = 1

        while True:
            with self._timed_request(method, url) as timer:
                try:
                    response = session.request(
                        method=method,
                        url=url,
                        params=params,
                        json=json,
                        data=data,
                        headers=headers,
                        auth=auth,
                        timeout=timeout,
                    )
                except Exception as exc:
                    if attempt >= cfg.max_attempts:
                        raise TransientError(
                            f"{self.service_name} request failed: {exc}"
                        ) from exc
                    delay = self._compute_delay(cfg, attempt)
                    logger.warning(
                        "%s.retry transport_error attempt=%s delay=%.2fs error=%s",
                        self.service_name,
                        attempt,
                        delay,
                        exc,
                    )
                    self._sleep(delay)
                    attempt += 1
                    continue

            self._log_request(method, url, response.status_code, timer())

            status = response.status_code

            # Auth errors - don't retry
            if status in (401, 403):
                raise AuthenticationError(
                    f"{self.service_name} auth failed ({status})"
                )

            # Allowed statuses - return immediately
            if status in allow_statuses:
                return response

            # Rate limit - retry with Retry-After if available
            if status == 429:
                retry_after = self._retry_after_seconds(response)
                error = RateLimitError(
                    f"{self.service_name} rate limited (429)",
                    retry_after=retry_after,
                )
                if attempt >= cfg.max_attempts:
                    raise error
                delay = retry_after if retry_after is not None else self._compute_delay(cfg, attempt)
                logger.warning(
                    "%s.retry rate_limit attempt=%s delay=%.2fs",
                    self.service_name,
                    attempt,
                    delay,
                )
                self._sleep(delay)
                attempt += 1
                continue

            # Retryable status codes
            if status in cfg.retry_on:
                if attempt >= cfg.max_attempts:
                    raise TransientError(
                        f"{self.service_name} retryable error {status}: "
                        f"{self._response_snippet(response)}"
                    )
                delay = self._compute_delay(cfg, attempt)
                logger.warning(
                    "%s.retry status=%s attempt=%s delay=%.2fs",
                    self.service_name,
                    status,
                    attempt,
                    delay,
                )
                self._sleep(delay)
                attempt += 1
                continue

            # Other errors
            if not response.ok:
                raise HTTPError(
                    f"{self.service_name} error {status}: "
                    f"{self._response_snippet(response)}"
                )

            return response

    def _get(self, url: str, **kwargs: Any) -> Any:
        """Make a GET request."""
        return self._request("GET", url, **kwargs)

    def _post(self, url: str, **kwargs: Any) -> Any:
        """Make a POST request."""
        return self._request("POST", url, **kwargs)

    def _put(self, url: str, **kwargs: Any) -> Any:
        """Make a PUT request."""
        return self._request("PUT", url, **kwargs)

    def _patch(self, url: str, **kwargs: Any) -> Any:
        """Make a PATCH request."""
        return self._request("PATCH", url, **kwargs)

    def _delete(self, url: str, **kwargs: Any) -> Any:
        """Make a DELETE request."""
        return self._request("DELETE", url, **kwargs)

    def _compute_delay(self, cfg: RetryConfig, attempt: int) -> float:
        """Compute delay for exponential backoff with optional jitter."""
        delay = cfg.base_delay * (2 ** (attempt - 1))
        if delay > cfg.max_delay:
            delay = cfg.max_delay
        if cfg.jitter:
            delay *= random.uniform(0.5, 1.5)
        return delay

    def _retry_after_seconds(self, response: Any) -> Optional[float]:
        """Extract Retry-After header value in seconds."""
        header = response.headers.get("Retry-After")
        if not header:
            return None
        try:
            return float(header)
        except ValueError:
            try:
                dt = parsedate_to_datetime(header)
                return max(dt.timestamp() - time.time(), 0.0)
            except Exception:
                return None

    def _response_snippet(self, response: Any, limit: int = 200) -> str:
        """Extract a snippet of response text for error messages."""
        try:
            text = response.text or ""
        except Exception:
            text = ""
        return text[:limit]

    def _log_request(
        self, method: str, url: str, status: int, duration_ms: float
    ) -> None:
        """Log completed request."""
        logger.info(
            "%s.request method=%s url=%s status=%s duration_ms=%.1f",
            self.service_name,
            method,
            self._sanitize_url(url),
            status,
            duration_ms,
        )

    def _sanitize_url(self, url: str) -> str:
        """Remove query string from URL for logging."""
        return url.split("?")[0]

    def _sleep(self, delay: float) -> None:
        """Sleep for the specified delay. Override for testing."""
        time.sleep(delay)

    @contextlib.contextmanager
    def _timed_request(
        self, method: str, url: str
    ) -> Iterable[Callable[[], float]]:
        """Context manager to time a request."""
        start = time.time()

        def elapsed_ms() -> float:
            return (time.time() - start) * 1000

        yield elapsed_ms

    def close(self) -> None:
        """Close the HTTP session if we created it."""
        if self._session_created and self._session is not None:
            try:
                self._session.close()
            except Exception:
                pass
            self._session = None
            self._session_created = False

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

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