"""Anthropic response decoding."""

import json
from typing import Any, TypeAlias, cast

from anthropic import types as anthropic_types
from anthropic.lib.streaming import AsyncMessageStreamManager, MessageStreamManager

from ....content import (
    AssistantContentPart,
    Text,
    TextChunk,
    TextEndChunk,
    TextStartChunk,
    Thought,
    ThoughtChunk,
    ThoughtEndChunk,
    ThoughtStartChunk,
    ToolCall,
    ToolCallChunk,
    ToolCallEndChunk,
    ToolCallStartChunk,
)
from ....messages import AssistantMessage
from ....responses import (
    AsyncChunkIterator,
    ChunkIterator,
    FinishReason,
    FinishReasonChunk,
    RawMessageChunk,
    RawStreamEventChunk,
)
from ..model_ids import AnthropicModelId

ANTHROPIC_FINISH_REASON_MAP = {
    "max_tokens": FinishReason.MAX_TOKENS,
    "refusal": FinishReason.REFUSAL,
}


def _decode_assistant_content(
    content: anthropic_types.ContentBlock,
) -> AssistantContentPart:
    """Convert Anthropic content block to mirascope AssistantContentPart."""
    if content.type == "text":
        return Text(text=content.text)
    elif content.type == "tool_use":
        return ToolCall(
            id=content.id,
            name=content.name,
            args=json.dumps(content.input),
        )
    elif content.type == "thinking":
        return Thought(thought=content.thinking)
    else:
        raise NotImplementedError(
            f"Support for content type `{content.type}` is not yet implemented."
        )


def decode_response(
    response: anthropic_types.Message,
    model_id: AnthropicModelId,
) -> tuple[AssistantMessage, FinishReason | None]:
    """Convert Anthropic message to mirascope AssistantMessage."""
    assistant_message = AssistantMessage(
        content=[_decode_assistant_content(part) for part in response.content],
        provider="anthropic",
        model_id=model_id,
        raw_message={
            "role": response.role,
            "content": [part.model_dump() for part in response.content],
        },
    )
    finish_reason = (
        ANTHROPIC_FINISH_REASON_MAP.get(response.stop_reason)
        if response.stop_reason
        else None
    )
    return assistant_message, finish_reason


ContentBlock: TypeAlias = (
    anthropic_types.TextBlockParam
    | anthropic_types.ThinkingBlockParam
    | anthropic_types.ToolUseBlockParam
    | anthropic_types.ThinkingBlockParam
    | anthropic_types.RedactedThinkingBlockParam
)


class _AnthropicChunkProcessor:
    """Processes Anthropic stream events and maintains state across events."""

    def __init__(self) -> None:
        self.current_block_param: ContentBlock | None = None
        self.accumulated_tool_json: str = ""
        self.accumulated_blocks: list[ContentBlock] = []

    def process_event(
        self, event: anthropic_types.RawMessageStreamEvent
    ) -> ChunkIterator:
        """Process a single Anthropic event and yield the appropriate content chunks."""
        yield RawStreamEventChunk(raw_stream_event=event)

        if event.type == "content_block_start":
            content_block = event.content_block

            if content_block.type == "text":
                self.current_block_param = {
                    "type": "text",
                    "text": content_block.text,
                }
                yield TextStartChunk()
            elif content_block.type == "tool_use":
                self.current_block_param = {
                    "type": "tool_use",
                    "id": content_block.id,
                    "name": content_block.name,
                    "input": {},
                }
                self.accumulated_tool_json = ""
                yield ToolCallStartChunk(
                    id=content_block.id,
                    name=content_block.name,
                )
            elif content_block.type == "thinking":
                self.current_block_param = {
                    "type": "thinking",
                    "thinking": "",
                    "signature": "",
                }
                yield ThoughtStartChunk()
            elif content_block.type == "redacted_thinking":  # pragma: no cover
                self.current_block_param = {
                    "type": "redacted_thinking",
                    "data": content_block.data,
                }
            else:
                raise NotImplementedError

        elif event.type == "content_block_delta":
            if self.current_block_param is None:  # pragma: no cover
                raise RuntimeError("Received delta without a current block")

            delta = event.delta
            if delta.type == "text_delta":
                if self.current_block_param["type"] != "text":  # pragma: no cover
                    raise RuntimeError(
                        f"Received text_delta for {self.current_block_param['type']} block"
                    )
                self.current_block_param["text"] += delta.text
                yield TextChunk(delta=delta.text)
            elif delta.type == "input_json_delta":
                if self.current_block_param["type"] != "tool_use":  # pragma: no cover
                    raise RuntimeError(
                        f"Received input_json_delta for {self.current_block_param['type']} block"
                    )
                self.accumulated_tool_json += delta.partial_json
                yield ToolCallChunk(delta=delta.partial_json)
            elif delta.type == "thinking_delta":
                if self.current_block_param["type"] != "thinking":  # pragma: no cover
                    raise RuntimeError(
                        f"Received thinking_delta for {self.current_block_param['type']} block"
                    )
                self.current_block_param["thinking"] += delta.thinking
                yield ThoughtChunk(delta=delta.thinking)
            elif delta.type == "signature_delta":
                if self.current_block_param["type"] != "thinking":  # pragma: no cover
                    raise RuntimeError(
                        f"Received signature_delta for {self.current_block_param['type']} block"
                    )
                self.current_block_param["signature"] += delta.signature
            else:
                raise RuntimeError(
                    f"Received unsupported delta type: {delta.type}"
                )  # pragma: no cover

        elif event.type == "content_block_stop":
            if self.current_block_param is None:  # pragma: no cover
                raise RuntimeError("Received stop without a current block")

            block_type = self.current_block_param["type"]

            if block_type == "text":
                yield TextEndChunk()
            elif block_type == "tool_use":
                if self.current_block_param["type"] != "tool_use":  # pragma: no cover
                    raise RuntimeError(
                        f"Block type mismatch: stored {self.current_block_param['type']}, expected tool_use"
                    )
                self.current_block_param["input"] = (
                    json.loads(self.accumulated_tool_json)
                    if self.accumulated_tool_json
                    else {}
                )
                yield ToolCallEndChunk()
            elif block_type == "thinking":
                yield ThoughtEndChunk()
            else:
                raise NotImplementedError

            self.accumulated_blocks.append(self.current_block_param)
            self.current_block_param = None

        elif event.type == "message_delta":
            if event.delta.stop_reason:
                finish_reason = ANTHROPIC_FINISH_REASON_MAP.get(event.delta.stop_reason)
                if finish_reason is not None:
                    yield FinishReasonChunk(finish_reason=finish_reason)

    def raw_message_chunk(self) -> RawMessageChunk:
        return RawMessageChunk(
            raw_message=cast(
                dict[str, Any],
                {
                    "role": "assistant",
                    "content": self.accumulated_blocks,
                },
            )
        )


def decode_stream(
    anthropic_stream_manager: MessageStreamManager,
) -> ChunkIterator:
    """Returns a ChunkIterator converted from an Anthropic MessageStreamManager"""
    processor = _AnthropicChunkProcessor()
    with anthropic_stream_manager as stream:
        for event in stream._raw_stream:
            yield from processor.process_event(event)
    yield processor.raw_message_chunk()


async def decode_async_stream(
    anthropic_stream_manager: AsyncMessageStreamManager,
) -> AsyncChunkIterator:
    """Returns an AsyncChunkIterator converted from an Anthropic MessageStreamManager"""
    processor = _AnthropicChunkProcessor()
    async with anthropic_stream_manager as stream:
        async for event in stream._raw_stream:
            for item in processor.process_event(event):
                yield item
    yield processor.raw_message_chunk()
