"""Core HTTP transport, pagination, and base resource classes (sync and async)."""

import asyncio
import time
from collections.abc import AsyncIterator, Iterator
from typing import Any, TypeVar, overload

import httpx
from pydantic import BaseModel

from ._exceptions import RateLimitError, raise_for_response
from .models import PaginationMeta

T = TypeVar("T", bound=BaseModel)

API_VERSION = "v2"
DEFAULT_BASE_URL = "https://api.duffel.com"
CARDS_BASE_URL = "https://api.duffel.cards"
DEFAULT_TIMEOUT = 30.0
LONG_TIMEOUT = 130.0


class HttpTransport:
    """Thin wrapper around httpx that handles auth, headers, envelope, and rate limiting."""

    def __init__(
        self,
        token: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        http_client: httpx.Client | None = None,
    ):
        self.base_url = base_url.rstrip("/")
        self.token = token
        self.timeout = timeout
        self._client = http_client or httpx.Client(timeout=timeout)
        self._default_headers = {
            "Authorization": f"Bearer {token}",
            "Duffel-Version": API_VERSION,
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Accept-Encoding": "gzip",
        }

    def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        base_url: str | None = None,
        timeout: float | None = None,
    ) -> httpx.Response:
        """Make an HTTP request with rate-limit retry (once)."""
        url = f"{base_url or self.base_url}{path}"

        # Clean None values from params
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        request_kwargs: dict[str, Any] = {
            "method": method,
            "url": url,
            "headers": self._default_headers,
            "params": params or None,
            "timeout": timeout or self.timeout,
        }
        if json_body is not None:
            request_kwargs["json"] = json_body

        resp = self._client.request(**request_kwargs)

        # Rate limit retry: wait and retry once
        if resp.status_code == 429:
            reset = resp.headers.get("ratelimit-reset")
            if reset:
                try:
                    # ratelimit-reset is an HTTP-date; parse seconds to wait
                    from email.utils import parsedate_to_datetime
                    reset_dt = parsedate_to_datetime(reset)
                    wait = max(0, (reset_dt.timestamp() - time.time())) + 0.5
                    if wait <= 65:  # only auto-retry if wait is reasonable
                        time.sleep(wait)
                        resp = self._client.request(**request_kwargs)
                except (ValueError, TypeError):
                    pass

        return resp

    def close(self):
        self._client.close()


def unwrap_response(resp: httpx.Response) -> dict[str, Any]:
    """Check status, parse JSON, and return the raw dict."""
    if resp.status_code == 204:
        return {}

    body = resp.json() if resp.content else {}

    if resp.status_code >= 400:
        raise_for_response(resp.status_code, body, dict(resp.headers))

    return body


def unwrap_data(resp: httpx.Response, model: type[T]) -> T:
    """Unwrap the {"data": ...} envelope and parse into a model instance."""
    body = unwrap_response(resp)
    return model.model_validate(body["data"])


def unwrap_list(resp: httpx.Response, model: type[T]) -> tuple[list[T], PaginationMeta | None]:
    """Unwrap a paginated {"data": [...], "meta": {...}} response."""
    body = unwrap_response(resp)
    items = [model.model_validate(item) for item in body.get("data", [])]
    meta = PaginationMeta.model_validate(body["meta"]) if "meta" in body else None
    return items, meta


class PageIterator(Iterator[T]):
    """Lazy iterator over paginated API results.

    Fetches pages on demand. Can also be used to get a single page via `.page()`.
    """

    def __init__(
        self,
        transport: HttpTransport,
        path: str,
        model: type[T],
        params: dict[str, Any] | None = None,
    ):
        self._transport = transport
        self._path = path
        self._model = model
        self._params = dict(params or {})
        self._buffer: list[T] = []
        self._cursor: str | None = ""  # empty string = not yet fetched; None = exhausted
        self._index = 0

    def __iter__(self) -> "PageIterator[T]":
        return self

    def __next__(self) -> T:
        if self._index >= len(self._buffer):
            if self._cursor is None:
                raise StopIteration
            self._fetch_next_page()
            if not self._buffer:
                raise StopIteration
        item = self._buffer[self._index]
        self._index += 1
        return item

    def _fetch_next_page(self) -> None:
        params = dict(self._params)
        if self._cursor:  # non-empty string = we have a cursor from previous page
            params["after"] = self._cursor

        resp = self._transport.request("GET", self._path, params=params)
        items, meta = unwrap_list(resp, self._model)

        self._buffer = items
        self._index = 0
        self._cursor = meta.after if meta else None

    def page(self) -> list[T]:
        """Fetch and return the current/next page as a list."""
        if self._cursor is None:
            return []
        self._fetch_next_page()
        return list(self._buffer)


class BaseResource:
    """Base class for API resource groups. Provides common CRUD helpers."""

    _path: str  # e.g. "/air/offer_requests"

    def __init__(self, transport: HttpTransport):
        self._transport = transport

    def _get(self, id: str, model: type[T], *, params: dict[str, Any] | None = None) -> T:
        resp = self._transport.request("GET", f"{self._path}/{id}", params=params)
        return unwrap_data(resp, model)

    def _list(
        self,
        model: type[T],
        *,
        params: dict[str, Any] | None = None,
    ) -> PageIterator[T]:
        return PageIterator(self._transport, self._path, model, params=params)

    def _create(
        self,
        model: type[T],
        data: dict[str, Any],
        *,
        params: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> T:
        resp = self._transport.request(
            "POST", self._path, json_body={"data": data}, params=params, timeout=timeout,
        )
        return unwrap_data(resp, model)

    def _update(
        self,
        id: str,
        model: type[T],
        data: dict[str, Any],
        *,
        method: str = "PATCH",
    ) -> T:
        resp = self._transport.request(method, f"{self._path}/{id}", json_body={"data": data})
        return unwrap_data(resp, model)

    def _action(
        self,
        id: str,
        action: str,
        model: type[T],
        *,
        data: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> T:
        json_body = {"data": data} if data else None
        resp = self._transport.request(
            "POST", f"{self._path}/{id}/actions/{action}",
            json_body=json_body, timeout=timeout,
        )
        return unwrap_data(resp, model)

    def _delete(self, id: str) -> None:
        resp = self._transport.request("DELETE", f"{self._path}/{id}")
        unwrap_response(resp)


# ---------------------------------------------------------------------------
# Async variants
# ---------------------------------------------------------------------------


class AsyncHttpTransport:
    """Async wrapper around httpx that handles auth, headers, envelope, and rate limiting."""

    def __init__(
        self,
        token: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        http_client: httpx.AsyncClient | None = None,
    ):
        self.base_url = base_url.rstrip("/")
        self.token = token
        self.timeout = timeout
        self._client = http_client or httpx.AsyncClient(timeout=timeout)
        self._default_headers = {
            "Authorization": f"Bearer {token}",
            "Duffel-Version": API_VERSION,
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Accept-Encoding": "gzip",
        }

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        base_url: str | None = None,
        timeout: float | None = None,
    ) -> httpx.Response:
        """Make an async HTTP request with rate-limit retry (once)."""
        url = f"{base_url or self.base_url}{path}"

        if params:
            params = {k: v for k, v in params.items() if v is not None}

        request_kwargs: dict[str, Any] = {
            "method": method,
            "url": url,
            "headers": self._default_headers,
            "params": params or None,
            "timeout": timeout or self.timeout,
        }
        if json_body is not None:
            request_kwargs["json"] = json_body

        resp = await self._client.request(**request_kwargs)

        if resp.status_code == 429:
            reset = resp.headers.get("ratelimit-reset")
            if reset:
                try:
                    from email.utils import parsedate_to_datetime
                    reset_dt = parsedate_to_datetime(reset)
                    wait = max(0, (reset_dt.timestamp() - time.time())) + 0.5
                    if wait <= 65:
                        await asyncio.sleep(wait)
                        resp = await self._client.request(**request_kwargs)
                except (ValueError, TypeError):
                    pass

        return resp

    async def close(self):
        await self._client.aclose()


class AsyncPageIterator(AsyncIterator[T]):
    """Async lazy iterator over paginated API results."""

    def __init__(
        self,
        transport: AsyncHttpTransport,
        path: str,
        model: type[T],
        params: dict[str, Any] | None = None,
    ):
        self._transport = transport
        self._path = path
        self._model = model
        self._params = dict(params or {})
        self._buffer: list[T] = []
        self._cursor: str | None = ""
        self._index = 0

    def __aiter__(self) -> "AsyncPageIterator[T]":
        return self

    async def __anext__(self) -> T:
        if self._index >= len(self._buffer):
            if self._cursor is None:
                raise StopAsyncIteration
            await self._fetch_next_page()
            if not self._buffer:
                raise StopAsyncIteration
        item = self._buffer[self._index]
        self._index += 1
        return item

    async def _fetch_next_page(self) -> None:
        params = dict(self._params)
        if self._cursor:
            params["after"] = self._cursor

        resp = await self._transport.request("GET", self._path, params=params)
        items, meta = unwrap_list(resp, self._model)

        self._buffer = items
        self._index = 0
        self._cursor = meta.after if meta else None

    async def page(self) -> list[T]:
        """Fetch and return the current/next page as a list."""
        if self._cursor is None:
            return []
        await self._fetch_next_page()
        return list(self._buffer)


class AsyncBaseResource:
    """Async base class for API resource groups."""

    _path: str

    def __init__(self, transport: AsyncHttpTransport):
        self._transport = transport

    async def _get(self, id: str, model: type[T], *, params: dict[str, Any] | None = None) -> T:
        resp = await self._transport.request("GET", f"{self._path}/{id}", params=params)
        return unwrap_data(resp, model)

    def _list(
        self,
        model: type[T],
        *,
        params: dict[str, Any] | None = None,
    ) -> "AsyncPageIterator[T]":
        return AsyncPageIterator(self._transport, self._path, model, params=params)

    async def _create(
        self,
        model: type[T],
        data: dict[str, Any],
        *,
        params: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> T:
        resp = await self._transport.request(
            "POST", self._path, json_body={"data": data}, params=params, timeout=timeout,
        )
        return unwrap_data(resp, model)

    async def _update(
        self,
        id: str,
        model: type[T],
        data: dict[str, Any],
        *,
        method: str = "PATCH",
    ) -> T:
        resp = await self._transport.request(method, f"{self._path}/{id}", json_body={"data": data})
        return unwrap_data(resp, model)

    async def _action(
        self,
        id: str,
        action: str,
        model: type[T],
        *,
        data: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> T:
        json_body = {"data": data} if data else None
        resp = await self._transport.request(
            "POST", f"{self._path}/{id}/actions/{action}",
            json_body=json_body, timeout=timeout,
        )
        return unwrap_data(resp, model)

    async def _delete(self, id: str) -> None:
        resp = await self._transport.request("DELETE", f"{self._path}/{id}")
        unwrap_response(resp)
