"""ACP to native event converters.

This module provides conversion from ACP session updates to native llmling-agent
streaming events, enabling ACPAgent to yield the same event types as native agents.

This is the reverse of the conversion done in acp_server/session.py handle_event().
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, overload

from pydantic_ai import PartDeltaEvent
from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta

from acp.schema import (
    AgentMessageChunk,
    AgentPlanUpdate,
    AgentThoughtChunk,
    AudioContentBlock,
    BlobResourceContents,
    EmbeddedResourceContentBlock,
    ImageContentBlock,
    ResourceContentBlock,
    TextContentBlock,
    ToolCallProgress,
    ToolCallStart,
    UserMessageChunk,
)
from llmling_agent.agent.events import (
    DiffContentItem,
    FileEditProgressEvent,
    LocationContentItem,
    PlanUpdateEvent,
    ProcessStartEvent,
    TerminalContentItem,
    ToolCallProgressEvent,
    ToolCallStartEvent,
)


if TYPE_CHECKING:
    from collections.abc import Sequence

    from acp.schema import ContentBlock, SessionUpdate
    from acp.schema.mcp import HttpMcpServer, McpServer, SseMcpServer, StdioMcpServer
    from acp.schema.tool_call import ToolCallContent, ToolCallLocation
    from llmling_agent.agent.events import RichAgentStreamEvent, ToolCallContentItem
    from llmling_agent.models.content import BaseContent
    from llmling_agent_config.mcp_server import (
        MCPServerConfig,
        SSEMCPServerConfig,
        StdioMCPServerConfig,
        StreamableHTTPMCPServerConfig,
    )


def convert_acp_locations(
    locations: list[ToolCallLocation] | None,
) -> list[LocationContentItem]:
    """Convert ACP ToolCallLocation list to native LocationContentItem list."""
    if not locations:
        return []
    return [LocationContentItem(path=loc.path, line=loc.line) for loc in locations]


def convert_acp_content(
    content: list[ToolCallContent] | None,
) -> list[ToolCallContentItem]:
    """Convert ACP ToolCallContent list to native ToolCallContentItem list."""
    if not content:
        return []

    result: list[ToolCallContentItem] = []
    for item in content:
        match item.type:
            case "terminal":
                # TerminalToolCallContent
                result.append(TerminalContentItem(terminal_id=item.terminal_id))
            case "diff":
                # FileEditToolCallContent
                result.append(
                    DiffContentItem(
                        path=item.path,
                        old_text=item.old_text,
                        new_text=item.new_text,
                    )
                )
            case "content":
                # ContentToolCallContent - extract text if present
                if hasattr(item, "content") and isinstance(item.content, TextContentBlock):
                    from llmling_agent.agent.events import TextContentItem

                    result.append(TextContentItem(text=item.content.text))
    return result


def convert_to_acp_content(
    prompts: Sequence[str | BaseContent],
) -> list[ContentBlock]:
    """Convert prompts to ACP ContentBlock format.

    Handles text, images, and audio content types.

    Args:
        prompts: Processed prompts from prepare_prompts/convert_prompts

    Returns:
        List of ACP ContentBlock items
    """
    from llmling_agent.models.content import (
        AudioBase64Content,
        AudioURLContent,
        ImageBase64Content,
        ImageURLContent,
        PDFBase64Content,
        PDFURLContent,
    )

    content_blocks: list[ContentBlock] = []

    for item in prompts:
        match item:
            case str(text):
                content_blocks.append(TextContentBlock(text=text))
            case ImageBase64Content(data=data, mime_type=mime_type):
                content_blocks.append(ImageContentBlock(data=data, mime_type=mime_type))
            case ImageURLContent(url=url):
                # data is required in ImageContentBlock, use ResourceContentBlock for URLs
                content_blocks.append(
                    ResourceContentBlock(
                        uri=url,
                        name="Image",
                        mime_type="image/jpeg",
                    )
                )
            case AudioBase64Content(data=data, format=format_):
                mime_type = f"audio/{format_}" if format_ else "audio/wav"
                content_blocks.append(AudioContentBlock(data=data, mime_type=mime_type))
            case AudioURLContent(url=url):
                # Use ResourceContentBlock for URL-based audio
                content_blocks.append(
                    ResourceContentBlock(
                        uri=url,
                        name="Audio",
                        mime_type="audio/wav",
                        description="Audio content",
                    )
                )
            case PDFBase64Content(data=data):
                # Use EmbeddedResourceContentBlock for base64 PDF content
                blob_resource = BlobResourceContents(
                    blob=data,
                    mime_type="application/pdf",
                    uri=f"data:application/pdf;base64,{data[:50]}...",
                )
                content_blocks.append(EmbeddedResourceContentBlock(resource=blob_resource))
            case PDFURLContent(url=url):
                content_blocks.append(
                    ResourceContentBlock(
                        uri=url,
                        name="PDF",
                        mime_type="application/pdf",
                        description="PDF document",
                    )
                )

    return content_blocks


def acp_to_native_event(update: SessionUpdate) -> RichAgentStreamEvent[Any] | None:  # noqa: PLR0911
    """Convert ACP session update to native streaming event.

    Args:
        update: ACP SessionUpdate from session/update notification

    Returns:
        Corresponding native event, or None if no mapping exists
    """
    match update:
        # Text message chunks -> PartDeltaEvent with TextPartDelta
        case AgentMessageChunk(content=TextContentBlock(text=text)):
            return PartDeltaEvent(index=0, delta=TextPartDelta(content_delta=text))

        # Thought chunks -> PartDeltaEvent with ThinkingPartDelta
        case AgentThoughtChunk(content=TextContentBlock(text=text)):
            return PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=text))

        # User message echo - could emit as PartStartEvent if needed
        case UserMessageChunk():
            return None  # Usually ignored

        # Tool call start -> ToolCallStartEvent
        case ToolCallStart() as tc:
            return ToolCallStartEvent(
                tool_call_id=tc.tool_call_id,
                tool_name=tc.title,  # ACP uses title, not separate tool_name
                title=tc.title,
                kind=tc.kind or "other",
                content=convert_acp_content(list(tc.content) if tc.content else None),
                locations=convert_acp_locations(list(tc.locations) if tc.locations else None),
                raw_input=tc.raw_input or {},
            )

        # Tool call progress -> ToolCallProgressEvent
        case ToolCallProgress() as tc:
            # Check for special content types
            items = convert_acp_content(list(tc.content) if tc.content else None)

            # Check if this is a file edit progress (has diff content)
            for item in items:
                if isinstance(item, DiffContentItem):
                    return FileEditProgressEvent(
                        path=item.path,
                        old_text=item.old_text or "",
                        new_text=item.new_text,
                        status=tc.status or "in_progress",  # type: ignore[arg-type]
                        tool_call_id=tc.tool_call_id,
                    )

            # Check if this is a process/terminal event
            for item in items:
                if isinstance(item, TerminalContentItem):
                    # This is a process-related update
                    return ProcessStartEvent(
                        process_id=item.terminal_id,
                        command=tc.title or "",
                        success=tc.status != "failed",
                        error=str(tc.raw_output) if tc.status == "failed" else None,
                        tool_call_id=tc.tool_call_id,
                    )

            # Generic tool call progress
            return ToolCallProgressEvent(
                tool_call_id=tc.tool_call_id,
                status=tc.status or "in_progress",
                title=tc.title,
                items=items,
                message=str(tc.raw_output) if tc.raw_output else None,
            )

        # Plan update -> PlanUpdateEvent
        case AgentPlanUpdate(entries=entries):
            from llmling_agent.resource_providers.plan_provider import (
                PlanEntry as NativePlanEntry,
            )

            native_entries = [
                NativePlanEntry(
                    content=e.content,
                    priority=e.priority,
                    status=e.status,
                )
                for e in entries
            ]
            return PlanUpdateEvent(entries=native_entries)

        case _:
            return None


def extract_text_from_update(update: SessionUpdate) -> str | None:
    """Extract plain text content from an ACP session update.

    Args:
        update: ACP SessionUpdate

    Returns:
        Text content if this is a text-bearing update, None otherwise
    """
    match update:
        case AgentMessageChunk(content=TextContentBlock(text=text)):
            return text
        case AgentThoughtChunk(content=TextContentBlock(text=text)):
            return text
        case _:
            return None


def is_text_update(update: SessionUpdate) -> bool:
    """Check if this update contains text content."""
    return extract_text_from_update(update) is not None


@overload
def mcp_config_to_acp(config: StdioMCPServerConfig) -> StdioMcpServer | None: ...


@overload
def mcp_config_to_acp(config: SSEMCPServerConfig) -> SseMcpServer | None: ...


@overload
def mcp_config_to_acp(config: StreamableHTTPMCPServerConfig) -> HttpMcpServer | None: ...


@overload
def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None: ...


def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None:
    """Convert llmling MCPServerConfig to ACP McpServer format.

    Args:
        config: llmling-agent MCP server configuration

    Returns:
        ACP-compatible McpServer instance, or None if conversion not possible
    """
    from pydantic import HttpUrl

    from acp.schema.common import EnvVariable
    from acp.schema.mcp import HttpMcpServer, SseMcpServer, StdioMcpServer
    from llmling_agent_config.mcp_server import (
        SSEMCPServerConfig,
        StdioMCPServerConfig,
        StreamableHTTPMCPServerConfig,
    )

    match config:
        case StdioMCPServerConfig(command=command, args=args):
            env_vars = config.get_env_vars() if hasattr(config, "get_env_vars") else {}
            return StdioMcpServer(
                name=config.name or command,
                command=command,
                args=list(args) if args else [],
                env=[EnvVariable(name=k, value=v) for k, v in env_vars.items()],
            )
        case SSEMCPServerConfig(url=url):
            return SseMcpServer(
                name=config.name or str(url),
                url=HttpUrl(str(url)),
                headers=[],
            )
        case StreamableHTTPMCPServerConfig(url=url):
            return HttpMcpServer(
                name=config.name or str(url),
                url=HttpUrl(str(url)),
                headers=[],
            )
        case _:
            return None


def mcp_configs_to_acp(configs: Sequence[MCPServerConfig]) -> list[McpServer]:
    """Convert a sequence of MCPServerConfig to ACP McpServer list.

    Args:
        configs: Sequence of llmling-agent MCP server configurations

    Returns:
        List of ACP-compatible McpServer instances (skips unconvertible configs)
    """
    return [converted for config in configs if (converted := mcp_config_to_acp(config)) is not None]
