from __future__ import annotations

import asyncio
import json
import logging
import time
from typing import Any

import httpx

from ._base_client import (
    BaseClient,
    RequestOptions,
    _calculate_backoff,
    _is_api_error_body,
    _is_retryable_status,
)
from ._errors import APIConnectionError, RateLimitError, ShipMailError, map_error_from_response
from .resources.domains import AsyncDomains, SyncDomains
from .resources.mailboxes import AsyncMailboxes, SyncMailboxes
from .resources.messages import AsyncMessages, SyncMessages
from .resources.status import AsyncStatus, SyncStatus
from .resources.suppressions import AsyncSuppressions, SyncSuppressions
from .resources.threads import AsyncThreads, SyncThreads
from .resources.webhooks import AsyncWebhooks, SyncWebhooks

logger = logging.getLogger("shipmail")


class ShipMail(BaseClient):
    domains: SyncDomains
    mailboxes: SyncMailboxes
    messages: SyncMessages
    suppressions: SyncSuppressions
    threads: SyncThreads
    webhooks: SyncWebhooks
    status: SyncStatus

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str | None = None,
        max_retries: int | None = None,
        timeout: float | None = None,
        default_headers: dict[str, str] | None = None,
        transport: httpx.BaseTransport | None = None,
    ) -> None:
        from ._base_client import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT

        super().__init__(
            api_key,
            base_url=base_url or DEFAULT_BASE_URL,
            max_retries=max_retries if max_retries is not None else DEFAULT_MAX_RETRIES,
            timeout=timeout if timeout is not None else DEFAULT_TIMEOUT,
            default_headers=default_headers,
        )
        self._http = httpx.Client(transport=transport) if transport else httpx.Client()
        self.domains = SyncDomains(self)
        self.mailboxes = SyncMailboxes(self)
        self.messages = SyncMessages(self)
        self.suppressions = SyncSuppressions(self)
        self.threads = SyncThreads(self)
        self.webhooks = SyncWebhooks(self)
        self.status = SyncStatus(self)

    def request(self, options: RequestOptions) -> Any:
        url = self._build_url(options.path, options.query)
        headers = self._build_headers(options.method_options)
        effective_timeout = self._get_timeout(options.method_options)
        last_error: ShipMailError | None = None

        for attempt in range(self._max_retries + 1):
            if attempt > 0:
                retry_after_s = (
                    last_error.retry_after
                    if isinstance(last_error, RateLimitError) and last_error.retry_after is not None
                    else None
                )
                delay = _calculate_backoff(attempt - 1, retry_after_s)
                logger.debug("Retry attempt %d for %s %s after %.1fs", attempt, options.method, options.path, delay)
                time.sleep(delay)

            try:
                request_headers = dict(headers)
                content: bytes | None = None
                json_body: Any = None

                if options.raw_body is not None:
                    request_headers["Content-Type"] = options.content_type or "application/octet-stream"
                    content = options.raw_body
                elif options.body is not None:
                    request_headers["Content-Type"] = "application/json"
                    json_body = options.body

                response = self._http.request(
                    options.method,
                    url,
                    headers=request_headers,
                    content=content,
                    json=json_body,
                    timeout=effective_timeout,
                )
            except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as exc:
                last_error = APIConnectionError(str(exc))
                if attempt < self._max_retries:
                    continue
                raise last_error from exc

            if response.status_code == 204:
                return None

            if response.is_success:
                return response.json()

            request_id = response.headers.get("x-request-id")

            try:
                error_body = response.json()
            except Exception:
                error_body = None

            if _is_api_error_body(error_body):
                last_error = map_error_from_response(response.status_code, error_body, request_id)
            else:
                last_error = ShipMailError(
                    f"Request failed with status {response.status_code}",
                    status=response.status_code,
                    request_id=request_id,
                    retryable=_is_retryable_status(response.status_code),
                )

            if not last_error.retryable or attempt >= self._max_retries:
                raise last_error

        raise last_error or ShipMailError("Request failed after retries")

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

    def __enter__(self) -> ShipMail:
        return self

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


class AsyncShipMail(BaseClient):
    domains: AsyncDomains
    mailboxes: AsyncMailboxes
    messages: AsyncMessages
    suppressions: AsyncSuppressions
    threads: AsyncThreads
    webhooks: AsyncWebhooks
    status: AsyncStatus

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str | None = None,
        max_retries: int | None = None,
        timeout: float | None = None,
        default_headers: dict[str, str] | None = None,
        transport: httpx.AsyncBaseTransport | None = None,
    ) -> None:
        from ._base_client import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT

        super().__init__(
            api_key,
            base_url=base_url or DEFAULT_BASE_URL,
            max_retries=max_retries if max_retries is not None else DEFAULT_MAX_RETRIES,
            timeout=timeout if timeout is not None else DEFAULT_TIMEOUT,
            default_headers=default_headers,
        )
        self._http = httpx.AsyncClient(transport=transport) if transport else httpx.AsyncClient()
        self.domains = AsyncDomains(self)
        self.mailboxes = AsyncMailboxes(self)
        self.messages = AsyncMessages(self)
        self.suppressions = AsyncSuppressions(self)
        self.threads = AsyncThreads(self)
        self.webhooks = AsyncWebhooks(self)
        self.status = AsyncStatus(self)

    async def request(self, options: RequestOptions) -> Any:
        url = self._build_url(options.path, options.query)
        headers = self._build_headers(options.method_options)
        effective_timeout = self._get_timeout(options.method_options)
        last_error: ShipMailError | None = None

        for attempt in range(self._max_retries + 1):
            if attempt > 0:
                retry_after_s = (
                    last_error.retry_after
                    if isinstance(last_error, RateLimitError) and last_error.retry_after is not None
                    else None
                )
                delay = _calculate_backoff(attempt - 1, retry_after_s)
                logger.debug("Retry attempt %d for %s %s after %.1fs", attempt, options.method, options.path, delay)
                await asyncio.sleep(delay)

            try:
                request_headers = dict(headers)
                content: bytes | None = None
                json_body: Any = None

                if options.raw_body is not None:
                    request_headers["Content-Type"] = options.content_type or "application/octet-stream"
                    content = options.raw_body
                elif options.body is not None:
                    request_headers["Content-Type"] = "application/json"
                    json_body = options.body

                response = await self._http.request(
                    options.method,
                    url,
                    headers=request_headers,
                    content=content,
                    json=json_body,
                    timeout=effective_timeout,
                )
            except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as exc:
                last_error = APIConnectionError(str(exc))
                if attempt < self._max_retries:
                    continue
                raise last_error from exc

            if response.status_code == 204:
                return None

            if response.is_success:
                return response.json()

            request_id = response.headers.get("x-request-id")

            try:
                error_body = response.json()
            except Exception:
                error_body = None

            if _is_api_error_body(error_body):
                last_error = map_error_from_response(response.status_code, error_body, request_id)
            else:
                last_error = ShipMailError(
                    f"Request failed with status {response.status_code}",
                    status=response.status_code,
                    request_id=request_id,
                    retryable=_is_retryable_status(response.status_code),
                )

            if not last_error.retryable or attempt >= self._max_retries:
                raise last_error

        raise last_error or ShipMailError("Request failed after retries")

    async def aclose(self) -> None:
        await self._http.aclose()

    async def __aenter__(self) -> AsyncShipMail:
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.aclose()
