"""Anthropic Adapter — direct Claude API access.

Uses the Anthropic Messages API (``/v1/messages``) with automatic
translation to/from the OpenAI chat format used internally by AlbusOS.

When ``ANTHROPIC_API_KEY`` is set, ``anthropic/*`` and bare ``claude*``
models are routed here instead of through OpenRouter, avoiding the
markup and extra latency.

Requires: ``httpx`` (already a dependency).
"""

from __future__ import annotations

import json
import logging

import httpx

from core.types.llm import LLMEmbedResult, LLMGenerateResult, LLMToolCall, LLMToolCallFunction

logger = logging.getLogger(__name__)

# Anthropic API uses a different message format than OpenAI.
# This adapter translates between the two.

_ROLE_MAP = {"user": "user", "assistant": "assistant"}


class AnthropicAdapter:
    """Direct Anthropic provider adapter (Messages API)."""

    BASE_URL = "https://api.anthropic.com"
    API_VERSION = "2023-06-01"
    DEFAULT_MODEL = "claude-sonnet-4-20250514"

    def __init__(self, api_key: str):
        self.api_key = api_key
        self._client: httpx.AsyncClient | None = None

    def _ensure_client(self) -> httpx.AsyncClient:
        if self._client is None:
            self._client = httpx.AsyncClient(
                base_url=self.BASE_URL,
                headers={
                    "x-api-key": self.api_key,
                    "anthropic-version": self.API_VERSION,
                    "Content-Type": "application/json",
                },
                timeout=120.0,
            )
        return self._client

    async def aclose(self) -> None:
        if self._client is not None:
            await self._client.aclose()
            self._client = None

    def _normalize_model(self, model: str) -> str:
        """Strip ``anthropic/`` prefix if present."""
        if model.startswith("anthropic/"):
            return model[len("anthropic/") :]
        return model

    # ------------------------------------------------------------------
    # Message format translation
    # ------------------------------------------------------------------

    def _translate_messages(
        self,
        *,
        prompt: str,
        system: str | None,
        messages: list[dict[str, object]] | None,
        images: list[str] | None,
    ) -> tuple[str | None, list[dict[str, object]]]:
        """Convert OpenAI-format messages to Anthropic Messages API format.

        Returns ``(system_text, anthropic_messages)``.
        """
        system_text = system
        anthropic_msgs: list[dict[str, object]] = []

        if messages:
            for msg in messages:
                role = msg.get("role", "user")

                if role == "system":
                    # Anthropic uses a top-level system param, not a message
                    system_text = str(msg.get("content", ""))
                    continue

                if role == "tool":
                    # Anthropic uses tool_result content blocks
                    anthropic_msgs.append(
                        {
                            "role": "user",
                            "content": [
                                {
                                    "type": "tool_result",
                                    "tool_use_id": msg.get("tool_call_id", ""),
                                    "content": str(msg.get("content", "")),
                                },
                            ],
                        }
                    )
                    continue

                if role == "assistant" and msg.get("tool_calls"):
                    # Convert OpenAI tool_calls to Anthropic tool_use blocks
                    content_blocks: list[dict[str, object]] = []
                    if msg.get("content"):
                        content_blocks.append({"type": "text", "text": str(msg["content"])})
                    for tc in msg["tool_calls"]:
                        func = tc.get("function", {}) if isinstance(tc, dict) else {}
                        args_str = func.get("arguments", "{}") if isinstance(func, dict) else "{}"
                        try:
                            args_parsed = (
                                json.loads(args_str) if isinstance(args_str, str) else args_str
                            )
                        except json.JSONDecodeError:
                            args_parsed = {}
                        content_blocks.append(
                            {
                                "type": "tool_use",
                                "id": tc.get("id", "") if isinstance(tc, dict) else "",
                                "name": func.get("name", "") if isinstance(func, dict) else "",
                                "input": args_parsed,
                            }
                        )
                    anthropic_msgs.append({"role": "assistant", "content": content_blocks})
                    continue

                # Standard user/assistant message
                anthropic_msgs.append(
                    {
                        "role": role if role in ("user", "assistant") else "user",
                        "content": str(msg.get("content", "")),
                    }
                )
        else:
            # Build from prompt + images
            if images:
                content_parts: list[dict[str, object]] = []
                for img in images:
                    if img.startswith("data:"):
                        # Parse data URL
                        media_type = img.split(";")[0].split(":")[1] if ";" in img else "image/jpeg"
                        data = img.split(",", 1)[1] if "," in img else img
                    else:
                        media_type = "image/jpeg"
                        data = img
                    content_parts.append(
                        {
                            "type": "image",
                            "source": {
                                "type": "base64",
                                "media_type": media_type,
                                "data": data,
                            },
                        }
                    )
                content_parts.append({"type": "text", "text": prompt})
                anthropic_msgs.append({"role": "user", "content": content_parts})
            else:
                anthropic_msgs.append({"role": "user", "content": prompt})

        return system_text, anthropic_msgs

    def _translate_tools(
        self, tools: list[dict[str, object]] | None
    ) -> list[dict[str, object]] | None:
        """Convert OpenAI tool specs to Anthropic tool format."""
        if not tools:
            return None

        anthropic_tools: list[dict[str, object]] = []
        for tool in tools:
            func = tool.get("function", {})
            if not isinstance(func, dict):
                continue
            anthropic_tools.append(
                {
                    "name": func.get("name", ""),
                    "description": func.get("description", ""),
                    "input_schema": func.get("parameters", {"type": "object", "properties": {}}),
                }
            )
        return anthropic_tools or None

    # ------------------------------------------------------------------
    # Generate
    # ------------------------------------------------------------------

    async def generate(
        self,
        *,
        prompt: str = "",
        model: str | None = None,
        system: str | None = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        tools: list[dict[str, object]] | None = None,
        tool_choice: str | dict[str, object] | None = None,
        messages: list[dict[str, object]] | None = None,
        images: list[str] | None = None,
        response_format: str = "text",
    ) -> LLMGenerateResult:
        """Generate via the Anthropic Messages API."""
        from core.llm.providers import LLMProviderError

        client = self._ensure_client()
        model = self._normalize_model(model or self.DEFAULT_MODEL)

        system_text, anthropic_msgs = self._translate_messages(
            prompt=prompt,
            system=system,
            messages=messages,
            images=images,
        )

        request_data: dict[str, object] = {
            "model": model,
            "messages": anthropic_msgs,
            "max_tokens": max_tokens,
            "temperature": temperature,
        }

        if system_text:
            request_data["system"] = system_text

        anthropic_tools = self._translate_tools(tools)
        if anthropic_tools:
            request_data["tools"] = anthropic_tools
            # Translate tool_choice
            if tool_choice == "auto":
                request_data["tool_choice"] = {"type": "auto"}
            elif tool_choice == "required":
                request_data["tool_choice"] = {"type": "any"}
            elif tool_choice == "none":
                pass  # Don't send tool_choice
            elif isinstance(tool_choice, dict) and "function" in tool_choice:
                request_data["tool_choice"] = {
                    "type": "tool",
                    "name": tool_choice["function"].get("name", ""),
                }

        try:
            response = await client.post("/v1/messages", json=request_data)
            response.raise_for_status()
            data = response.json()
        except httpx.HTTPStatusError as e:
            error_body = e.response.text
            try:
                error_data = json.loads(error_body)
                error_msg = error_data.get("error", {}).get("message", error_body)
            except json.JSONDecodeError:
                error_msg = error_body
            raise LLMProviderError(
                message=f"Anthropic API error: {error_msg}",
                provider="anthropic",
                model=model,
                is_transient=e.response.status_code >= 500,
            ) from e
        except Exception as e:
            raise LLMProviderError(
                message=str(e),
                provider="anthropic",
                model=model,
                is_transient=True,
            ) from e

        # Parse Anthropic response into OpenAI-compatible result
        content_text = ""
        tool_calls: list[LLMToolCall] | None = None

        for block in data.get("content", []):
            if block.get("type") == "text":
                content_text += block.get("text", "")
            elif block.get("type") == "tool_use":
                if tool_calls is None:
                    tool_calls = []
                tool_calls.append(
                    LLMToolCall(
                        id=block.get("id", ""),
                        function=LLMToolCallFunction(
                            name=block.get("name", ""),
                            arguments=json.dumps(block.get("input", {})),
                        ),
                    ),
                )

        usage = data.get("usage", {})
        input_tokens = usage.get("input_tokens", 0)
        output_tokens = usage.get("output_tokens", 0)

        # Map Anthropic stop_reason to OpenAI finish_reason
        stop_reason = data.get("stop_reason", "end_turn")
        finish_reason_map = {
            "end_turn": "stop",
            "tool_use": "tool_calls",
            "max_tokens": "length",
            "stop_sequence": "stop",
        }
        finish_reason = finish_reason_map.get(stop_reason, stop_reason)

        return LLMGenerateResult(
            content=content_text,
            model=data.get("model", model),
            tokens_used=input_tokens + output_tokens,
            cost_usd=0.0,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            provider="anthropic",
            finish_reason=finish_reason,
            tool_calls=tool_calls,
            metadata={"id": data.get("id")},
        )

    async def generate_stream(
        self,
        *,
        prompt: str = "",
        model: str | None = None,
        system: str | None = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        tools: list[dict[str, object]] | None = None,
        tool_choice: str | dict[str, object] | None = None,
        messages: list[dict[str, object]] | None = None,
        images: list[str] | None = None,
        response_format: str = "text",
    ):
        """Stream via Anthropic Messages API with SSE."""
        from core.llm.providers import LLMProviderError
        from core.types.llm import LLMStreamChunk

        client = self._ensure_client()
        model = self._normalize_model(model or self.DEFAULT_MODEL)

        system_text, anthropic_msgs = self._translate_messages(
            prompt=prompt,
            system=system,
            messages=messages,
            images=images,
        )

        request_data: dict[str, object] = {
            "model": model,
            "messages": anthropic_msgs,
            "max_tokens": max_tokens,
            "temperature": temperature,
            "stream": True,
        }

        if system_text:
            request_data["system"] = system_text

        anthropic_tools = self._translate_tools(tools)
        if anthropic_tools:
            request_data["tools"] = anthropic_tools
            if tool_choice == "auto":
                request_data["tool_choice"] = {"type": "auto"}

        # Track tool_use blocks being streamed
        current_tool_id = ""
        current_tool_name = ""
        current_tool_input = ""

        try:
            async with client.stream("POST", "/v1/messages", json=request_data) as resp:
                resp.raise_for_status()
                async for line in resp.aiter_lines():
                    if not line.startswith("data: "):
                        continue
                    payload = line[len("data: ") :]
                    if not payload.strip():
                        continue

                    event_data = json.loads(payload)
                    event_type = event_data.get("type", "")

                    if event_type == "content_block_start":
                        block = event_data.get("content_block", {})
                        if block.get("type") == "tool_use":
                            current_tool_id = block.get("id", "")
                            current_tool_name = block.get("name", "")
                            current_tool_input = ""

                    elif event_type == "content_block_delta":
                        delta = event_data.get("delta", {})
                        if delta.get("type") == "text_delta":
                            yield LLMStreamChunk(
                                delta=delta.get("text", ""),
                                finish_reason=None,
                            )
                        elif delta.get("type") == "input_json_delta":
                            current_tool_input += delta.get("partial_json", "")

                    elif event_type == "content_block_stop":
                        if current_tool_name:
                            yield LLMStreamChunk(
                                delta="",
                                finish_reason=None,
                                tool_calls=[
                                    LLMToolCall(
                                        id=current_tool_id,
                                        function=LLMToolCallFunction(
                                            name=current_tool_name,
                                            arguments=current_tool_input or "{}",
                                        ),
                                    ),
                                ],
                            )
                            current_tool_id = ""
                            current_tool_name = ""
                            current_tool_input = ""

                    elif event_type == "message_delta":
                        stop_reason = event_data.get("delta", {}).get("stop_reason")
                        if stop_reason:
                            finish_map = {
                                "end_turn": "stop",
                                "tool_use": "tool_calls",
                                "max_tokens": "length",
                            }
                            yield LLMStreamChunk(
                                delta="",
                                finish_reason=finish_map.get(stop_reason, stop_reason),
                            )

                    elif event_type == "message_stop":
                        break

        except httpx.HTTPStatusError as e:
            raise LLMProviderError(
                message=f"Anthropic streaming error: {e.response.text}",
                provider="anthropic",
                model=model,
                is_transient=e.response.status_code >= 500,
            ) from e
        except Exception as e:
            if isinstance(e, LLMProviderError):
                raise
            raise LLMProviderError(
                message=str(e),
                provider="anthropic",
                model=model,
                is_transient=True,
            ) from e

    async def embed(
        self,
        texts: list[str],
        *,
        model: str = "voyage-3",
    ) -> LLMEmbedResult:
        """Anthropic doesn't have a native embedding API — raise clearly."""
        from core.llm.providers import LLMProviderError

        raise LLMProviderError(
            message=(
                "Anthropic does not provide an embedding API. "
                "Use OpenAI or a dedicated embedding provider."
            ),
            provider="anthropic",
            model=model,
        )


__all__ = ["AnthropicAdapter"]
