"""Thin wrapper around the Anthropic Messages API via the `anthropic` SDK."""

from __future__ import annotations

import base64
import mimetypes
from pathlib import Path
import logging
from typing import Optional, Sequence
from urllib.request import urlopen

from anthropic import APIError, Anthropic

from .types import ImageInput, MessageSequence, normalize_messages
from .utils import clamp_retries, run_sync_in_thread, run_with_retries

logger = logging.getLogger(__name__)


class AnthropicClient:
    """Convenience wrapper around the Anthropic Messages API."""

    def generate_response(
        self,
        *,
        api_key: str,
        prompt: Optional[str] = None,
        model: str,
        max_tokens: int = 32000,
        reasoning_effort: Optional[str] = None,
        temperature: Optional[float] = None,
        top_p: Optional[float] = None,
        images: Optional[Sequence[ImageInput]] = None,
        messages: Optional[MessageSequence] = None,
        request_id: Optional[str] = None,
        timeout_s: Optional[float] = None,
        max_retries: Optional[int] = None,
        retry_backoff_s: float = 0.5,
    ) -> str:
        """Generate a response from the specified Anthropic model.

        Args:
            api_key: API key used to authenticate with Anthropic.
            prompt: Natural-language instruction or query for the model.
            model: Identifier of the Anthropic model to target (for example, ``"claude-3-5-sonnet-20241022"``).
            max_tokens: Cap for tokens across the entire exchange, defaults to 32000.
            reasoning_effort: Included for API parity; currently unused by the Anthropic SDK.
            temperature: Optional sampling temperature.
            top_p: Optional nucleus sampling value.
            images: Optional collection of image references (local paths, URLs, or data URLs).
            messages: Optional list of chat-style messages (role/content).
            request_id: Optional request identifier for tracing/logging.
            timeout_s: Optional request timeout in seconds.
            max_retries: Optional retry count for transient failures.
            retry_backoff_s: Base delay (seconds) for exponential backoff between retries.

        Returns:
            The text output produced by the model.

        Raises:
            ValueError: If required arguments are missing or the request payload is empty.
            URLError: If an image URL cannot be retrieved.
            APIError: If the underlying Anthropic request fails.
        """
        if not api_key:
            raise ValueError("api_key must be provided.")
        if not prompt and not messages and not images:
            raise ValueError("At least one of prompt, messages, or images must be provided.")
        if not model:
            raise ValueError("model must be provided.")

        normalized_messages = normalize_messages(prompt=prompt, messages=messages)
        message_payloads: list[dict] = []
        for message in normalized_messages:
            blocks: list[dict] = []
            if message["content"]:
                blocks.append({"type": "text", "text": message["content"]})
            message_payloads.append({"role": message["role"], "content": blocks})

        if images:
            image_blocks = [self._to_image_block(image) for image in images]
            target_index = next(
                (
                    index
                    for index in range(len(message_payloads) - 1, -1, -1)
                    if message_payloads[index]["role"] == "user"
                ),
                None,
            )
            if target_index is None:
                message_payloads.append({"role": "user", "content": image_blocks})
            else:
                message_payloads[target_index]["content"].extend(image_blocks)

        if not message_payloads or not any(msg["content"] for msg in message_payloads):
            raise ValueError("No content provided for response generation.")

        retry_count = clamp_retries(max_retries)

        def _run_request() -> str:
            client_kwargs = {"api_key": api_key}
            if timeout_s is not None:
                client_kwargs["timeout"] = timeout_s
            client = Anthropic(**client_kwargs)

            try:
                request_kwargs: dict[str, object] = {
                    "model": model,
                    "max_tokens": max_tokens,
                    "messages": message_payloads,
                }
                if temperature is not None:
                    request_kwargs["temperature"] = float(temperature)
                if top_p is not None:
                    request_kwargs["top_p"] = float(top_p)
                response = client.messages.create(**request_kwargs)
            except Exception as exc:
                logger.exception(
                    "Anthropic messages.create failed: %s request_id=%s",
                    exc,
                    request_id,
                )
                raise

            text_blocks: list[str] = []
            for block in getattr(response, "content", []) or []:
                if getattr(block, "type", None) == "text":
                    text = getattr(block, "text", None)
                    if text:
                        text_blocks.append(text)

            if text_blocks:
                result_text = "".join(text_blocks)
                logger.info(
                    "Anthropic messages.create succeeded: model=%s images=%d text_len=%d request_id=%s",
                    model,
                    len(images or []),
                    len(result_text or ""),
                    request_id,
                )
                return result_text

            # Treat successful calls without textual content as a successful, empty response
            # rather than raising. This aligns with callers that handle empty outputs gracefully.
            logger.info(
                "Anthropic messages.create succeeded with no text: model=%s images=%d request_id=%s",
                model,
                len(images or []),
                request_id,
            )
            return ""

        return run_with_retries(
            func=_run_request,
            max_retries=retry_count,
            retry_backoff_s=retry_backoff_s,
            request_id=request_id,
        )

    async def async_generate_response(
        self,
        *,
        api_key: str,
        prompt: Optional[str] = None,
        model: str,
        max_tokens: int = 32000,
        reasoning_effort: Optional[str] = None,
        temperature: Optional[float] = None,
        top_p: Optional[float] = None,
        images: Optional[Sequence[ImageInput]] = None,
        messages: Optional[MessageSequence] = None,
        request_id: Optional[str] = None,
        timeout_s: Optional[float] = None,
        max_retries: Optional[int] = None,
        retry_backoff_s: float = 0.5,
    ) -> str:
        return await run_sync_in_thread(
            lambda: self.generate_response(
                api_key=api_key,
                prompt=prompt,
                model=model,
                max_tokens=max_tokens,
                reasoning_effort=reasoning_effort,
                temperature=temperature,
                top_p=top_p,
                images=images,
                messages=messages,
                request_id=request_id,
                timeout_s=timeout_s,
                max_retries=max_retries,
                retry_backoff_s=retry_backoff_s,
            )
        )

    def generate_image(
        self,
        *,
        api_key: str,
        prompt: str,
        model: str,
        image_size: str = "2K",
        image: Optional[ImageInput] = None,
    ) -> bytes:
        """Generate an image using the Anthropic API.

        Raises:
            NotImplementedError: This method is not yet implemented for Anthropic.
        """
        raise NotImplementedError("Image generation is not implemented for Anthropic.")

    async def async_generate_image(
        self,
        *,
        api_key: str,
        prompt: str,
        model: str,
        image_size: str = "2K",
        image: Optional[ImageInput] = None,
    ) -> bytes:
        return await run_sync_in_thread(
            lambda: self.generate_image(
                api_key=api_key,
                prompt=prompt,
                model=model,
                image_size=image_size,
                image=image,
            )
        )

    def list_models(
        self,
        *,
        api_key: str,
        request_id: Optional[str] = None,
        timeout_s: Optional[float] = None,
        max_retries: Optional[int] = None,
        retry_backoff_s: float = 0.5,
    ) -> list[dict[str, Optional[str]]]:
        """Return the models available to the authenticated Anthropic account."""
        if not api_key:
            raise ValueError("api_key must be provided.")

        retry_count = clamp_retries(max_retries)

        def _run_request() -> list[dict[str, Optional[str]]]:
            client_kwargs = {"api_key": api_key}
            if timeout_s is not None:
                client_kwargs["timeout"] = timeout_s
            client = Anthropic(**client_kwargs)
            models: list[dict[str, Optional[str]]] = []

            try:
                iterator = client.models.list()
            except Exception as exc:
                logger.exception(
                    "Anthropic list models failed: %s request_id=%s",
                    exc,
                    request_id,
                )
                raise

            for model in iterator:
                model_id = getattr(model, "id", None)
                if model_id is None and isinstance(model, dict):
                    model_id = model.get("id")
                if not model_id:
                    continue

                display_name = getattr(model, "display_name", None)
                if display_name is None and isinstance(model, dict):
                    display_name = model.get("display_name")

                models.append({"id": model_id, "display_name": display_name})

            logger.info(
                "Anthropic list_models succeeded: count=%d request_id=%s",
                len(models),
                request_id,
            )
            return models

        return run_with_retries(
            func=_run_request,
            max_retries=retry_count,
            retry_backoff_s=retry_backoff_s,
            request_id=request_id,
        )

    async def async_list_models(
        self,
        *,
        api_key: str,
        request_id: Optional[str] = None,
        timeout_s: Optional[float] = None,
        max_retries: Optional[int] = None,
        retry_backoff_s: float = 0.5,
    ) -> list[dict[str, Optional[str]]]:
        return await run_sync_in_thread(
            lambda: self.list_models(
                api_key=api_key,
                request_id=request_id,
                timeout_s=timeout_s,
                max_retries=max_retries,
                retry_backoff_s=retry_backoff_s,
            )
        )

    @staticmethod
    def _to_image_block(image: ImageInput) -> dict:
        """Convert an image reference into an Anthropic content block."""
        if isinstance(image, Path):
            return _block_from_path(image)

        if image.startswith("data:"):
            return _block_from_data_url(image)

        if image.startswith(("http://", "https://")):
            return _block_from_url(image)

        return _block_from_path(Path(image))


def _block_from_path(path: Path) -> dict:
    """Create an image block from a local filesystem path."""
    expanded = path.expanduser()
    data = expanded.read_bytes()
    encoded = base64.b64encode(data).decode("utf-8")
    mime_type = mimetypes.guess_type(expanded.name)[0] or "application/octet-stream"

    return {
        "type": "image",
        "source": {
            "type": "base64",
            "media_type": mime_type,
            "data": encoded,
        },
    }


def _block_from_url(url: str) -> dict:
    """Create an image block by downloading content from a URL."""
    with urlopen(url) as response:
        data = response.read()
        mime_type = response.info().get_content_type()

    if not mime_type or mime_type == "application/octet-stream":
        mime_type = mimetypes.guess_type(url)[0] or "application/octet-stream"

    encoded = base64.b64encode(data).decode("utf-8")

    return {
        "type": "image",
        "source": {
            "type": "base64",
            "media_type": mime_type,
            "data": encoded,
        },
    }


def _block_from_data_url(data_url: str) -> dict:
    """Create an image block from a data URL."""
    header, encoded = data_url.split(",", 1)
    metadata = header[len("data:") :]
    mime_type = "application/octet-stream"
    is_base64 = False

    if ";" in metadata:
        mime_type_part, _, remainder = metadata.partition(";")
        if mime_type_part:
            mime_type = mime_type_part
        is_base64 = "base64" in remainder
    elif metadata:
        mime_type = metadata

    if is_base64:
        data_b64 = encoded
    else:
        data_bytes = encoded.encode("utf-8")
        data_b64 = base64.b64encode(data_bytes).decode("utf-8")

    return {
        "type": "image",
        "source": {
            "type": "base64",
            "media_type": mime_type or "application/octet-stream",
            "data": data_b64,
        },
    }
