"""Anthropic Responses API compatibility layer.

Converts OpenAI Responses-style payloads to Anthropic Messages format and
streams Anthropic events back as OpenAI Responses-style Server-Sent Events.

This enables clients speaking the Responses protocol to use Claude models
with minimal adaptation.
"""

from __future__ import annotations

import json
import logging
import re
import uuid
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any

from ember._internal.exceptions import ProviderAPIError
from ember.models.providers.anthropic._defaults import (
    ANTHROPIC_MAX_TOKENS_DEFAULT,
    max_output_for_model,
)
from ember.models.providers.reasoning_config import translate_reasoning_for_provider
from ember.models.providers.utils import (
    ResponsesContentPart,
    ResponsesRequest,
    parse_responses_request,
)

_LOG = logging.getLogger(__name__)


@dataclass
class AnthropicParams:
    """Parameters for an Anthropic Messages API call."""

    model: str
    messages: list[dict[str, Any]] = field(default_factory=list)
    system: list[dict[str, str]] | None = None
    max_tokens: int = ANTHROPIC_MAX_TOKENS_DEFAULT
    temperature: float | None = None
    top_p: float | None = None
    stop_sequences: tuple[str, ...] = ()
    tools: list[dict[str, Any]] | None = None
    tool_choice: dict[str, Any] | None = None
    thinking: dict[str, Any] | None = None
    output_config: dict[str, Any] | None = None


def validate_responses_payload(payload: Mapping[str, Any]) -> ResponsesRequest:
    """Validate and parse a Responses payload.

    Args:
        payload: Raw Responses request payload.

    Returns:
        Validated ResponsesRequest object.

    Raises:
        ProviderAPIError: When validation fails.
    """
    if not isinstance(payload, Mapping):
        raise ProviderAPIError("Responses payload must be a JSON object")
    try:
        return parse_responses_request(dict(payload))
    except ValueError as exc:
        raise ProviderAPIError(str(exc)) from exc


def build_anthropic_params(request: ResponsesRequest) -> AnthropicParams:
    """Convert a ResponsesRequest into Anthropic API parameters.

    Maps OpenAI Responses conventions (function tools, reasoning effort) to
    their Anthropic equivalents (tool definitions, thinking budgets).

    Args:
        request: Validated Responses request.

    Returns:
        AnthropicParams ready for Anthropic Messages API calls.
    """
    messages = _build_messages(request)
    system = _build_system(request.instructions)
    tools = _convert_tools(request.tools)
    tool_choice = _convert_tool_choice(
        request.tool_choice,
        disable_parallel=request.parallel_tool_calls is False,
    )

    # Route reasoning result by shape:
    #   {"effort": "..."} → output_config (native effort)
    #   {"type": "enabled", "budget_tokens": N} → thinking (explicit budget)
    translated = translate_reasoning_for_provider(
        request.reasoning, "anthropic", request.model,
    )
    thinking: dict[str, Any] | None = None
    output_config: dict[str, Any] | None = None
    if translated is not None:
        if "effort" in translated:
            output_config = translated
        elif "budget_tokens" in translated:
            thinking = translated

    return AnthropicParams(
        model=request.model,
        messages=messages,
        system=system,
        max_tokens=request.max_output_tokens or max_output_for_model(request.model),
        temperature=request.temperature,
        top_p=request.top_p,
        stop_sequences=request.stop_sequences,
        tools=tools,
        tool_choice=tool_choice,
        thinking=thinking,
        output_config=output_config,
    )


def build_completed_responses_payload(
    *,
    response_id: str,
    model: str,
    output_text: str,
    reasoning_summary: str | None,
    tool_calls: Sequence[dict[str, Any]] | None,
    usage: dict[str, int] | None,
) -> dict[str, Any]:
    """Build the final response.completed payload.

    Args:
        response_id: Unique response identifier.
        model: Model name.
        output_text: Accumulated text output.
        reasoning_summary: Accumulated reasoning/thinking text.
        tool_calls: Tool call requests if any.
        usage: Token usage statistics.

    Returns:
        Structured response payload matching Responses API format.
    """
    content_items: list[dict[str, Any]] = []

    if reasoning_summary:
        content_items.append({"type": "reasoning_summary", "text": reasoning_summary})
    if output_text:
        content_items.append({"type": "output_text", "text": output_text})
    if tool_calls:
        for call in tool_calls:
            content_items.append(
                {
                    "type": "tool_use",
                    "id": call.get("id"),
                    "name": call.get("name"),
                    "arguments": call.get("arguments"),
                }
            )

    # Output item format: role + content + status (no "type" field)
    output_item: dict[str, Any] = {
        "role": "assistant",
        "content": content_items,
        "status": "completed",
    }

    response: dict[str, Any] = {
        "id": response_id,
        "model": model,
        "status": "completed",
        "output": [output_item],
        "output_text": output_text,
    }

    if reasoning_summary:
        response["reasoning_summary"] = reasoning_summary

    if usage:
        response["usage"] = usage

    return response


def clone_messages(
    messages: Sequence[Mapping[str, object]],
) -> list[dict[str, Any]]:
    """Deep clone a sequence of messages.

    Args:
        messages: Source messages to clone.

    Returns:
        New list with recursively cloned message dictionaries.
    """
    return [_deep_clone(dict(msg)) for msg in messages]


def stream_events_from_anthropic(
    stream: Iterable[Any],
    model: str,
    response_id: str | None = None,
) -> Iterable[str]:
    """Convert Anthropic streaming events to OpenAI Responses-style SSE.

    Translates content_block_start/delta/stop and message events from the
    Anthropic SDK into the event types expected by Responses API clients.

    Args:
        stream: Iterable of Anthropic streaming events.
        model: Model name for response payloads.
        response_id: Optional response ID; auto-generated if not provided.

    Yields:
        JSON-encoded event strings matching Responses API format.
    """
    if response_id is None:
        response_id = f"resp_{uuid.uuid4().hex[:24]}"

    yield json.dumps(
        {
            "type": "response.created",
            "response": {"id": response_id, "model": model, "status": "in_progress"},
        }
    )

    state = _StreamState()

    for event in stream:
        event_type = _get_event_type(event)

        if event_type == "content_block_start":
            content_block = _get_content_block(event)
            block_type = content_block.get("type") if content_block else None
            if block_type == "tool_use" and content_block is not None:
                tool_id = content_block.get("id", "")
                tool_name = content_block.get("name", "")
                state.start_tool_call(tool_id, tool_name)
                output_index = state.next_output_index()
                # Emit OpenAI-compliant output_item.added event
                yield json.dumps(
                    {
                        "type": "response.output_item.added",
                        "output_index": output_index,
                        "item": {
                            "type": "function_call",
                            "id": tool_id,
                            "call_id": tool_id,
                            "name": tool_name,
                            "arguments": "",
                            "status": "in_progress",
                        },
                        "sequence_number": state.next_sequence(),
                    }
                )

        elif event_type == "content_block_delta":
            content_block = _get_content_block(event)
            delta = _get_delta(event)
            block_type = content_block.get("type") if content_block else None

            if block_type == "thinking":
                text = delta.get("text", "") if delta else ""
                if text:
                    state.reasoning_parts.append(text)
                    yield json.dumps(
                        {
                            "type": "response.reasoning_summary_text.delta",
                            "delta": text,
                        }
                    )

            elif block_type == "text":
                text = delta.get("text", "") if delta else ""
                if text:
                    state.text_parts.append(text)
                    yield json.dumps(
                        {
                            "type": "response.output_text.delta",
                            "delta": text,
                        }
                    )

            elif block_type == "tool_use":
                partial_json = delta.get("partial_json", "") if delta else ""
                if partial_json:
                    state.append_tool_json(partial_json)
                    # Emit OpenAI-compliant function_call_arguments.delta event
                    yield json.dumps(
                        {
                            "type": "response.function_call_arguments.delta",
                            "output_index": state.current_output_index,
                            "call_id": state._current_tool_id,
                            "delta": partial_json,
                            "sequence_number": state.next_sequence(),
                        }
                    )

        elif event_type == "content_block_stop":
            # Emit OpenAI-compliant function_call completion events
            if state._current_tool_id:
                final_args = "".join(state._current_tool_json_parts)
                output_index = state.current_output_index
                # First emit function_call_arguments.done
                yield json.dumps(
                    {
                        "type": "response.function_call_arguments.done",
                        "output_index": output_index,
                        "call_id": state._current_tool_id,
                        "arguments": final_args,
                        "sequence_number": state.next_sequence(),
                    }
                )
                # Then emit output_item.done
                yield json.dumps(
                    {
                        "type": "response.output_item.done",
                        "output_index": output_index,
                        "item": {
                            "type": "function_call",
                            "id": state._current_tool_id,
                            "call_id": state._current_tool_id,
                            "name": state._current_tool_name,
                            "arguments": final_args,
                            "status": "completed",
                        },
                        "sequence_number": state.next_sequence(),
                    }
                )
            state.finalize_tool_call()

        elif event_type == "message_delta":
            # Extract usage from message_delta and emit response.delta
            usage = _get_usage(event)
            if usage:
                input_tokens = usage.get("input_tokens", 0)
                output_tokens = usage.get("output_tokens", 0)
                total = input_tokens + output_tokens
                state.usage = {
                    "prompt_tokens": input_tokens,
                    "completion_tokens": output_tokens,
                    "total_tokens": total,
                    "input_tokens": input_tokens,
                    "output_tokens": output_tokens,
                }
                yield json.dumps(
                    {
                        "type": "response.delta",
                        "usage_delta": state.usage,
                    }
                )

        elif event_type == "message_stop":
            # Handle both dict and object events
            if isinstance(event, Mapping):
                stop_reason = event.get("stop_reason")
            else:
                stop_reason = getattr(event, "stop_reason", None)

            # Build the completed response payload
            response_payload = build_completed_responses_payload(
                response_id=response_id,
                model=model,
                output_text="".join(state.text_parts),
                reasoning_summary="".join(state.reasoning_parts) or None,
                tool_calls=state.tool_calls if state.tool_calls else None,
                usage=state.usage,
            )

            # Set status based on stop reason
            if stop_reason == "tool_use" and state.tool_calls:
                response_payload["status"] = "requires_action"
            else:
                response_payload["status"] = "completed"

            completed_event: dict[str, Any] = {
                "type": "response.completed",
                "response": response_payload,
                "sequence_number": state.next_sequence(),
            }
            # Include usage at top level for easy access
            if state.usage:
                completed_event["usage"] = state.usage
            yield json.dumps(completed_event)


class _StreamState:
    """Mutable state tracker for streaming event conversion.

    Tracks output items and indices for OpenAI Responses API-compliant
    streaming events. Each tool call and text output is tracked as a
    separate output item with proper sequencing.
    """

    def __init__(self) -> None:
        self.text_parts: list[str] = []
        self.reasoning_parts: list[str] = []
        self.tool_calls: list[dict[str, Any]] = []
        self.usage: dict[str, int] | None = None
        self._current_tool_id: str | None = None
        self._current_tool_name: str | None = None
        self._current_tool_json_parts: list[str] = []

        # OpenAI Responses API tracking
        self._output_index: int = -1
        self._sequence_number: int = 0

    def next_sequence(self) -> int:
        """Get next sequence number for event ordering."""
        self._sequence_number += 1
        return self._sequence_number

    def next_output_index(self) -> int:
        """Get next output index for a new output item."""
        self._output_index += 1
        return self._output_index

    @property
    def current_output_index(self) -> int:
        """Get current output index (without incrementing)."""
        return max(0, self._output_index)

    def start_tool_call(self, tool_id: str, tool_name: str) -> None:
        self._current_tool_id = tool_id
        self._current_tool_name = tool_name
        self._current_tool_json_parts = []

    def append_tool_json(self, partial: str) -> None:
        self._current_tool_json_parts.append(partial)

    def finalize_tool_call(self) -> None:
        if self._current_tool_id and self._current_tool_name:
            json_str = "".join(self._current_tool_json_parts)
            try:
                arguments = json.loads(json_str) if json_str else {}
            except json.JSONDecodeError:
                # Log warning with tool context - invalid JSON in tool arguments
                preview = json_str[:100] + "..." if len(json_str) > 100 else json_str
                _LOG.warning(
                    "Invalid JSON in tool call arguments (tool=%s, id=%s): %s",
                    self._current_tool_name,
                    self._current_tool_id,
                    preview,
                )
                # Preserve raw string so downstream can see what was attempted
                arguments = {"_raw_arguments": json_str}

            self.tool_calls.append(
                {
                    "id": self._current_tool_id,
                    "name": self._current_tool_name,
                    "type": "function",
                    "arguments": arguments,
                }
            )

        self._current_tool_id = None
        self._current_tool_name = None
        self._current_tool_json_parts = []


def _parse_data_url(data_url: str) -> tuple[str, str]:
    """Parse a data URL into media type and base64 data.

    Args:
        data_url: Data URL in format "data:image/png;base64,..."

    Returns:
        Tuple of (media_type, base64_data).
    """
    match = re.match(r"data:([^;]+);base64,(.+)", data_url)
    if match:
        return match.group(1), match.group(2)
    return "application/octet-stream", data_url.split(",", 1)[-1]


def _convert_image_url(fields: Mapping[str, Any]) -> dict[str, Any]:
    """Convert OpenAI image_url to Anthropic image format.

    Handles both URL references and base64-encoded data URLs.

    Args:
        fields: The fields dict from a ResponsesContentPart with type "image_url".

    Returns:
        Anthropic-formatted image content block.
    """
    image_url = fields.get("image_url", {})
    url = image_url.get("url", "") if isinstance(image_url, Mapping) else str(image_url)

    if url.startswith("data:"):
        media_type, base64_data = _parse_data_url(url)
        return {
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": media_type,
                "data": base64_data,
            },
        }
    else:
        return {
            "type": "image",
            "source": {
                "type": "url",
                "url": url,
            },
        }


def _convert_content_part(part: ResponsesContentPart) -> dict[str, Any] | None:
    """Convert a single content part from OpenAI Responses to Anthropic format.

    Handles text, images, tool_use, and tool_result content types.
    Unknown types are logged and skipped.

    Args:
        part: Content part from a ResponsesMessage.

    Returns:
        Anthropic-formatted content block, or None if the type is unsupported.
    """
    if part.type in {"input_text", "output_text", "text"}:
        text = part.fields.get("text", "")
        return {"type": "text", "text": text} if text else None

    if part.type == "image_url":
        return _convert_image_url(part.fields)

    if part.type == "image":
        # Already in Anthropic-like format, normalize source structure
        source = part.fields.get("source", {})
        if isinstance(source, Mapping):
            return {"type": "image", "source": dict(source)}
        return None

    if part.type == "tool_use":
        return {
            "type": "tool_use",
            "id": part.fields.get("id", ""),
            "name": part.fields.get("name", ""),
            "input": part.fields.get("arguments", part.fields.get("input", {})),
        }

    if part.type in {"tool_result", "function_call_output"}:
        content = part.fields.get("content", part.fields.get("output", ""))
        return {
            "type": "tool_result",
            "tool_use_id": part.fields.get("tool_use_id", part.fields.get("call_id", "")),
            "content": content if isinstance(content, str) else str(content),
        }

    if part.type == "thinking":
        text = part.fields.get("thinking", part.fields.get("text", ""))
        return {"type": "thinking", "thinking": text} if text else None

    _LOG.warning(
        "Dropping unsupported content type '%s'; data in this block will be lost",
        part.type,
    )
    return None


def _build_messages(request: ResponsesRequest) -> list[dict[str, Any]]:
    """Convert Responses input/messages to Anthropic message format.

    Handles all content types: text, images, tool_use, tool_result.
    Messages with no convertible content are skipped.
    """
    messages: list[dict[str, Any]] = []

    if isinstance(request.input, list):
        for msg in request.input:
            content_blocks = []
            for part in msg.content:
                converted = _convert_content_part(part)
                if converted:
                    content_blocks.append(converted)
            if content_blocks:
                messages.append({"role": msg.role, "content": content_blocks})
    elif isinstance(request.input, str):
        messages.append(
            {
                "role": "user",
                "content": [{"type": "text", "text": request.input}],
            }
        )

    for msg in request.messages:
        content_blocks = []
        for part in msg.content:
            converted = _convert_content_part(part)
            if converted:
                content_blocks.append(converted)
        if content_blocks:
            messages.append({"role": msg.role, "content": content_blocks})

    return messages


def _build_system(instructions: str | None) -> list[dict[str, str]] | None:
    """Convert instructions to Anthropic system format."""
    if not instructions:
        return None
    return [{"type": "text", "text": instructions}]


def _convert_tools(
    tools: Sequence[Mapping[str, Any]] | None,
) -> list[dict[str, Any]] | None:
    """Convert OpenAI function-style tools to Anthropic format.

    Handles both nested Chat Completions and flat Responses formats:
    - Nested: {"type": "function", "function": {"name": "X", ...}}
    - Flat: {"type": "function", "name": "X", ...}

    As of Jan 2025, Anthropic requires tools to have explicit type fields:
    - 'bash_20250124' for bash/shell tools
    - 'text_editor_20250124' (or later versions) for text editing tools
    - 'web_search_20250305' for web search
    - 'custom' for all other user-defined tools

    This function converts OpenAI function-calling format to Anthropic's
    custom tool format.
    """
    if not tools:
        return None

    converted: list[dict[str, Any]] = []
    for tool in tools:
        tool_type = tool.get("type")
        if tool_type == "function":
            # Check flat format first (Responses API), then nested (Chat Completions)
            name = tool.get("name")
            description = tool.get("description")
            parameters = tool.get("parameters")

            if not name:
                # Nested format: {"type": "function", "function": {...}}
                func = tool.get("function", {})
                name = func.get("name", "")
                description = func.get("description", "")
                parameters = func.get("parameters", {})

            # Convert to Anthropic custom tool format
            # Ensure input_schema is a valid JSON Schema (must have "type" field)
            input_schema = parameters if parameters else {"type": "object"}
            converted.append(
                {
                    "type": "custom",
                    "name": name or "",
                    "description": description or "",
                    "input_schema": input_schema,
                }
            )
        elif tool_type in (
            "bash_20250124",
            "text_editor_20250124",
            "text_editor_20250429",
            "text_editor_20250728",
            "web_search_20250305",
            "custom",
        ):
            # Already in Anthropic format, pass through
            converted.append(dict(tool))
        else:
            # Unknown type - check if it has function-like fields (flat format without type)
            if "name" in tool:
                # Ensure input_schema is a valid JSON Schema (must have "type" field)
                input_schema = tool.get("parameters") or {"type": "object"}
                converted.append(
                    {
                        "type": "custom",
                        "name": tool.get("name", ""),
                        "description": tool.get("description", ""),
                        "input_schema": input_schema,
                    }
                )
            else:
                # Truly unknown, add type: custom and preserve other fields
                tool_dict = dict(tool)
                tool_dict["type"] = "custom"
                converted.append(tool_dict)

    return converted if converted else None


def _convert_tool_choice(
    tool_choice: Mapping[str, Any] | str | None,
    *,
    disable_parallel: bool,
) -> dict[str, Any] | None:
    """Convert OpenAI tool_choice to Anthropic format.

    OpenAI semantics:
    - "auto": Model decides whether to use tools (default)
    - "none": Explicitly disable tool use
    - "required": Model must use at least one tool
    - {"type": "function", "name": "..."}: Force specific tool

    Anthropic semantics:
    - {"type": "auto"}: Model decides (default when tool_choice omitted)
    - {"type": "none"}: Explicitly disable tool use
    - {"type": "any"}: Must use at least one tool
    - {"type": "tool", "name": "..."}: Force specific tool
    """
    if tool_choice is None:
        # Even without explicit tool_choice, emit "auto" if disable_parallel is set
        # so Anthropic receives the disable_parallel_tool_use flag.
        if disable_parallel:
            return {"type": "auto", "disable_parallel_tool_use": True}
        return None

    if isinstance(tool_choice, str):
        if tool_choice == "auto":
            return {"type": "auto", "disable_parallel_tool_use": disable_parallel}
        if tool_choice == "none":
            # Anthropic now supports type: "none" directly
            return {"type": "none"}
        if tool_choice == "required":
            return {"type": "any", "disable_parallel_tool_use": disable_parallel}
        return None

    choice_type = tool_choice.get("type")
    if choice_type == "function":
        name = tool_choice.get("name") or tool_choice.get("function", {}).get("name")
        return {
            "type": "tool",
            "name": name,
            "disable_parallel_tool_use": disable_parallel,
        }

    return dict(tool_choice)


def _get_event_type(event: Any) -> str:
    """Extract event type from an Anthropic streaming event."""
    if isinstance(event, Mapping):
        return str(event.get("type", ""))
    return str(getattr(event, "type", ""))


def _get_content_block(event: Any) -> dict[str, Any] | None:
    """Extract content_block from an Anthropic streaming event."""
    if isinstance(event, Mapping):
        block = event.get("content_block")
    else:
        block = getattr(event, "content_block", None)

    if isinstance(block, Mapping):
        return dict(block)
    return None


def _get_delta(event: Any) -> dict[str, Any] | None:
    """Extract delta from an Anthropic streaming event."""
    if isinstance(event, Mapping):
        delta = event.get("delta")
    else:
        delta = getattr(event, "delta", None)

    if isinstance(delta, Mapping):
        return dict(delta)
    return None


def _get_usage(event: Any) -> dict[str, Any] | None:
    """Extract usage from an Anthropic streaming event."""
    if isinstance(event, Mapping):
        usage = event.get("usage")
    else:
        usage = getattr(event, "usage", None)

    if isinstance(usage, Mapping):
        return dict(usage)
    return None


def _deep_clone(obj: Any) -> Any:
    """Recursively clone nested dicts and lists."""
    if isinstance(obj, dict):
        return {k: _deep_clone(v) for k, v in obj.items()}
    if isinstance(obj, list):
        return [_deep_clone(item) for item in obj]
    return obj


__all__ = [
    "AnthropicParams",
    "build_anthropic_params",
    "build_completed_responses_payload",
    "clone_messages",
    "stream_events_from_anthropic",
    "validate_responses_payload",
]
