"""Sigil API client."""

from __future__ import annotations

import os
import random
import time
from typing import Any, cast

import requests

from sigil.exceptions import SigilAuthError, SigilError, SigilNotFoundError
from sigil.models import Persona

DEFAULT_BASE_URL = "https://api.sigil.l8ntlabs.com"
DEFAULT_TIMEOUT = (10, 30)  # (connect, read) in seconds
DEFAULT_RETRY_COUNT = 3
DEFAULT_RETRY_BACKOFF = 0.5

# HTTP status codes that should trigger a retry
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}


class SigilClient:
    """
    Client for the Sigil API.

    Supports context manager usage for automatic session cleanup::

        with SigilClient(api_key="...", org_id="...") as client:
            persona = client.get("my-agent")

    Args:
        api_key: API key for authentication. Falls back to SIGIL_API_KEY env var.
        base_url: Base URL for the API. Falls back to SIGIL_BASE_URL env var,
                  then to http://localhost:8100.
        timeout: Request timeout as (connect, read) tuple or single value in seconds.
                 Defaults to (10, 30).
        retry_count: Number of retries for transient failures. Defaults to 3.
        retry_backoff: Base backoff delay in seconds (doubles each retry). Defaults to 0.5.

    Raises:
        SigilError: If no API key is provided or found in environment.
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: int | tuple[int, int] = DEFAULT_TIMEOUT,
        retry_count: int = DEFAULT_RETRY_COUNT,
        retry_backoff: float = DEFAULT_RETRY_BACKOFF,
    ) -> None:
        self.api_key = api_key or os.environ.get("SIGIL_API_KEY")
        if not self.api_key:
            raise SigilError("API key required. Pass api_key or set SIGIL_API_KEY.")

        self.base_url = (
            base_url
            or os.environ.get("SIGIL_BASE_URL")
            or DEFAULT_BASE_URL
        ).rstrip("/")

        self.timeout = timeout
        self.retry_count = retry_count
        self.retry_backoff = retry_backoff
        self._session = requests.Session()
        self._session.headers.update(
            {
                "Authorization": f"Bearer {self.api_key}",
                "Accept": "application/json",
            }
        )

    def close(self) -> None:
        """Close the underlying HTTP session."""
        self._session.close()

    def __enter__(self) -> "SigilClient":
        return self

    def __exit__(self, exc_type: type | None, exc_val: Exception | None, exc_tb: Any) -> bool:
        self.close()
        return False

    def get(self, name: str, version: str | None = None) -> Persona:
        """
        Fetch a persona by name.

        Args:
            name: The persona name (e.g., "staccato").
            version: Optional specific version (e.g., "1.0.0").
                     If not provided, fetches the latest version.

        Returns:
            A Persona object with inject() method.

        Raises:
            SigilNotFoundError: If the persona is not found.
            SigilAuthError: If authentication fails.
            SigilError: For other API errors.
        """
        if version:
            endpoint = f"/v1/personas/{name}/{version}"
        else:
            endpoint = f"/v1/personas/{name}"

        url = f"{self.base_url}{endpoint}"
        response = self._request("GET", url)
        return Persona.from_api_response(response)

    def _request(self, method: str, url: str, **kwargs: Any) -> dict[str, Any]:
        """Make an HTTP request with retry logic and handle errors."""
        last_exception: Exception | None = None

        for attempt in range(self.retry_count + 1):
            try:
                response = self._session.request(
                    method, url, timeout=self.timeout, **kwargs
                )

                # Check if we should retry based on status code
                if response.status_code in RETRYABLE_STATUS_CODES:
                    if attempt < self.retry_count:
                        self._sleep_with_backoff(attempt)
                        continue

                return self._handle_response(response)

            except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
                last_exception = e
                if attempt < self.retry_count:
                    self._sleep_with_backoff(attempt)
                    continue
                raise SigilError(f"Request failed: {e}") from e
            except requests.RequestException as e:
                # Non-retryable request exceptions
                raise SigilError(f"Request failed: {e}") from e

        # If we exhausted retries on a retryable status code
        if last_exception:
            raise SigilError(f"Request failed: {last_exception}") from last_exception
        return self._handle_response(response)

    def _sleep_with_backoff(self, attempt: int) -> None:
        """Sleep with exponential backoff and jitter."""
        base_delay = self.retry_backoff * (2 ** attempt)
        # Add jitter: random value between 0 and base_delay
        jitter = random.uniform(0, base_delay)
        time.sleep(base_delay + jitter)

    def _handle_response(self, response: requests.Response) -> dict[str, Any]:
        """Handle API response and raise appropriate errors."""
        if response.status_code == 401:
            raise SigilAuthError()

        if response.status_code == 404:
            raise SigilNotFoundError()

        if not response.ok:
            try:
                error_data = response.json()
                message = error_data.get("detail", response.text)
            except Exception:
                message = response.text or f"HTTP {response.status_code}"
            raise SigilError(message, status_code=response.status_code)

        try:
            return cast(dict[str, Any], response.json())
        except Exception as e:
            raise SigilError(f"Invalid JSON response: {e}") from e
