"""Shared configuration and request utilities."""

from __future__ import annotations

import logging
import os

from typing import Any

from ._exceptions import (
    APIError,
    AuthenticationError,
    ConflictError,
    InfluxionError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)

DEFAULT_BASE_URL = "https://api.influxion.io/v1"

_logger = logging.getLogger("influxion")

# Global state set by init(). Clients may use these as defaults when not passed explicitly.
_global: dict[str, Any] = {
    "api_key": None,
    "base_url": None,
    "project_id": None,
    "agent_id": None,
}


def init(
    *,
    api_key: str | None = None,
    base_url: str | None = None,
    project_id: str | None = None,
    agent_id: str | None = None,
) -> None:
    """Set global defaults for the SDK.

    Values are used as fallbacks when creating clients or making calls that
    do not pass these options explicitly. Call with keyword arguments only;
    omitted keys are left unchanged (or unset if :func:`init` has not been
    called before).

    Args:
        api_key: API key for authentication.
        base_url: Base URL for the Influxion API.
        project_id: Default project ID.
        agent_id: Default agent ID.
    """
    if api_key is not None:
        _global["api_key"] = api_key
    if base_url is not None:
        _global["base_url"] = base_url
    if project_id is not None:
        _global["project_id"] = project_id
    if agent_id is not None:
        _global["agent_id"] = agent_id

    resolved_key = (
        _global["api_key"]
        if isinstance(_global.get("api_key"), str)
        else None
    ) or os.environ.get("INFLUXION_API_KEY")
    if not resolved_key:
        raise AuthenticationError(
            "No API key provided. Pass api_key= to init(), or set the INFLUXION_API_KEY"
            " environment variable."
        )


def resolve_project_and_agent(
    project_id: str | None,
    agent_id: str | None,
) -> tuple[str, str]:
    """Resolve project_id and agent_id from args or global config. Raises if either is missing."""
    resolved_project = project_id or _global.get("project_id")
    resolved_agent = agent_id or _global.get("agent_id")
    if not resolved_project:
        raise InfluxionError(
            "project_id is required. Pass project_id= or call influxion.init(project_id=...)."
        )
    if not resolved_agent:
        raise InfluxionError(
            "agent_id is required. Pass agent_id= or call influxion.init(agent_id=...)."
        )
    return (resolved_project, resolved_agent)


MAX_RETRIES = 3
_INITIAL_BACKOFF = 0.5
_MAX_BACKOFF = 8.0
_BACKOFF_MULTIPLIER = 2.0

# Status codes that warrant a retry attempt.
_RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}


class InfluxionConfig:
    """Resolved, immutable client configuration."""

    __slots__ = ("api_key", "base_url", "fail_silently")

    def __init__(self, api_key: str, base_url: str, fail_silently: bool) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.fail_silently = fail_silently


def build_config(
    api_key: str | None,
    base_url: str | None,
    fail_silently: bool,
) -> InfluxionConfig:
    """Resolve client configuration from arguments, :func:`init` global state, and environment.

    Resolution order for api_key and base_url: explicit argument → values set by
    :func:`init` → environment variables (``INFLUXION_API_KEY``, ``INFLUXION_BASE_URL``).

    Raises :exc:`~influxion.AuthenticationError` immediately if no API key is
    available, regardless of ``fail_silently``.
    """
    resolved_key = (
        api_key
        or (_global["api_key"] if isinstance(_global["api_key"], str) else None)
        or os.environ.get("INFLUXION_API_KEY")
    )
    if not resolved_key:
        raise AuthenticationError(
            "No API key provided. Pass api_key=, call influxion.init(api_key=...), or set the INFLUXION_API_KEY"
            " environment variable."
        )
    resolved_url = (
        base_url
        or (_global["base_url"] if isinstance(_global["base_url"], str) else None)
        or os.environ.get("INFLUXION_BASE_URL", DEFAULT_BASE_URL)
    )
    return InfluxionConfig(
        api_key=resolved_key,
        base_url=resolved_url,
        fail_silently=fail_silently,
    )


def auth_headers(api_key: str) -> dict[str, str]:
    return {"Authorization": f"Bearer {api_key}"}


def backoff_seconds(attempt: int) -> float:
    """Return the sleep duration for ``attempt`` (0-indexed): 0.5 s, 1 s, 2 s, …"""
    return min(_INITIAL_BACKOFF * (_BACKOFF_MULTIPLIER**attempt), _MAX_BACKOFF)


def should_retry_status(status_code: int) -> bool:
    return status_code in _RETRYABLE_STATUS_CODES


def map_http_error(status_code: int, detail: str) -> APIError:
    """Map an HTTP status code to the most specific :exc:`APIError` subclass."""
    if status_code == 429:
        return RateLimitError(detail, status_code=status_code)
    if status_code == 404:
        return NotFoundError(detail, status_code=status_code)
    if status_code == 409:
        return ConflictError(detail, status_code=status_code)
    if status_code == 422:
        return ValidationError(detail, status_code=status_code)
    return APIError(detail, status_code=status_code)
