"""Client for the Brookmimir API."""

from __future__ import annotations

import asyncio
import base64
import contextlib
import logging
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, TypeVar, Union

import httpx
from pydantic import BaseModel, TypeAdapter, ValidationError

from .models import (
    CreditResponse,
    EsResponse,
    FaceSearchRequest,
    QueryRequest,
    QueryResponse,
    StatusMessage,
    UserProfileResponse,
    VersionResponse,
)

logger = logging.getLogger(__name__)

TModel = TypeVar("TModel", bound=BaseModel)
StatusOrEsAdapter = TypeAdapter(Union[StatusMessage, EsResponse])
StatusOrQueryAdapter = TypeAdapter(Union[StatusMessage, QueryResponse])
StatusOrCreditAdapter = TypeAdapter(Union[StatusMessage, CreditResponse])


@dataclass
class OdinsEyeError(Exception):
    """Base error for client failures.

    This exception is raised for client-side errors such as validation failures,
    network issues, or malformed responses.

    Attributes:
        message: A human-readable error description
    """

    message: str

    def __str__(self) -> str:
        return self.message


@dataclass
class OdinsEyeAPIError(OdinsEyeError):
    """Error raised when the API responds with a non-2xx status.

    This exception is raised for server-side errors indicated by HTTP
    status codes 4xx and 5xx.

    Attributes:
        message: A human-readable error description
        status_code: The HTTP status code from the API
        error: The error field from the API response, if available
        error_details: Additional error details from the API, if available
        response_text: Raw response text if JSON parsing failed
        rate_limit_reset: Timestamp when rate limit resets (for 429 errors)
    """

    status_code: int
    error: str | None = None
    error_details: str | None = None
    response_text: str | None = None
    rate_limit_reset: int | None = None


@dataclass
class RetryConfig:
    """Configuration for automatic retry behavior.

    Attributes:
        max_retries: Maximum number of retry attempts (default: 3)
        initial_delay: Initial delay in seconds before first retry (default: 1.0)
        max_delay: Maximum delay in seconds between retries (default: 60.0)
        exponential_base: Base for exponential backoff calculation (default: 2.0)
        retry_on_rate_limit: Whether to retry on 429 rate limit errors (default: True)
    """

    max_retries: int = 3
    initial_delay: float = 1.0
    max_delay: float = 60.0
    exponential_base: float = 2.0
    retry_on_rate_limit: bool = True


class OdinsEyeClient:
    """Synchronous client for interacting with the Brookmimir API over HTTP.

    This client provides a convenient interface to all Brookmimir API endpoints
    with automatic retry logic, rate limiting handling, and comprehensive error
    reporting.

    Args:
        api_key: Your Brookmimir API key for authentication
        headers: Additional HTTP headers to include in requests
        timeout: Custom timeout configuration for requests
        http2: Whether to enable HTTP/2 support (default: True)
        retry_config: Configuration for automatic retry behavior
        enable_logging: Whether to enable debug logging (default: False)

    Examples:
        Basic usage with context manager:

        >>> with OdinsEyeClient(api_key="your-key") as client:
        ...     status = client.index()
        ...     print(status["message"])

        Manual resource management:

        >>> client = OdinsEyeClient(api_key="your-key")
        >>> try:
        ...     profile = client.profile()
        ... finally:
        ...     client.close()

        Custom retry configuration:

        >>> retry_config = RetryConfig(max_retries=5, initial_delay=2.0)
        >>> client = OdinsEyeClient(api_key="your-key", retry_config=retry_config)
    """

    def __init__(
        self,
        *,
        api_key: str | None = None,
        headers: dict[str, str] | None = None,
        timeout: httpx.Timeout | None = None,
        http2: bool = True,
        retry_config: RetryConfig | None = None,
        enable_logging: bool = False,
    ) -> None:
        """Initialize the synchronous Brookmimir API client."""
        default_headers = {
            "Accept": "application/json",
            "User-Agent": "odins-eye-pypi/1.0.0",
        }
        if api_key:
            default_headers["x-api-key"] = api_key
        if headers:
            default_headers.update(headers)

        self._client = httpx.Client(
            base_url="https://api.brookmimir.com",
            headers=default_headers,
            timeout=timeout or httpx.Timeout(10.0, connect=5.0),
            http2=http2,
        )
        self._retry_config = retry_config or RetryConfig()
        self._enable_logging = enable_logging

        if enable_logging:
            logger.setLevel(logging.DEBUG)

    def close(self) -> None:
        """Close the underlying HTTP connection pool.

        This should be called when you're done using the client to release
        resources. Alternatively, use the client as a context manager.
        """
        self._client.close()

    def __enter__(self) -> OdinsEyeClient:
        """Enable use as a context manager."""
        return self

    def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
        """Close the client when leaving a context manager."""
        self.close()

    def index(self) -> dict[str, Any]:
        """Fetch the API status message.

        Returns:
            A dictionary containing status information about the API

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     status = client.index()
            ...     print(f"API Status: {status['status']}")
        """
        payload = self._request_json("GET", "/")
        return self._validate_response(StatusMessage, payload)

    def document(self, doc_id: str) -> dict[str, Any]:
        """Fetch a document by ID.

        Args:
            doc_id: The unique identifier of the document to retrieve

        Returns:
            A dictionary containing the document data

        Raises:
            ValueError: If doc_id is empty
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     doc = client.document("doc-123")
            ...     print(doc)
        """
        if not doc_id:
            raise ValueError("doc_id is required")
        payload = self._request_json(
            "GET",
            "/document",
            headers={"bmm-doc-id": doc_id},
        )
        return self._validate_adapter(StatusOrEsAdapter, payload)

    def query(self, query: dict[str, Any]) -> dict[str, Any]:
        """Submit a query payload.

        Args:
            query: A dictionary containing the query parameters

        Returns:
            A dictionary containing the query results

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the query or response cannot be validated

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     result = client.query({"query": {"match_all": {}}})
            ...     print(result)
        """
        request = QueryRequest.model_validate(query).root
        payload = self._request_json("POST", "/query", json=request)
        return self._validate_adapter(StatusOrQueryAdapter, payload)

    def profile(self) -> dict[str, Any]:
        """Fetch the current user profile.

        Returns:
            A dictionary containing the user profile information

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     profile = client.profile()
            ...     print(f"User: {profile['user']['name']}")
        """
        payload = self._request_json("GET", "/profile")
        return self._validate_response(UserProfileResponse, payload)

    def version(self) -> dict[str, Any]:
        """Fetch the API version.

        Returns:
            A dictionary containing the API version information

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     version_info = client.version()
            ...     print(f"API Version: {version_info['version']}")
        """
        payload = self._request_json("GET", "/version")
        return self._validate_response(VersionResponse, payload)

    def credits(self) -> dict[str, Any]:
        """Fetch the current credit balance.

        Returns:
            A dictionary containing credit balance information

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     credits = client.credits()
            ...     print(f"Balance: {credits['current_balance']}")
        """
        payload = self._request_json("GET", "/credits")
        return self._validate_adapter(StatusOrCreditAdapter, payload)

    def face_search(self, image_path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
        """Submit a face search using an image file.

        Args:
            image_path: Path to the image file to search
            payload: Optional additional parameters for the search

        Returns:
            A dictionary containing the face search results

        Raises:
            ValueError: If the image file does not exist
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the request or response cannot be validated

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     results = client.face_search("photo.jpg")
            ...     print(results)

            With additional parameters:

            >>> params = {"threshold": 0.8, "max_results": 10}
            >>> results = client.face_search("photo.jpg", payload=params)
        """
        request_payload = dict(payload or {})
        request_payload["image_base64"] = _encode_image_b64(image_path)
        request = FaceSearchRequest.model_validate(request_payload).root
        return self._request_json("POST", "/face/search", json=request)

    def nettest(self) -> dict[str, Any]:
        """Run a lightweight network test.

        Returns:
            A dictionary containing network test results

        Raises:
            OdinsEyeAPIError: If the API returns an error response

        Examples:
            >>> with OdinsEyeClient(api_key="your-key") as client:
            ...     result = client.nettest()
            ...     print(result)
        """
        return self._request_json("GET", "/nettest")

    def _request_json(self, method: str, path: str, **kwargs: Any) -> Any:
        """Send an HTTP request with retry logic and decode the JSON payload."""
        last_exception = None

        for attempt in range(self._retry_config.max_retries + 1):
            try:
                if self._enable_logging:
                    logger.debug(f"Request: {method} {path} (attempt {attempt + 1})")

                response = self._client.request(method, path, **kwargs)

                if self._enable_logging:
                    logger.debug(f"Response: {response.status_code}")

                # Handle rate limiting
                if response.status_code == 429:
                    rate_limit_error = self._handle_rate_limit(response, attempt)
                    if not self._retry_config.retry_on_rate_limit:
                        raise rate_limit_error
                    last_exception = rate_limit_error
                    self._sleep_for_retry(attempt, rate_limit_error.rate_limit_reset)
                    continue

                # Handle other errors
                if response.status_code >= 400:
                    error = self._api_error(response)
                    # Retry on 5xx server errors
                    if (
                        500 <= response.status_code < 600
                        and attempt < self._retry_config.max_retries
                    ):
                        last_exception = error
                        self._sleep_for_retry(attempt)
                        continue
                    raise error

                # Success - parse JSON
                try:
                    return response.json()
                except ValueError:
                    raise OdinsEyeError("API returned non-JSON response") from None

            except (httpx.TimeoutException, httpx.NetworkError) as e:
                # Retry on network errors
                if attempt < self._retry_config.max_retries:
                    last_exception = OdinsEyeError(f"Network error: {e}")
                    self._sleep_for_retry(attempt)
                    continue
                raise OdinsEyeError(f"Network error after {attempt + 1} attempts: {e}") from None

        # If we get here, we've exhausted retries
        if last_exception:
            raise last_exception
        raise OdinsEyeError("Request failed after all retries")

    def _handle_rate_limit(self, response: httpx.Response, attempt: int) -> OdinsEyeAPIError:
        """Handle rate limit errors."""
        reset_time = None
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            with contextlib.suppress(ValueError):
                reset_time = int(time.time()) + int(retry_after)

        if self._enable_logging:
            logger.warning(f"Rate limit exceeded (attempt {attempt + 1})")
            if reset_time:
                logger.warning(f"Rate limit resets at {reset_time}")

        return OdinsEyeAPIError(
            message="API rate limit exceeded",
            status_code=429,
            error="rate_limit_exceeded",
            rate_limit_reset=reset_time,
        )

    def _sleep_for_retry(self, attempt: int, reset_time: int | None = None) -> None:
        """Calculate and sleep for retry delay."""
        if reset_time:
            # Use rate limit reset time if available
            delay = max(0, reset_time - int(time.time()))
        else:
            # Exponential backoff
            delay = min(
                self._retry_config.initial_delay * (self._retry_config.exponential_base**attempt),
                self._retry_config.max_delay,
            )

        if self._enable_logging:
            logger.debug(f"Retrying in {delay:.2f} seconds...")

        time.sleep(delay)

    def _validate_response(self, model: type[TModel], payload: Any) -> dict[str, Any]:
        """Validate the response payload with Pydantic and return a dict."""
        try:
            return model.model_validate(payload).model_dump()
        except ValidationError as exc:
            raise OdinsEyeError(f"API response validation failed: {exc}") from None

    def _validate_adapter(self, adapter: TypeAdapter, payload: Any) -> dict[str, Any]:
        """Validate the response using a TypeAdapter."""
        try:
            value = adapter.validate_python(payload)
            return value.model_dump() if hasattr(value, "model_dump") else value
        except ValidationError as exc:
            raise OdinsEyeError(f"API response validation failed: {exc}") from None

    def _api_error(self, response: httpx.Response) -> OdinsEyeAPIError:
        """Parse error responses into a structured exception."""
        error = None
        error_details = None
        response_text = None
        try:
            payload = response.json()
            error = payload.get("error")
            error_details = payload.get("error_details")
        except ValueError:
            response_text = response.text

        message = error or f"API request failed with status {response.status_code}"
        return OdinsEyeAPIError(
            message=message,
            status_code=response.status_code,
            error=error,
            error_details=error_details,
            response_text=response_text,
        )


class AsyncOdinsEyeClient:
    """Asynchronous client for interacting with the Brookmimir API over HTTP.

    This client provides async/await support for all Brookmimir API endpoints
    with automatic retry logic, rate limiting handling, and comprehensive error
    reporting.

    Args:
        api_key: Your Brookmimir API key for authentication
        headers: Additional HTTP headers to include in requests
        timeout: Custom timeout configuration for requests
        http2: Whether to enable HTTP/2 support (default: True)
        retry_config: Configuration for automatic retry behavior
        enable_logging: Whether to enable debug logging (default: False)

    Examples:
        Basic usage with async context manager:

        >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
        ...     status = await client.index()
        ...     print(status["message"])

        Concurrent requests:

        >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
        ...     results = await asyncio.gather(
        ...         client.version(),
        ...         client.profile(),
        ...         client.credits()
        ...     )
    """

    def __init__(
        self,
        *,
        api_key: str | None = None,
        headers: dict[str, str] | None = None,
        timeout: httpx.Timeout | None = None,
        http2: bool = True,
        retry_config: RetryConfig | None = None,
        enable_logging: bool = False,
    ) -> None:
        """Initialize the asynchronous Brookmimir API client."""
        default_headers = {
            "Accept": "application/json",
            "User-Agent": "odins-eye-pypi/1.0.0",
        }
        if api_key:
            default_headers["api-key"] = api_key
        if headers:
            default_headers.update(headers)

        self._client = httpx.AsyncClient(
            base_url="https://api.brookmimir.com",
            headers=default_headers,
            timeout=timeout or httpx.Timeout(10.0, connect=5.0),
            http2=http2,
        )
        self._retry_config = retry_config or RetryConfig()
        self._enable_logging = enable_logging

        if enable_logging:
            logger.setLevel(logging.DEBUG)

    async def aclose(self) -> None:
        """Close the underlying HTTP connection pool.

        This should be called when you're done using the client to release
        resources. Alternatively, use the client as a context manager.
        """
        await self._client.aclose()

    async def __aenter__(self) -> AsyncOdinsEyeClient:
        """Enable use as an async context manager."""
        return self

    async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
        """Close the client when leaving an async context manager."""
        await self.aclose()

    async def index(self) -> dict[str, Any]:
        """Fetch the API status message.

        Returns:
            A dictionary containing status information about the API

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     status = await client.index()
            ...     print(f"API Status: {status['status']}")
        """
        payload = await self._request_json("GET", "/")
        return self._validate_response(StatusMessage, payload)

    async def document(self, doc_id: str) -> dict[str, Any]:
        """Fetch a document by ID.

        Args:
            doc_id: The unique identifier of the document to retrieve

        Returns:
            A dictionary containing the document data

        Raises:
            ValueError: If doc_id is empty
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     doc = await client.document("doc-123")
            ...     print(doc)
        """
        if not doc_id:
            raise ValueError("doc_id is required")
        payload = await self._request_json(
            "GET",
            "/document",
            headers={"bmm-doc-id": doc_id},
        )
        return self._validate_adapter(StatusOrEsAdapter, payload)

    async def query(self, query: dict[str, Any]) -> dict[str, Any]:
        """Submit a query payload.

        Args:
            query: A dictionary containing the query parameters

        Returns:
            A dictionary containing the query results

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the query or response cannot be validated

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     result = await client.query({"query": {"match_all": {}}})
            ...     print(result)
        """
        request = QueryRequest.model_validate(query).root
        payload = await self._request_json("POST", "/query", json=request)
        return self._validate_adapter(StatusOrQueryAdapter, payload)

    async def profile(self) -> dict[str, Any]:
        """Fetch the current user profile.

        Returns:
            A dictionary containing the user profile information

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     profile = await client.profile()
            ...     print(f"User: {profile['user']['name']}")
        """
        payload = await self._request_json("GET", "/profile")
        return self._validate_response(UserProfileResponse, payload)

    async def version(self) -> dict[str, Any]:
        """Fetch the API version.

        Returns:
            A dictionary containing the API version information

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     version_info = await client.version()
            ...     print(f"API Version: {version_info['version']}")
        """
        payload = await self._request_json("GET", "/version")
        return self._validate_response(VersionResponse, payload)

    async def credits(self) -> dict[str, Any]:
        """Fetch the current credit balance.

        Returns:
            A dictionary containing credit balance information

        Raises:
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the response cannot be validated

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     credits = await client.credits()
            ...     print(f"Balance: {credits['current_balance']}")
        """
        payload = await self._request_json("GET", "/credits")
        return self._validate_adapter(StatusOrCreditAdapter, payload)

    async def face_search(
        self, image_path: str, payload: dict[str, Any] | None = None
    ) -> dict[str, Any]:
        """Submit a face search using an image file.

        Args:
            image_path: Path to the image file to search
            payload: Optional additional parameters for the search

        Returns:
            A dictionary containing the face search results

        Raises:
            ValueError: If the image file does not exist
            OdinsEyeAPIError: If the API returns an error response
            OdinsEyeError: If the request or response cannot be validated

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     results = await client.face_search("photo.jpg")
            ...     print(results)

            With additional parameters:

            >>> params = {"threshold": 0.8, "max_results": 10}
            >>> results = await client.face_search("photo.jpg", payload=params)
        """
        request_payload = dict(payload or {})
        request_payload["image_base64"] = _encode_image_b64(image_path)
        request = FaceSearchRequest.model_validate(request_payload).root
        return await self._request_json("POST", "/face/search", json=request)

    async def nettest(self) -> dict[str, Any]:
        """Run a lightweight network test.

        Returns:
            A dictionary containing network test results

        Raises:
            OdinsEyeAPIError: If the API returns an error response

        Examples:
            >>> async with AsyncOdinsEyeClient(api_key="your-key") as client:
            ...     result = await client.nettest()
            ...     print(result)
        """
        return await self._request_json("GET", "/nettest")

    async def _request_json(self, method: str, path: str, **kwargs: Any) -> Any:
        """Send an HTTP request with retry logic and decode the JSON payload."""
        last_exception = None

        for attempt in range(self._retry_config.max_retries + 1):
            try:
                if self._enable_logging:
                    logger.debug(f"Request: {method} {path} (attempt {attempt + 1})")

                response = await self._client.request(method, path, **kwargs)

                if self._enable_logging:
                    logger.debug(f"Response: {response.status_code}")

                # Handle rate limiting
                if response.status_code == 429:
                    rate_limit_error = self._handle_rate_limit(response, attempt)
                    if not self._retry_config.retry_on_rate_limit:
                        raise rate_limit_error
                    last_exception = rate_limit_error
                    await self._async_sleep_for_retry(attempt, rate_limit_error.rate_limit_reset)
                    continue

                # Handle other errors
                if response.status_code >= 400:
                    error = self._api_error(response)
                    # Retry on 5xx server errors
                    if (
                        500 <= response.status_code < 600
                        and attempt < self._retry_config.max_retries
                    ):
                        last_exception = error
                        await self._async_sleep_for_retry(attempt)
                        continue
                    raise error

                # Success - parse JSON
                try:
                    return response.json()
                except ValueError:
                    raise OdinsEyeError("API returned non-JSON response") from None

            except (httpx.TimeoutException, httpx.NetworkError) as e:
                # Retry on network errors
                if attempt < self._retry_config.max_retries:
                    last_exception = OdinsEyeError(f"Network error: {e}")
                    await self._async_sleep_for_retry(attempt)
                    continue
                raise OdinsEyeError(f"Network error after {attempt + 1} attempts: {e}") from None

        # If we get here, we've exhausted retries
        if last_exception:
            raise last_exception
        raise OdinsEyeError("Request failed after all retries")

    def _handle_rate_limit(self, response: httpx.Response, attempt: int) -> OdinsEyeAPIError:
        """Handle rate limit errors."""
        reset_time = None
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            with contextlib.suppress(ValueError):
                reset_time = int(time.time()) + int(retry_after)

        if self._enable_logging:
            logger.warning(f"Rate limit exceeded (attempt {attempt + 1})")
            if reset_time:
                logger.warning(f"Rate limit resets at {reset_time}")

        return OdinsEyeAPIError(
            message="API rate limit exceeded",
            status_code=429,
            error="rate_limit_exceeded",
            rate_limit_reset=reset_time,
        )

    async def _async_sleep_for_retry(self, attempt: int, reset_time: int | None = None) -> None:
        """Calculate and sleep for retry delay asynchronously."""
        if reset_time:
            # Use rate limit reset time if available
            delay = max(0, reset_time - int(time.time()))
        else:
            # Exponential backoff
            delay = min(
                self._retry_config.initial_delay * (self._retry_config.exponential_base**attempt),
                self._retry_config.max_delay,
            )

        if self._enable_logging:
            logger.debug(f"Retrying in {delay:.2f} seconds...")

        await asyncio.sleep(delay)

    def _validate_response(self, model: type[TModel], payload: Any) -> dict[str, Any]:
        """Validate the response payload with Pydantic and return a dict."""
        try:
            return model.model_validate(payload).model_dump()
        except ValidationError as exc:
            raise OdinsEyeError(f"API response validation failed: {exc}") from None

    def _validate_adapter(self, adapter: TypeAdapter, payload: Any) -> dict[str, Any]:
        """Validate the response using a TypeAdapter."""
        try:
            value = adapter.validate_python(payload)
            return value.model_dump() if hasattr(value, "model_dump") else value
        except ValidationError as exc:
            raise OdinsEyeError(f"API response validation failed: {exc}") from None

    def _api_error(self, response: httpx.Response) -> OdinsEyeAPIError:
        """Parse error responses into a structured exception."""
        error = None
        error_details = None
        response_text = None
        try:
            payload = response.json()
            error = payload.get("error")
            error_details = payload.get("error_details")
        except ValueError:
            response_text = response.text

        message = error or f"API request failed with status {response.status_code}"
        return OdinsEyeAPIError(
            message=message,
            status_code=response.status_code,
            error=error,
            error_details=error_details,
            response_text=response_text,
        )


def _encode_image_b64(image_path: str) -> str:
    """Encode an image file to base64.

    Args:
        image_path: Path to the image file

    Returns:
        Base64-encoded string of the image data

    Raises:
        ValueError: If the image file does not exist
    """
    path = Path(image_path).expanduser()
    if not path.is_file():
        raise ValueError(f"image_path not found: {image_path}")
    data = path.read_bytes()
    return base64.b64encode(data).decode("ascii")
