from __future__ import annotations

import os
import platform
import threading
import time
from http import HTTPStatus
from typing import cast

try:
    from importlib.metadata import version
except ImportError:  # pragma: no cover
    from pkg_resources import get_distribution

import requests

from descope.common import (
    DEFAULT_BASE_URL,
    DEFAULT_DOMAIN,
    DEFAULT_TIMEOUT_SECONDS,
    DEFAULT_URL_PREFIX,
)
from descope.exceptions import (
    API_RATE_LIMIT_RETRY_AFTER_HEADER,
    ERROR_TYPE_API_RATE_LIMIT,
    ERROR_TYPE_INVALID_ARGUMENT,
    ERROR_TYPE_SERVER_ERROR,
    AuthException,
    RateLimitException,
)


def sdk_version():
    try:
        return version("descope")  # type: ignore
    except NameError:  # pragma: no cover
        return get_distribution("descope").version  # type: ignore


# HTTP status codes that should trigger automatic retries
_RETRY_STATUS_CODES = {503, 521, 522, 524, 530}
# Delays in seconds between retries: first retry after 100ms, subsequent retries after 5s
_RETRY_DELAYS_SECONDS = [0.1, 5.0, 5.0]

_default_headers = {
    "Content-Type": "application/json",
    "x-descope-sdk-name": "python",
    "x-descope-sdk-python-version": platform.python_version(),
    "x-descope-sdk-version": sdk_version(),
}


class DescopeResponse:
    """
    Wrapper around requests.Response that provides dict-like access to JSON data
    while preserving access to HTTP metadata (headers, status_code, etc.).

    This allows backward compatibility (acting like a dict) while exposing
    HTTP metadata like cf-ray headers for debugging.
    """

    def __init__(self, response: requests.Response):
        self.raw = response
        self._json_data = None

    def json(self):
        """Get the parsed JSON response, cached after first access."""
        if self._json_data is None:
            self._json_data = self.raw.json()
        return self._json_data

    # Dict-like interface for backward compatibility
    def __getitem__(self, key):
        return self.json()[key]

    def __contains__(self, key):
        return key in self.json()

    def keys(self):
        return self.json().keys()

    def values(self):
        return self.json().values()

    def items(self):
        return self.json().items()

    def get(self, key, default=None):
        return self.json().get(key, default)

    def __str__(self):
        return str(self.json())

    def __repr__(self):
        return f"DescopeResponse({repr(self.json())})"

    def __bool__(self):
        return bool(self.json())

    def __len__(self):
        return len(self.json())

    def __eq__(self, other):
        if isinstance(other, DescopeResponse):
            return self.json() == other.json()
        return self.json() == other

    def __ne__(self, other):
        return not self.__eq__(other)

    def __iter__(self):
        return iter(self.json())

    # HTTP metadata properties
    @property
    def headers(self):
        """Access response headers (e.g., response.headers.get('cf-ray'))."""
        return self.raw.headers

    @property
    def status_code(self):
        """HTTP status code."""
        return self.raw.status_code

    @property
    def cookies(self):
        """Response cookies."""
        return self.raw.cookies

    @property
    def text(self):
        """Raw response text."""
        return self.raw.text

    @property
    def content(self):
        """Raw response content (bytes)."""
        return self.raw.content

    @property
    def url(self):
        """Request URL."""
        return self.raw.url

    @property
    def ok(self):
        """True if status code < 400."""
        return self.raw.ok


class HTTPClient:
    def __init__(
        self,
        project_id: str,
        base_url: str | None = None,
        *,
        timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
        secure: bool = True,
        management_key: str | None = None,
        verbose: bool = False,
    ) -> None:
        if not project_id:
            raise AuthException(
                400,
                ERROR_TYPE_INVALID_ARGUMENT,
                (
                    "Project ID is required to initialize HTTP client"
                    "Set environment variable DESCOPE_PROJECT_ID or pass your Project ID to the init function."
                ),
            )

        # Prefer explicitly provided base_url, then env var, then computed default
        env_base = os.getenv("DESCOPE_BASE_URI")
        self.base_url = base_url or env_base or self.base_url_for_project_id(project_id)

        self.project_id = project_id
        self.timeout_seconds = timeout_seconds
        self.secure = secure
        self.management_key = management_key
        self.verbose = verbose
        self._thread_local = threading.local()

    # ------------- public API -------------
    def get(
        self,
        uri: str,
        *,
        params=None,
        allow_redirects: bool | None = True,
        pswd: str | None = None,
    ) -> requests.Response:
        response = self._execute_with_retry(
            lambda: requests.get(
                f"{self.base_url}{uri}",
                headers=self._get_default_headers(pswd),
                params=params,
                allow_redirects=cast(bool, allow_redirects),
                verify=self.secure,
                timeout=self.timeout_seconds,
            )
        )
        if self.verbose:
            self._thread_local.last_response = DescopeResponse(response)
        self._raise_from_response(response)
        return response

    def post(
        self,
        uri: str,
        *,
        body: dict | list[dict] | list[str] | None = None,
        params=None,
        pswd: str | None = None,
        base_url: str | None = None,
    ) -> requests.Response:
        response = self._execute_with_retry(
            lambda: requests.post(
                f"{base_url or self.base_url}{uri}",
                headers=self._get_default_headers(pswd),
                json=body,
                allow_redirects=False,
                verify=self.secure,
                params=params,
                timeout=self.timeout_seconds,
            )
        )
        if self.verbose:
            self._thread_local.last_response = DescopeResponse(response)
        self._raise_from_response(response)
        return response

    def put(
        self,
        uri: str,
        *,
        body: dict | list[dict] | list[str] | None = None,
        params=None,
        pswd: str | None = None,
    ) -> requests.Response:
        response = self._execute_with_retry(
            lambda: requests.put(
                f"{self.base_url}{uri}",
                headers=self._get_default_headers(pswd),
                json=body,
                allow_redirects=False,
                verify=self.secure,
                params=params,
                timeout=self.timeout_seconds,
            )
        )
        self._raise_from_response(response)
        return response

    def patch(
        self,
        uri: str,
        *,
        body: dict | list[dict] | list[str] | None,
        params=None,
        pswd: str | None = None,
    ) -> requests.Response:
        response = self._execute_with_retry(
            lambda: requests.patch(
                f"{self.base_url}{uri}",
                headers=self._get_default_headers(pswd),
                json=body,
                allow_redirects=False,
                verify=self.secure,
                params=params,
                timeout=self.timeout_seconds,
            )
        )
        if self.verbose:
            self._thread_local.last_response = DescopeResponse(response)
        self._raise_from_response(response)
        return response

    def delete(
        self,
        uri: str,
        *,
        body: dict | list[dict] | list[str] | None = None,
        params=None,
        pswd: str | None = None,
    ) -> requests.Response:
        response = self._execute_with_retry(
            lambda: requests.delete(
                f"{self.base_url}{uri}",
                params=params,
                json=body,
                headers=self._get_default_headers(pswd),
                allow_redirects=False,
                verify=self.secure,
                timeout=self.timeout_seconds,
            )
        )
        if self.verbose:
            self._thread_local.last_response = DescopeResponse(response)
        self._raise_from_response(response)
        return response

    def get_last_response(self) -> DescopeResponse | None:
        """
        Get the last HTTP response when verbose mode is enabled.

        Useful for accessing HTTP metadata like headers (e.g., cf-ray),
        status codes, and raw response data for debugging.

        This method is thread-safe: each thread will receive its own
        last response when using a shared client instance.

        Returns:
            DescopeResponse: The last response if verbose mode is enabled, None otherwise.

        Example:
            client = DescopeClient(project_id, management_key, verbose=True)
            try:
                client.mgmt.user.create(login_id="u1")
            except AuthException:
                resp = client.get_last_response()
                if resp:
                    logger.error(f"cf-ray: {resp.headers.get('cf-ray')}")
        """
        return getattr(self._thread_local, "last_response", None)

    def get_default_headers(self, pswd: str | None = None) -> dict:
        return self._get_default_headers(pswd)

    # ------------- helpers -------------
    def _execute_with_retry(self, request_fn) -> requests.Response:
        """Execute request_fn and retry on retryable status codes.

        Retries up to 3 times: first retry after 100ms, subsequent retries after 5s each.
        The prior response is closed before each retry to release the connection back to
        the pool and avoid connection exhaustion under repeated transient errors.
        """
        response = request_fn()
        for delay in _RETRY_DELAYS_SECONDS:
            if response.status_code not in _RETRY_STATUS_CODES:
                break
            response.close()
            time.sleep(delay)
            response = request_fn()
        return response

    @staticmethod
    def base_url_for_project_id(project_id: str) -> str:
        if len(project_id) >= 32:
            region = project_id[1:5]
            return ".".join([DEFAULT_URL_PREFIX, region, DEFAULT_DOMAIN])
        return DEFAULT_BASE_URL

    def _parse_retry_after(self, headers) -> int:
        try:
            return int(headers.get(API_RATE_LIMIT_RETRY_AFTER_HEADER, 0))
        except (ValueError, TypeError):
            return 0

    def _raise_rate_limit_exception(self, response):
        try:
            resp = response.json()
            raise RateLimitException(
                resp.get("errorCode", HTTPStatus.TOO_MANY_REQUESTS),
                ERROR_TYPE_API_RATE_LIMIT,
                resp.get("errorDescription", ""),
                resp.get("errorMessage", ""),
                rate_limit_parameters={
                    API_RATE_LIMIT_RETRY_AFTER_HEADER: self._parse_retry_after(
                        response.headers
                    )
                },
            )
        except RateLimitException:
            raise
        except Exception:
            raise RateLimitException(
                status_code=HTTPStatus.TOO_MANY_REQUESTS,
                error_type=ERROR_TYPE_API_RATE_LIMIT,
                error_message=ERROR_TYPE_API_RATE_LIMIT,
                error_description=ERROR_TYPE_API_RATE_LIMIT,
            )

    def _raise_from_response(self, response):
        if response.ok:
            return
        if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
            self._raise_rate_limit_exception(response)
        raise AuthException(
            response.status_code,
            ERROR_TYPE_SERVER_ERROR,
            response.text,
        )

    def _get_default_headers(self, pswd: str | None = None):
        headers = _default_headers.copy()
        headers["x-descope-project-id"] = self.project_id
        bearer = self.project_id
        if pswd:
            bearer = f"{self.project_id}:{pswd}"
        if self.management_key:
            bearer = f"{bearer}:{self.management_key}"
        headers["Authorization"] = f"Bearer {bearer}"
        return headers
