"""Core HTTP client — the backbone of the SDK."""

from __future__ import annotations

import time
from collections.abc import Iterator
from typing import Any, Protocol, TypeVar, runtime_checkable

import httpx

from ._exceptions import AplikuError, parse_error_response
from ._models import PaginatedResponse


@runtime_checkable
class _Validatable(Protocol):
    @classmethod
    def model_validate(cls, data: Any) -> Any: ...


T = TypeVar("T", bound=_Validatable)
BASE_URL = "https://api.appliku.com"
MAX_RETRIES = 3


class AplikuClient:
    """Low-level HTTP client. Handles auth, retries, error parsing."""

    def __init__(self, token: str) -> None:
        self._token = token
        self._session = httpx.Client(
            base_url=BASE_URL,
            timeout=30.0,
            headers={
                "Authorization": f"Token {token}",
                "Accept": "application/json",
            },
        )

    def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
        """Execute request with retry logic (429 + 5xx).

        Retry strategy: up to MAX_RETRIES attempts. On 429, respect Retry-After header.
        On 5xx, exponential backoff (1s, 2s, 4s). Raises on final failure.
        """
        last_exc: AplikuError | None = None

        for attempt in range(MAX_RETRIES):
            response = self._session.request(method, path, **kwargs)

            if response.status_code < 400:
                return response

            # Try to parse JSON body for error details
            try:
                body = response.json()
            except Exception:
                body = {"detail": response.text}

            exc = parse_error_response(response.status_code, body)

            if response.status_code == 429:
                retry_after = int(response.headers.get("retry-after", "1"))
                if attempt < MAX_RETRIES - 1:
                    time.sleep(retry_after)
                    last_exc = exc
                    continue
                raise exc

            if response.status_code >= 500:
                if attempt < MAX_RETRIES - 1:
                    time.sleep(2**attempt)  # 1s, 2s, 4s
                    last_exc = exc
                    continue
                raise exc

            # 4xx (except 429) — don't retry
            raise exc

        # Should not reach here, but just in case
        if last_exc:
            raise last_exc
        raise AplikuError("Request failed after retries")  # pragma: no cover

    def get(self, path: str, **kwargs: Any) -> dict[str, Any]:
        response = self._request("GET", path, **kwargs)
        return response.json()  # type: ignore[no-any-return]

    def post(self, path: str, **kwargs: Any) -> dict[str, Any]:
        response = self._request("POST", path, **kwargs)
        return response.json()  # type: ignore[no-any-return]

    def put(self, path: str, **kwargs: Any) -> dict[str, Any]:
        response = self._request("PUT", path, **kwargs)
        return response.json()  # type: ignore[no-any-return]

    def patch(self, path: str, **kwargs: Any) -> dict[str, Any]:
        response = self._request("PATCH", path, **kwargs)
        return response.json()  # type: ignore[no-any-return]

    def delete(self, path: str) -> None:
        self._request("DELETE", path)

    def paginate(
        self,
        path: str,
        params: dict[str, Any] | None = None,
        response_type: type[T] = ...,  # type: ignore[assignment]
    ) -> PaginatedResponse[T]:
        """Fetch one page, return PaginatedResponse with parsed models."""
        data = self.get(path, params=params)
        results = [response_type.model_validate(item) for item in data.get("results", [])]
        return PaginatedResponse(
            count=data.get("count", len(results)),
            next=data.get("next"),
            previous=data.get("previous"),
            results=results,
        )

    def paginate_all(
        self,
        path: str,
        params: dict[str, Any] | None = None,
        response_type: type[T] = ...,  # type: ignore[assignment]
    ) -> Iterator[T]:
        """Auto-follow pagination, yield individual items."""
        current_path: str | None = path
        current_params = params
        while current_path is not None:
            data = self.get(current_path, params=current_params)
            for item in data.get("results", []):
                yield response_type.model_validate(item)
            next_url = data.get("next")
            if next_url:
                # Next URL is absolute; strip base to get relative path + query
                current_path = next_url.replace(BASE_URL, "")
                current_params = None  # params are in the URL now
            else:
                current_path = None

    def close(self) -> None:
        self._session.close()
