from __future__ import annotations

import os
import random
import time
import uuid
from typing import Any, Dict, Iterator, Optional

import requests

from .errors import (
    AuthenticationError,
    ForbiddenError,
    NetworkError,
    NotFoundError,
    OptropicError,
    QuotaExceededError,
    RateLimitError,
    ServiceUnavailableError,
    TimeoutError as OptropicTimeoutError,
    ValidationError,
    create_error_from_response,
)


# ═══════════════════════════════════════════════════════════════════════════
# CONSTANTS
# ═══════════════════════════════════════════════════════════════════════════

_DEFAULT_BASE_URL = "https://api.optropic.com"
_DEFAULT_TIMEOUT = 30.0  # seconds
_DEFAULT_MAX_RETRIES = 3
_DEFAULT_BASE_DELAY = 1.0  # seconds
_DEFAULT_MAX_DELAY = 10.0  # seconds


# ═══════════════════════════════════════════════════════════════════════════
# OPTROPIC CLIENT
# ═══════════════════════════════════════════════════════════════════════════

class Optropic:
    """Client for the Optropic Trust API.

    Usage::

        from optropic import Optropic

        # Auto-detects sandbox mode from key prefix
        client = Optropic(api_key="optr_test_...")
        client.is_sandbox  # True

        client = Optropic(api_key="optr_live_...")
        client.is_sandbox  # False

    Retry behaviour::

        # Customize retry logic
        client = Optropic(
            api_key="optr_live_...",
            max_retries=5,
            base_delay=0.5,
            max_delay=30.0,
        )
    """

    # API key prefixes that indicate sandbox/test mode
    _SANDBOX_PREFIXES = ("optr_test_",)
    _LIVE_PREFIXES = ("optr_live_",)

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = _DEFAULT_BASE_URL,
        sandbox: Optional[bool] = None,
        timeout: float = _DEFAULT_TIMEOUT,
        max_retries: int = _DEFAULT_MAX_RETRIES,
        base_delay: float = _DEFAULT_BASE_DELAY,
        max_delay: float = _DEFAULT_MAX_DELAY,
    ) -> None:
        api_key = api_key or os.environ.get("OPTROPIC_API_KEY")
        if not api_key:
            raise ValueError(
                "api_key is required — pass it directly or set the "
                "OPTROPIC_API_KEY environment variable"
            )

        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._max_retries = max_retries
        self._base_delay = base_delay
        self._max_delay = max_delay

        # Auto-detect sandbox mode from API key prefix unless explicitly set
        if sandbox is not None:
            self._sandbox = sandbox
        else:
            self._sandbox = any(
                api_key.startswith(p) for p in self._SANDBOX_PREFIXES
            )

        self._session = requests.Session()
        self._session.headers.update(
            {
                "X-API-Key": self._api_key,
                "Content-Type": "application/json",
                "Accept": "application/json",
                "X-SDK-Version": "2.1.0",
                "X-SDK-Language": "python",
            }
        )

        # Lazy-initialised resource namespaces
        self._assets: Optional[Any] = None
        self._compliance: Optional[Any] = None
        self._webhooks: Optional[Any] = None
        self._audit: Optional[Any] = None
        self._keys: Optional[Any] = None
        self._schemas: Optional[Any] = None
        self._documents: Optional[Any] = None
        self._provenance: Optional[Any] = None

    # ------------------------------------------------------------------
    # Environment introspection
    # ------------------------------------------------------------------

    @property
    def is_sandbox(self) -> bool:
        """``True`` when the client is using a sandbox/test API key."""
        return self._sandbox

    @property
    def is_live(self) -> bool:
        """``True`` when the client is using a live/production API key."""
        return not self._sandbox

    @property
    def environment(self) -> str:
        """Return ``'sandbox'`` or ``'live'`` based on the API key."""
        return "sandbox" if self._sandbox else "live"

    # ------------------------------------------------------------------
    # Resource properties (lazy)
    # ------------------------------------------------------------------

    @property
    def assets(self) -> Any:
        if self._assets is None:
            from .resources.assets import Assets

            self._assets = Assets(self)
        return self._assets

    @property
    def compliance(self) -> Any:
        if self._compliance is None:
            from .resources.compliance import Compliance

            self._compliance = Compliance(self)
        return self._compliance

    @property
    def webhooks(self) -> Any:
        if self._webhooks is None:
            from .resources.webhooks import Webhooks

            self._webhooks = Webhooks(self)
        return self._webhooks

    @property
    def audit(self) -> Any:
        if self._audit is None:
            from .resources.audit import Audit

            self._audit = Audit(self)
        return self._audit

    @property
    def keys(self) -> Any:
        if self._keys is None:
            from .resources.keys import Keys

            self._keys = Keys(self)
        return self._keys

    @property
    def schemas(self) -> Any:
        if self._schemas is None:
            from .resources.schemas import Schemas

            self._schemas = Schemas(self)
        return self._schemas

    @property
    def documents(self) -> Any:
        if self._documents is None:
            from .resources.documents import Documents

            self._documents = Documents(self)
        return self._documents

    @property
    def provenance(self) -> Any:
        if self._provenance is None:
            from .resources.provenance import Provenance

            self._provenance = Provenance(self)
        return self._provenance

    # ------------------------------------------------------------------
    # Internal HTTP helpers
    # ------------------------------------------------------------------

    def _request(
        self,
        method: str,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        idempotency_key: Optional[str] = None,
        timeout: Optional[float] = None,
    ) -> Any:
        """Send a request to the Optropic API and return the parsed JSON body.

        Retries on transient failures with exponential backoff + jitter.
        Raises a typed :class:`OptropicError` subclass on non-2xx responses.
        """
        url = f"{self._base_url}/v1{path}"
        effective_timeout = timeout or self._timeout

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

        # Strip None values from JSON body
        if json:
            json = {k: v for k, v in json.items() if v is not None}

        # Build per-request headers
        extra_headers: Dict[str, str] = {}
        if idempotency_key:
            extra_headers["Idempotency-Key"] = idempotency_key
        elif method in ("POST", "PUT", "PATCH"):
            # Auto-generate idempotency key for mutations
            extra_headers["Idempotency-Key"] = str(uuid.uuid4())

        last_error: Optional[Exception] = None
        attempt = 0

        while attempt <= self._max_retries:
            try:
                response = self._session.request(
                    method,
                    url,
                    json=json,
                    params=params,
                    timeout=effective_timeout,
                    headers=extra_headers,
                )

                if response.ok:
                    if response.status_code == 204:
                        return {}
                    return response.json()

                # Parse error
                error = self._parse_error(response)
                last_error = error

                # Don't retry non-retryable errors
                if not error.retryable:
                    raise error

                # Exhausted retries?
                if attempt >= self._max_retries:
                    raise error

                # Rate limit — use server's retry-after
                if isinstance(error, RateLimitError) and error.retry_after:
                    time.sleep(error.retry_after)
                else:
                    # Exponential backoff with jitter
                    delay = min(
                        self._base_delay * (2 ** attempt),
                        self._max_delay,
                    )
                    jitter = delay * 0.5 * random.random()
                    time.sleep(delay + jitter)

                attempt += 1

            except requests.exceptions.Timeout:
                last_error = OptropicTimeoutError(int(effective_timeout * 1000))
                if attempt >= self._max_retries:
                    raise last_error from None
                delay = min(self._base_delay * (2 ** attempt), self._max_delay)
                jitter = delay * 0.5 * random.random()
                time.sleep(delay + jitter)
                attempt += 1

            except requests.exceptions.ConnectionError as exc:
                last_error = NetworkError(str(exc))
                if attempt >= self._max_retries:
                    raise last_error from exc
                delay = min(self._base_delay * (2 ** attempt), self._max_delay)
                jitter = delay * 0.5 * random.random()
                time.sleep(delay + jitter)
                attempt += 1

            except OptropicError:
                raise

            except Exception as exc:
                raise OptropicError(
                    message=str(exc), code="UNKNOWN_ERROR"
                ) from exc

        raise last_error or OptropicError(message="Request failed", code="UNKNOWN_ERROR")

    def _request_raw(
        self,
        method: str,
        path: str,
        *,
        timeout: Optional[float] = None,
    ) -> requests.Response:
        """Send a request and return the raw response (for binary endpoints)."""
        url = f"{self._base_url}/v1{path}"
        effective_timeout = timeout or self._timeout

        response = self._session.request(
            method,
            url,
            headers={"Accept": "application/octet-stream"},
            timeout=effective_timeout,
        )
        if not response.ok:
            raise self._parse_error(response)
        return response

    def _parse_error(self, response: requests.Response) -> OptropicError:
        """Map an HTTP error response to a typed SDK exception."""
        try:
            body = response.json()
        except Exception:
            body = {}

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

        # Normalise the body into the shape create_error_from_response expects
        error_body: Dict[str, Any] = {}

        if isinstance(body, dict):
            error_obj = body.get("error", body)
            if isinstance(error_obj, dict):
                error_body["code"] = error_obj.get("code", "UNKNOWN_ERROR")
                error_body["message"] = error_obj.get(
                    "message", response.text
                )
                error_body["details"] = error_obj.get("details")
            else:
                error_body["code"] = "UNKNOWN_ERROR"
                error_body["message"] = body.get("message", response.text)
        else:
            error_body["code"] = "UNKNOWN_ERROR"
            error_body["message"] = response.text

        if request_id:
            error_body["requestId"] = request_id

        return create_error_from_response(response.status_code, error_body)

    # ------------------------------------------------------------------
    # Pagination helper
    # ------------------------------------------------------------------

    def _paginate(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        page_size: int = 100,
    ) -> Iterator[Any]:
        """Auto-paginate through a list endpoint, yielding individual items.

        Example::

            for asset in client._paginate("GET", "/assets", page_size=50):
                print(asset["id"])
        """
        page = 1
        query = dict(params or {})

        while True:
            query["page"] = page
            query["per_page"] = page_size

            response = self._request(method, path, params=query)

            # The API returns { data: [...], pagination: { ... } }
            data = response.get("data", [])
            yield from data

            pagination = response.get("pagination", {})
            total_pages = pagination.get("totalPages", 1)

            if page >= total_pages or not data:
                break

            page += 1
