"""
— HTTP Transport Layer
================================================
All network I/O lives here. No other module makes HTTP calls directly.

Responsibilities:
  - Attach auth headers
  - Retry on transient errors (network errors + 5xx) with exponential back-off
  - Map HTTP error codes to typed SDK exceptions
  - Log every request/response at DEBUG level for diagnostics

The Transport class is synchronous (uses `requests`). An async variant
(AsyncTransport using `aiohttp`) can be added later without changing any
other module.
"""

from __future__ import annotations

import json
import logging
import time
from typing import Any, Optional

import requests
import requests.adapters
import requests.exceptions

from .config import SDKConfig
from .exceptions import (
    AuthError, GovernanceError, NEESAPIError,
    RateLimitError, TimeoutError, ValidationError,
)

logger = logging.getLogger("nees_gp_sdk.transport")

# Status codes that are worth retrying (server-side transient errors)
_RETRYABLE_STATUS = {500, 502, 503, 504}


class Transport:
    """
    Synchronous HTTP transport with retry logic and structured error mapping.

    Usage (internal — callers use Client, not Transport directly):
        t = Transport(config)
        data = t.post("/v1/proxy/chat", json={"message": "..."})
        data = t.get("/health")
    """

    def __init__(self, config: SDKConfig) -> None:
        self._cfg     = config
        self._session = requests.Session()
        self._session.headers.update(config.default_headers)
        # Mount a single HTTPAdapter (no custom retry — we handle it ourselves
        # for better error type control)
        adapter = requests.adapters.HTTPAdapter(max_retries=0)
        self._session.mount("https://", adapter)
        self._session.mount("http://",  adapter)

    # ── Public methods ────────────────────────────────────────────────────────

    def get(self, path: str, params: Optional[dict] = None) -> dict[str, Any]:
        """Perform a GET request. Returns parsed JSON."""
        return self._request("GET", path, params=params)

    def post(self, path: str, json: Optional[dict] = None) -> dict[str, Any]:
        """Perform a POST request with a JSON body. Returns parsed JSON."""
        return self._request("POST", path, json=json)

    # ── Internal ──────────────────────────────────────────────────────────────

    def _request(
        self,
        method: str,
        path:   str,
        params: Optional[dict] = None,
        json:   Optional[dict] = None,
    ) -> dict[str, Any]:
        url          = self._cfg.url(path)
        last_exc: Optional[Exception] = None
        attempt_count = 0
        max_attempts  = 1 + self._cfg.max_retries

        while attempt_count < max_attempts:
            attempt_count += 1
            delay = self._cfg.retry_delay_s * (2 ** (attempt_count - 2)) if attempt_count > 1 else 0
            if delay > 0:
                logger.debug(
                    "Transport: retry %d/%d in %.1fs | %s %s",
                    attempt_count - 1, self._cfg.max_retries, delay, method, path,
                )
                time.sleep(delay)

            t0 = time.perf_counter()
            try:
                resp = self._session.request(
                    method=method,
                    url=url,
                    params=params,
                    json=json,
                    timeout=self._cfg.timeout,
                    verify=self._cfg.verify_ssl,
                )
                elapsed_ms = (time.perf_counter() - t0) * 1000
                logger.debug(
                    "Transport: %s %s → %d (%.0fms)",
                    method, path, resp.status_code, elapsed_ms,
                )

            except requests.exceptions.Timeout as exc:
                last_exc = TimeoutError(url, self._cfg.timeout)
                logger.warning("Transport: timeout on %s %s", method, url)
                # Timeouts are always retried
                continue

            except requests.exceptions.ConnectionError as exc:
                last_exc = NEESAPIError(0, f"Connection error: {exc}")
                logger.warning("Transport: connection error on %s %s: %s", method, url, exc)
                continue

            # ── Map status codes to exceptions ────────────────────────────
            try:
                body = resp.json()
            except Exception:
                body = {"detail": resp.text[:500]}

            if resp.status_code == 200:
                return body

            if resp.status_code == 401:
                raise AuthError()

            if resp.status_code == 422:
                errors = body.get("detail", [])
                raise ValidationError(errors if isinstance(errors, list) else [errors])

            if resp.status_code == 429:
                retry_after = float(resp.headers.get("Retry-After", 0))
                raise RateLimitError(retry_after)

            if resp.status_code == 503:
                detail = str(body.get("detail", "Governance pipeline unavailable."))
                if attempt_count < max_attempts:
                    last_exc = GovernanceError(detail)
                    continue
                raise GovernanceError(detail)

            if resp.status_code in _RETRYABLE_STATUS:
                last_exc = NEESAPIError(resp.status_code, str(body))
                continue

            # Non-retryable 4xx/5xx
            raise NEESAPIError(resp.status_code, str(body))

        # All retries exhausted
        if last_exc is not None:
            raise last_exc
        raise NEESAPIError(0, "All retries exhausted with no response.")

    def close(self) -> None:
        """Close the underlying requests.Session and release connections."""
        self._session.close()