"""Synchronous Influxion client."""

from __future__ import annotations

import time
from typing import Any
from uuid import UUID

import httpx

from ._base import (
    MAX_RETRIES,
    InfluxionConfig,
    _logger,
    auth_headers,
    backoff_seconds,
    build_config,
    map_http_error,
    should_retry_status,
)
from ._exceptions import AuthenticationError, InfluxionError, NetworkError


class InfluxionClient:
    """Synchronous Influxion SDK client.

    Example::

        client = InfluxionClient(api_key="sk-...")

        session_id = client.create_session(
            name="weekly-report-run",
            project_id="<uuid>",
            agent_id="report-agent",
        )

        client.create_session_artifact(
            session_id=session_id,
            artifact_name="report",
            artifact="The quarterly results show...",
        )

        client.end_session(session_id, success=True)

    Use as a context manager to ensure the underlying HTTP client is closed::

        with InfluxionClient(api_key="sk-...") as client:
            ...
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        fail_silently: bool = True,
    ) -> None:
        """
        Args:
            api_key: API key for authentication. Falls back to the
                ``INFLUXION_API_KEY`` environment variable. Raises
                :exc:`~influxion.AuthenticationError` immediately if neither
                is set.
            base_url: Base URL for the Influxion API. Falls back to the
                ``INFLUXION_BASE_URL`` environment variable, then
                ``https://api.influxion.io/v1``.
            fail_silently: If ``True`` (default), call-time errors are logged
                and methods return ``None`` instead of raising. Set to
                ``False`` during development to surface errors immediately.
                :exc:`~influxion.AuthenticationError` at construction always
                raises regardless of this setting.
        """
        self._config: InfluxionConfig = build_config(api_key, base_url, fail_silently)
        self._http = httpx.Client(
            headers=auth_headers(self._config.api_key),
            timeout=30.0,
        )

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def close(self) -> None:
        """Close the underlying HTTP client."""
        self._http.close()

    def __enter__(self) -> InfluxionClient:
        return self

    def __exit__(self, *args: Any) -> bool:
        self.close()
        return False

    # ------------------------------------------------------------------
    # Internal request machinery
    # ------------------------------------------------------------------

    def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        """Execute an HTTP request with retry/backoff logic.

        Returns the parsed JSON body on success, or ``None`` for empty
        responses (e.g. 204). Raises on unrecoverable errors.
        """
        url = f"{self._config.base_url}{path}"
        last_exc: Exception | None = None

        for attempt in range(MAX_RETRIES):
            try:
                response = self._http.request(method, url, **kwargs)

                if response.is_success:
                    return response.json() if response.content else None

                try:
                    detail = response.json().get("detail", response.text)
                except Exception:
                    detail = response.text or str(response.status_code)

                if response.status_code == 401:
                    raise AuthenticationError(f"Authentication failed: {detail}")

                error = map_http_error(response.status_code, str(detail))
                if (
                    should_retry_status(response.status_code)
                    and attempt < MAX_RETRIES - 1
                ):
                    last_exc = error
                    time.sleep(backoff_seconds(attempt))
                    continue
                raise error

            except (AuthenticationError, InfluxionError):
                raise
            except httpx.TransportError as exc:
                last_exc = NetworkError(f"Network error: {exc}")
                if attempt < MAX_RETRIES - 1:
                    time.sleep(backoff_seconds(attempt))
                    continue
                raise last_exc from exc

        # Reached only when all retries consumed via `continue` on the last attempt,
        # which cannot happen given the loop structure — but satisfies type checkers.
        raise last_exc  # type: ignore[misc]

    def _call(self, method: str, path: str, **kwargs: Any) -> Any:
        """Wrap :meth:`_request` applying ``fail_silently`` behaviour."""
        try:
            return self._request(method, path, **kwargs)
        except AuthenticationError:
            raise  # always propagates
        except Exception as exc:
            if not self._config.fail_silently:
                raise
            _logger.error("Influxion SDK error (%s %s): %s", method, path, exc)
            return None

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def create_session(
        self,
        name: str,
        project_id: str | UUID,
        agent_id: str,
        tags: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> str | None:
        """Create a new agent session.

        Args:
            name: A human-readable label for this session.
            project_id: The Influxion project ID (UUID or string).
            agent_id: An identifier for your agent (e.g. ``"email-agent"``).
                Used to match Agent Eval rules.
            tags: Optional list of tags. Deduplicated server-side.
            metadata: Optional JSON-serializable dict attached to the session.

        Returns:
            The session ID string, or ``None`` if an error occurs and
            ``fail_silently=True``.
        """
        body: dict[str, Any] = {
            "name": name,
            "project_id": str(project_id),
            "agent_id": agent_id,
        }
        if tags is not None:
            body["tags"] = tags
        if metadata is not None:
            body["metadata"] = metadata
        result = self._call("POST", "/agent_sessions", json=body)
        return result["session_id"] if result else None

    def create_session_artifact(
        self,
        session_id: str,
        artifact_name: str,
        artifact: str,
        artifact_type: str = "text",
        tags: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> str | None:
        """Upload an artifact to a session.

        Args:
            session_id: The session this artifact belongs to.
            artifact_name: A name for the artifact (e.g. ``"executive-summary"``).
            artifact: The artifact payload — the content to store and evaluate.
            artifact_type: Content type. Currently ``"text"``.
            tags: Optional list of tags. Deduplicated server-side.
            metadata: Optional JSON-serializable dict attached to the artifact.

        Returns:
            The artifact ID string, or ``None`` if an error occurs and
            ``fail_silently=True``.
        """
        body: dict[str, Any] = {
            "artifact_name": artifact_name,
            "artifact": artifact,
            "artifact_type": artifact_type,
        }
        if tags is not None:
            body["tags"] = tags
        if metadata is not None:
            body["metadata"] = metadata
        result = self._call(
            "POST", f"/agent_sessions/{session_id}/artifacts", json=body
        )
        return result["artifact_id"] if result else None

    def update_session(
        self,
        session_id: str,
        *,
        success: bool | None = None,
        failure_message: str | None = None,
        tags: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> None:
        """Update mutable fields on an existing session.

        All parameters are optional. Providing ``success`` causes the server
        to record the session's end time. If no fields are provided, no
        request is made.

        Use :meth:`end_session` as a convenience wrapper when recording the
        session's final outcome.
        """
        body: dict[str, Any] = {}
        if success is not None:
            body["success"] = success
        if failure_message is not None:
            body["failure_message"] = failure_message
        if tags is not None:
            body["tags"] = tags
        if metadata is not None:
            body["metadata"] = metadata
        if body:
            self._call("PATCH", f"/agent_sessions/{session_id}", json=body)

    def end_session(
        self,
        session_id: str,
        *,
        success: bool,
        failure_message: str | None = None,
    ) -> None:
        """Record the outcome of a session and mark it as ended.

        ``success`` is required. The server records the end time when a
        ``success`` value is present. Use ``failure_message`` to describe
        what went wrong when ``success=False``.

        Args:
            session_id: The session to end.
            success: Whether the agent run completed successfully.
            failure_message: Optional description of what went wrong, used
                when ``success=False``.
        """
        self.update_session(
            session_id,
            success=success,
            failure_message=failure_message,
        )
