from __future__ import annotations

import asyncio
import random
from dataclasses import dataclass
from typing import Any, Dict, Optional, Callable
import logging
from ._version import __version__

import httpx

from .errors import (
    ApiError,
    AuthenticationError,
    BillingError,
    NetworkError,
    RateLimitError,
    TimeoutError,
    ValidationError,
)

DEFAULT_BASE_URL = "https://api.lamen.dev"
DEFAULT_TIMEOUT_S = 30.0

def _redact_headers(headers: dict[str, str]) -> dict[str, str]:
    redacted = {}
    for k, v in headers.items():
        kl = k.lower()
        if kl in {"authorization", "x-api-key"}:
            redacted[k] = "***"
        else:
            redacted[k] = v
    return redacted


@dataclass(frozen=True)
class RetryConfig:
    """Retry policy for transient failures.

    Notes:
      - Retries are only safe for *idempotent* operations.
      - For POST requests, you should provide an idempotency key (either a field
        in the payload, or a header on endpoints that support it).
    """

    max_retries: int = 2
    base_backoff_s: float = 0.25
    max_backoff_s: float = 4.0
    jitter_s: float = 0.15


class HttpClient:
    """Low-level sync HTTP wrapper with retries and typed errors."""

    def __init__(
        self,
        *,
        api_key: Optional[str] = None,
        base_url: str = DEFAULT_BASE_URL,
        dashboard_token: Optional[str] = None,
        timeout_s: float = DEFAULT_TIMEOUT_S,
        user_agent: str = f"lamen-python/{__version__}",
        retry: RetryConfig | None = None,
        extra_headers: Optional[Dict[str, str]] = None,
        logger: logging.Logger | None = None,
        debug: bool = False,
    ) -> None:
        self._retry = retry or RetryConfig()
        self._logger = logger or logging.getLogger("lamen")
        self._debug = debug

        headers: Dict[str, str] = {
            "User-Agent": user_agent,
            "Accept": "application/json",
        }
        if api_key:
            headers["x-api-key"] = api_key
        if dashboard_token:
            headers["Authorization"] = f"Bearer {dashboard_token}"
        if extra_headers:
            headers.update({k: v for k, v in extra_headers.items() if v is not None})

        self._client = httpx.Client(
            base_url=base_url.rstrip("/"),
            headers=headers,
            timeout=httpx.Timeout(timeout_s),
        )
        
    def _has_idempotency_key(
        self,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> bool:
        # Header-based idempotency
        if headers:
            for k in ("Idempotency-Key", "X-Idempotency-Key", "x-idempotency-key"):
                v = headers.get(k)
                if v is not None and str(v).strip():
                    return True

        # Body-based idempotency (preferred in Lamen)
        if json and isinstance(json, dict):
            v = json.get("idempotency_key")
            if v is not None and str(v).strip():
                return True

        return False

    def _effective_attempts(
        self,
        *,
        method_u: str,
        json: Optional[Dict[str, Any]],
        headers: Optional[Dict[str, str]],
        cfg: RetryConfig,
    ) -> int:
        # Safe methods
        if method_u in {"GET", "DELETE"}:
            return 1 + max(0, int(cfg.max_retries))

        # Unsafe methods without idempotency → NEVER retry
        if method_u in {"POST", "PATCH", "PUT"} and not self._has_idempotency_key(
            json=json, headers=headers
        ):
            return 1

        # Unsafe but idempotent → retries allowed
        return 1 + max(0, int(cfg.max_retries))


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

    def _request_id(self, r: httpx.Response) -> Optional[str]:
        return r.headers.get("x-request-id") or r.headers.get("cf-ray")

    def _parse_json_best_effort(self, r: httpx.Response) -> Any:
        try:
            return r.json()
        except Exception:
            return None

    def _handle_response(self, *, method: str, path: str, r: httpx.Response) -> Any:
        request_id = self._request_id(r)

        if r.status_code in (401, 403):
            raise AuthenticationError(f"Authentication failed (status {r.status_code}).")

        if r.status_code == 402:
            data = self._parse_json_best_effort(r)
            msg = data.get("detail") if isinstance(data, dict) else None
            raise BillingError(msg or "Billing is required for this operation.")

        if r.status_code in (400, 422):
            data = self._parse_json_best_effort(r)
            msg = data.get("detail") if isinstance(data, dict) else None
            raise ValidationError(msg or "Invalid request.")

        if r.status_code == 429:
            raise RateLimitError("Rate limit exceeded.")

        if r.status_code >= 400:
            data = self._parse_json_best_effort(r)
            msg = None
            if isinstance(data, dict):
                msg = data.get("detail") or data.get("message")
            if not msg:
                msg = r.text.strip() or f"HTTP {r.status_code}"
            raise ApiError(
                status_code=r.status_code,
                message=str(msg),
                method=method.upper(),
                path=path,
                response_json=data if isinstance(data, dict) else None,
                request_id=request_id,
            )

        if r.status_code == 204:
            return None

        ctype = (r.headers.get("content-type") or "").lower()
        if "application/json" in ctype:
            return r.json() if r.content else None
        return r.text

    def _retry_after_seconds(self, r: httpx.Response) -> Optional[float]:
        ra = r.headers.get("retry-after")
        if not ra:
            return None
        try:
            return float(ra)
        except Exception:
            return None

    def _backoff(self, attempt_index: int) -> float:
        raw = self._retry.base_backoff_s * (2**attempt_index)
        raw = min(raw, self._retry.max_backoff_s)
        return raw + random.uniform(0.0, self._retry.jitter_s)

    def _should_retry_status(self, status_code: int) -> bool:
        if status_code in (408, 409, 425, 429):
            return True
        return 500 <= status_code <= 599

    def request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry: RetryConfig | None = None,
    ) -> Any:
        cfg = retry or self._retry
        method_u = method.upper()
        attempts = self._effective_attempts(
            method_u=method_u,
            json=json,
            headers=headers,
            cfg=cfg,
        )
        
        if self._debug:
            safe_headers = _redact_headers(dict(self._client.headers))
            self._logger.debug(
                "Lamen request: %s %s params=%s json=%s headers=%s",
                method_u,
                path,
                params,
                json,
                safe_headers,
            )

        for i in range(attempts):
            try:
                r = self._client.request(method_u, path, params=params, json=json, headers=headers)
            except httpx.TimeoutException as e:
                if i >= attempts - 1:
                    raise TimeoutError("Request timed out.") from e
                time_s = self._backoff(i)
                import time
                time.sleep(time_s)
                continue
            except httpx.RequestError as e:
                if i >= attempts - 1:
                    raise NetworkError("Network error while calling Lamen API.") from e
                time_s = self._backoff(i)
                import time
                time.sleep(time_s)
                continue

            if i < attempts - 1 and self._should_retry_status(r.status_code):
                ra = self._retry_after_seconds(r)
                time_s = ra if ra is not None else self._backoff(i)
                import time
                time.sleep(time_s)
                continue

            if self._debug:
                self._logger.debug(
                    "Lamen response: %s %s status=%s request_id=%s",
                    method_u,
                    path,
                    r.status_code,
                    self._request_id(r),
                )


            return self._handle_response(method=method_u, path=path, r=r)

        raise NetworkError("Unknown error while calling Lamen API.")

    def get(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
        return self.request("GET", path, params=params)

    def post(self, path: str, *, json: Optional[Dict[str, Any]] = None) -> Any:
        return self.request("POST", path, json=json)

    def patch(self, path: str, *, json: Optional[Dict[str, Any]] = None) -> Any:
        return self.request("PATCH", path, json=json)

    def delete(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
        return self.request("DELETE", path, params=params)


class AsyncHttpClient:
    """Low-level async HTTP wrapper with retries and typed errors."""

    def __init__(
        self,
        *,
        api_key: Optional[str] = None,
        base_url: str = DEFAULT_BASE_URL,
        dashboard_token: Optional[str] = None,
        timeout_s: float = DEFAULT_TIMEOUT_S,
        user_agent: str = f"lamen-python/{__version__}",
        retry: RetryConfig | None = None,
        extra_headers: Optional[Dict[str, str]] = None,
        logger: logging.Logger | None = None,
        debug: bool = False,
    ) -> None:
        self._retry = retry or RetryConfig()
        self._logger = logger or logging.getLogger("lamen")
        self._debug = debug

        headers: Dict[str, str] = {
            "User-Agent": user_agent,
            "Accept": "application/json",
        }
        if api_key:
            headers["x-api-key"] = api_key
        if dashboard_token:
            headers["Authorization"] = f"Bearer {dashboard_token}"
        if extra_headers:
            headers.update({k: v for k, v in extra_headers.items() if v is not None})

        self._client = httpx.AsyncClient(
            base_url=base_url.rstrip("/"),
            headers=headers,
            timeout=httpx.Timeout(timeout_s),
        )

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

    def _request_id(self, r: httpx.Response) -> Optional[str]:
        return r.headers.get("x-request-id") or r.headers.get("cf-ray")

    def _parse_json_best_effort(self, r: httpx.Response) -> Any:
        try:
            return r.json()
        except Exception:
            return None
        

    def _has_idempotency_key(
        self,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> bool:
        if headers:
            for k in ("Idempotency-Key", "X-Idempotency-Key", "x-idempotency-key"):
                v = headers.get(k)
                if v is not None and str(v).strip():
                    return True

        if json and isinstance(json, dict):
            v = json.get("idempotency_key")
            if v is not None and str(v).strip():
                return True

        return False


    def _effective_attempts(
        self,
        *,
        method_u: str,
        json: Optional[Dict[str, Any]],
        headers: Optional[Dict[str, str]],
        cfg: RetryConfig,
    ) -> int:
        if method_u in {"GET", "DELETE"}:
            return 1 + max(0, int(cfg.max_retries))

        if method_u in {"POST", "PATCH", "PUT"} and not self._has_idempotency_key(
            json=json, headers=headers
        ):
            return 1

        return 1 + max(0, int(cfg.max_retries))

    def _handle_response(self, *, method: str, path: str, r: httpx.Response) -> Any:
        request_id = self._request_id(r)

        if r.status_code in (401, 403):
            raise AuthenticationError(f"Authentication failed (status {r.status_code}).")

        if r.status_code == 402:
            data = self._parse_json_best_effort(r)
            msg = data.get("detail") if isinstance(data, dict) else None
            raise BillingError(msg or "Billing is required for this operation.")

        if r.status_code in (400, 422):
            data = self._parse_json_best_effort(r)
            msg = data.get("detail") if isinstance(data, dict) else None
            raise ValidationError(msg or "Invalid request.")

        if r.status_code == 429:
            raise RateLimitError("Rate limit exceeded.")

        if r.status_code >= 400:
            data = self._parse_json_best_effort(r)
            msg = None
            if isinstance(data, dict):
                msg = data.get("detail") or data.get("message")
            if not msg:
                msg = r.text.strip() or f"HTTP {r.status_code}"
            raise ApiError(
                status_code=r.status_code,
                message=str(msg),
                method=method.upper(),
                path=path,
                response_json=data if isinstance(data, dict) else None,
                request_id=request_id,
            )

        if r.status_code == 204:
            return None

        ctype = (r.headers.get("content-type") or "").lower()
        if "application/json" in ctype:
            return r.json() if r.content else None
        return r.text

    def _retry_after_seconds(self, r: httpx.Response) -> Optional[float]:
        ra = r.headers.get("retry-after")
        if not ra:
            return None
        try:
            return float(ra)
        except Exception:
            return None

    def _backoff(self, attempt_index: int) -> float:
        raw = self._retry.base_backoff_s * (2**attempt_index)
        raw = min(raw, self._retry.max_backoff_s)
        return raw + random.uniform(0.0, self._retry.jitter_s)

    def _should_retry_status(self, status_code: int) -> bool:
        if status_code in (408, 409, 425, 429):
            return True
        return 500 <= status_code <= 599

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry: RetryConfig | None = None,
    ) -> Any:
        cfg = retry or self._retry
        method_u = method.upper()
        attempts = self._effective_attempts(
            method_u=method_u,
            json=json,
            headers=headers,
            cfg=cfg,
        )

        for i in range(attempts):
            try:
                r = await self._client.request(method_u, path, params=params, json=json, headers=headers)
            except httpx.TimeoutException as e:
                if i >= attempts - 1:
                    raise TimeoutError("Request timed out.") from e
                await asyncio.sleep(self._backoff(i))
                continue
            except httpx.RequestError as e:
                if i >= attempts - 1:
                    raise NetworkError("Network error while calling Lamen API.") from e
                await asyncio.sleep(self._backoff(i))
                continue

            if i < attempts - 1 and self._should_retry_status(r.status_code):
                ra = self._retry_after_seconds(r)
                await asyncio.sleep(ra if ra is not None else self._backoff(i))
                continue

            return self._handle_response(method=method_u, path=path, r=r)

        raise NetworkError("Unknown error while calling Lamen API.")

    async def get(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
        return await self.request("GET", path, params=params)

    async def post(self, path: str, *, json: Optional[Dict[str, Any]] = None) -> Any:
        return await self.request("POST", path, json=json)

    async def patch(self, path: str, *, json: Optional[Dict[str, Any]] = None) -> Any:
        return await self.request("PATCH", path, json=json)

    async def delete(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
        return await self.request("DELETE", path, params=params)
