# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: AGPL-3.0
"""Part type definitions - based on opencode Part design.

Message consists of multiple Parts, each Part has different type and purpose.
"""

from dataclasses import dataclass
from typing import Any, Dict, Literal, Optional, Union


@dataclass
class TextPart:
    """Text content component."""

    text: str = ""
    type: Literal["text"] = "text"


@dataclass
class ContextPart:
    """Context reference component (L0 abstract + URI).

    Used to track which contexts (memory/resource/skill) the message references.
    """

    type: Literal["context"] = "context"
    uri: str = ""
    context_type: Literal["memory", "resource", "skill"] = "memory"
    abstract: str = ""


@dataclass
class ImagePart:
    """Image URL component compatible with OpenAI-style message content."""

    type: Literal["image_url"] = "image_url"
    url: str = ""
    detail: Optional[str] = None


@dataclass
class ToolPart:
    """Tool call component (references tool file within session).

    Tool status: pending | running | completed | error
    """

    type: Literal["tool"] = "tool"
    tool_id: str = ""
    tool_name: str = ""
    tool_uri: str = ""  # viking://user/{user_id}/sessions/{session_id}/tools/{tool_id}
    skill_uri: str = ""  # viking://user/{user_id}/skills/{skill_name}
    tool_input: Optional[dict] = None
    tool_output: str = ""
    tool_status: str = "pending"  # pending | running | completed | error
    duration_ms: Optional[float] = None  # 执行耗时（毫秒）
    prompt_tokens: Optional[int] = None  # 输入 Token
    completion_tokens: Optional[int] = None  # 输出 Token
    tool_output_ref: str = ""
    tool_output_truncated: bool = False
    tool_output_original_chars: Optional[int] = None
    tool_output_preview_chars: Optional[int] = None
    tool_output_sha256: str = ""
    tool_output_storage_uri: str = ""
    tool_output_mime_type: str = "text/plain"
    tool_output_source_ref: str = ""
    tool_output_source_offset: Optional[int] = None
    tool_output_source_limit: Optional[int] = None
    tool_output_externalization_error: str = ""
    tool_output_group_id: str = ""
    tool_output_externalized_reason: str = ""
    tool_output_group_original_chars: Optional[int] = None
    tool_output_group_budget_chars: Optional[int] = None


Part = Union[TextPart, ContextPart, ImagePart, ToolPart]


def _parse_image_url_payload(data: Dict[str, Any]) -> tuple[str, Optional[str]]:
    image_url = data.get("image_url")
    if isinstance(image_url, dict):
        return str(image_url.get("url", "") or ""), image_url.get("detail")
    if isinstance(image_url, str):
        return image_url, None
    return "", None


def part_from_dict(data: Dict[str, Any]) -> Part:
    """Convert a dict to a Part object.

    Args:
        data: Dictionary with part data. Must contain 'type' field.

    Returns:
        Part object (TextPart, ContextPart, or ToolPart)
    """
    part_type = data.get("type", "text")
    if part_type == "text":
        return TextPart(text=data.get("text", ""))
    elif part_type == "context":
        return ContextPart(
            uri=data.get("uri", ""),
            context_type=data.get("context_type", "memory"),
            abstract=data.get("abstract", ""),
        )
    elif part_type == "image_url":
        url, detail = _parse_image_url_payload(data)
        if not url.strip():
            raise ValueError("image_url part requires a non-empty URL")
        return ImagePart(
            url=url,
            detail=detail,
        )
    elif part_type == "tool":
        return ToolPart(
            tool_id=data.get("tool_id", ""),
            tool_name=data.get("tool_name", ""),
            tool_uri=data.get("tool_uri", ""),
            skill_uri=data.get("skill_uri", ""),
            tool_input=data.get("tool_input"),
            tool_output=data.get("tool_output", ""),
            tool_status=data.get("tool_status", "pending"),
            duration_ms=data.get("duration_ms"),
            prompt_tokens=data.get("prompt_tokens"),
            completion_tokens=data.get("completion_tokens"),
            tool_output_ref=data.get("tool_output_ref", ""),
            tool_output_truncated=bool(data.get("tool_output_truncated", False)),
            tool_output_original_chars=data.get("tool_output_original_chars"),
            tool_output_preview_chars=data.get("tool_output_preview_chars"),
            tool_output_sha256=data.get("tool_output_sha256", ""),
            tool_output_storage_uri=data.get("tool_output_storage_uri", ""),
            tool_output_mime_type=data.get("tool_output_mime_type", "text/plain"),
            tool_output_source_ref=data.get("tool_output_source_ref", ""),
            tool_output_source_offset=data.get("tool_output_source_offset"),
            tool_output_source_limit=data.get("tool_output_source_limit"),
            tool_output_externalization_error=data.get("tool_output_externalization_error", ""),
            tool_output_group_id=data.get("tool_output_group_id", ""),
            tool_output_externalized_reason=data.get("tool_output_externalized_reason", ""),
            tool_output_group_original_chars=data.get("tool_output_group_original_chars"),
            tool_output_group_budget_chars=data.get("tool_output_group_budget_chars"),
        )
    else:
        if "text" in data:
            return TextPart(text=str(data.get("text", "") or ""))
        return TextPart(text=str(data))
