from __future__ import annotations

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

import httpx

from spherepay_mcp.domain.context import get_correlation_id
from spherepay_mcp.domain.value_objects import ApiError, ErrorCategory

logger = logging.getLogger(__name__)

_RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}


class ReliabilityPolicy:
    """Handles timeouts, retries with backoff, and error normalization."""

    def __init__(
        self,
        max_retries: int = 3,
        base_backoff: float = 1.0,
        timeout: float = 30.0,
    ):
        self.max_retries = max_retries
        self.base_backoff = base_backoff
        self.timeout = timeout

    async def execute(
        self,
        client: httpx.AsyncClient,
        method: str,
        url: str,
        *,
        json_body: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        last_error: Exception | None = None
        start = time.monotonic()

        for attempt in range(self.max_retries + 1):
            try:
                response = await client.request(
                    method,
                    url,
                    json=json_body,
                    params=params,
                    headers=headers,
                    timeout=self.timeout,
                )

                if response.status_code < 400:
                    elapsed_ms = round((time.monotonic() - start) * 1000)
                    logger.info(
                        "%s %s -> %d (%dms, %d attempt(s)) [%s]",
                        method,
                        url,
                        response.status_code,
                        elapsed_ms,
                        attempt + 1,
                        get_correlation_id(),
                    )
                    return response.json()

                if response.status_code in _RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    backoff = self.base_backoff * (2**attempt)
                    if response.status_code == 429:
                        retry_after = response.headers.get("retry-after")
                        if retry_after:
                            backoff = max(backoff, float(retry_after))
                    logger.warning(
                        "Retryable error %d on %s %s (attempt %d/%d), backing off %.1fs",
                        response.status_code,
                        method,
                        url,
                        attempt + 1,
                        self.max_retries,
                        backoff,
                    )
                    await asyncio.sleep(backoff)
                    continue

                raise self.normalize_error(response)

            except httpx.TimeoutException as e:
                last_error = e
                if attempt < self.max_retries:
                    backoff = self.base_backoff * (2**attempt)
                    logger.warning(
                        "Timeout on %s %s (attempt %d/%d), backing off %.1fs",
                        method,
                        url,
                        attempt + 1,
                        self.max_retries,
                        backoff,
                    )
                    await asyncio.sleep(backoff)
                    continue
                raise ApiError(
                    category=ErrorCategory.UPSTREAM_UNAVAILABLE,
                    message=f"Request timed out after {self.timeout}s: {e}",
                    status_code=504,
                ) from e
            except ApiError:
                raise
            except httpx.HTTPError as e:
                last_error = e
                if attempt < self.max_retries:
                    await asyncio.sleep(self.base_backoff * (2**attempt))
                    continue
                raise ApiError(
                    category=ErrorCategory.UPSTREAM_UNAVAILABLE,
                    message=f"HTTP error: {e}",
                    status_code=502,
                ) from e

        raise ApiError(
            category=ErrorCategory.UPSTREAM_UNAVAILABLE,
            message=f"Max retries exhausted: {last_error}",
            status_code=502,
        )

    @staticmethod
    def normalize_error(response: httpx.Response) -> ApiError:
        status = response.status_code
        try:
            body = response.json()
        except Exception:
            body = {"detail": response.text}

        detail = body.get("detail", body.get("message", "Unknown error"))
        correlation_id = body.get("correlationId")

        if status == 401 or status == 403:
            category = ErrorCategory.AUTH
        elif status == 404:
            category = ErrorCategory.NOT_FOUND
        elif status == 409:
            category = ErrorCategory.CONFLICT
        elif status == 422 or status == 400:
            category = ErrorCategory.VALIDATION
        elif status == 429:
            category = ErrorCategory.RATE_LIMIT
        elif status >= 500:
            category = ErrorCategory.UPSTREAM_UNAVAILABLE
        else:
            category = ErrorCategory.UNKNOWN

        return ApiError(
            category=category,
            message=detail,
            status_code=status,
            details=body.get("errors"),
            correlation_id=correlation_id,
        )


class IdempotencyStrategy:
    """Generates deterministic idempotency keys for mutating operations."""

    @staticmethod
    def generate_key(operation: str, params: dict[str, Any]) -> str:
        canonical = json.dumps(
            {"op": operation, **params},
            sort_keys=True,
            default=str,
        )
        return hashlib.sha256(canonical.encode()).hexdigest()[:32]
