# -*- coding: utf-8 -*-
"""Extract attributes from AgentScope components for OpenTelemetry tracing."""
import inspect
from typing import Any, Dict, TYPE_CHECKING

from ...message import Msg, ToolCallBlock

from ._attributes import (
    SpanAttributes,
    OperationNameValues,
    ProviderNameValues,
)
from ._converter import _convert_block_to_part
from ._utils import _serialize_to_str
from ...model import ChatResponse, ChatModelBase
from ...event import (
    ExternalExecutionResultEvent,
    UserConfirmResultEvent,
)

if TYPE_CHECKING:
    from ...agent import Agent
    from ...tool import Toolkit, ToolChoice

_CLASS_NAME_MAP = {
    "dashscope": ProviderNameValues.DASHSCOPE,
    "openai": ProviderNameValues.OPENAI,
    "anthropic": ProviderNameValues.ANTHROPIC,
    "gemini": ProviderNameValues.GCP_GEMINI,
    "ollama": ProviderNameValues.OLLAMA,
    "deepseek": ProviderNameValues.DEEPSEEK,
    "xai": ProviderNameValues.XAI,
    "moonshot": ProviderNameValues.MOONSHOT,
}

# Map base URL fragments to provider names for OpenAI-compatible APIs
_BASE_URL_PROVIDER_MAP = [
    ("api.openai.com", ProviderNameValues.OPENAI),
    ("dashscope", ProviderNameValues.DASHSCOPE),
    ("deepseek", ProviderNameValues.DEEPSEEK),
    ("moonshot", ProviderNameValues.MOONSHOT),
    ("generativelanguage.googleapis.com", ProviderNameValues.GCP_GEMINI),
    ("openai.azure.com", ProviderNameValues.AZURE_AI_OPENAI),
    ("amazonaws.com", ProviderNameValues.AWS_BEDROCK),
    ("api.x.ai", ProviderNameValues.XAI),
]


def _get_common_attributes(session_id: str = "") -> Dict[str, str]:
    """Get common attributes for all spans.

    Args:
        session_id (`str`):
            The session ID to set as conversation ID.

    Returns:
        `Dict[str, str]`:
            Common span attributes including conversation ID.
    """
    return {
        SpanAttributes.GEN_AI_CONVERSATION_ID: (
            session_id if session_id else "[no_session_id]"
        ),
    }


def _get_provider_name(instance: "ChatModelBase") -> str:
    """Get provider name from ChatModelBase instance.

    Maps ChatModelBase class names to provider names, with special handling
    for OpenAI-compatible APIs that may use different base URLs.
    This follows the implementation pattern from agentscope-java PR #73.

    Args:
        instance (`ChatModelBase`):
            The chat model instance to get the provider name for.

    Returns:
        `str`:
            Provider name (e.g., "openai", "dashscope", "anthropic")
    """
    classname = instance.__class__.__name__

    # For other model types, use direct mapping
    prefix_key = (
        classname.removesuffix("ChatModel")
        .removesuffix("MultiAgentModel")
        .removesuffix("ResponseModel")
        .lower()
    )

    # Special handling for OpenAI-compatible models — inspect base_url
    # from credential to distinguish the actual provider.
    if prefix_key == "openai":
        base_url = getattr(instance.credential, "base_url", None)
        if base_url:
            base_url = str(base_url)
            for url_fragment, provider_name in _BASE_URL_PROVIDER_MAP:
                if url_fragment in base_url:
                    return provider_name
        return ProviderNameValues.OPENAI

    return _CLASS_NAME_MAP.get(prefix_key, "unknown")


def _get_tool_definitions(
    tools: list[dict[str, Any]] | None,
    tool_choice: "ToolChoice | None",
) -> str | None:
    """Extract and serialize tool definitions for tracing.

    Converts AgentScope/OpenAI nested tool format to OpenTelemetry GenAI
    flat format for tracing.

    Args:
        tools (`list[dict[str, Any]] | None`, optional):
            List of tool definitions in OpenAI format with nested
            structure: ``[{"type": "function", "function": {...}}]``
        tool_choice (`ToolChoice | None`, optional):
            Tool choice configuration with ``mode`` and optional ``tools``
            fields. If mode is ``"none"``, returns None to indicate tools
            should not be traced.

    Returns:
        `str | None`:
            Serialized tool definitions in flat format:
            ``[{"type": "function", "name": ..., "parameters": ...}]``
            or None if tools should not be traced (e.g., tools is None/empty
            or tool_choice is "none").
    """
    # No tools provided
    if tools is None or not isinstance(tools, list) or len(tools) == 0:
        return None

    # Tool choice is explicitly "none" (model should not use tools)
    if tool_choice is not None and tool_choice.mode == "none":
        return None

    try:
        # Convert nested format to flat format for OpenTelemetry GenAI
        # TODO: Currently only supports "function" type tools. If other tool
        # types are added in the future (e.g., "retrieval", "code_interpreter",
        # "browser"), this conversion logic needs to be updated to handle them.
        flat_tools = []
        for tool in tools:
            if not isinstance(tool, dict) or "function" not in tool:
                continue

            func_def = tool["function"]
            flat_tool = {
                "type": tool.get("type", "function"),
                "name": func_def.get("name"),
                "description": func_def.get("description"),
                "parameters": func_def.get("parameters"),
            }
            # Remove None values
            flat_tool = {k: v for k, v in flat_tool.items() if v is not None}
            flat_tools.append(flat_tool)

        if flat_tools:
            return _serialize_to_str(flat_tools)
        return None

    except Exception:
        return None


def _get_llm_request_attributes(
    instance: "ChatModelBase",
    kwargs: Dict[str, Any],
) -> Dict[str, Any]:
    """Get LLM request attributes for OpenTelemetry tracing.

    Extracts request parameters from LLM model calls into GenAI attributes.

    Args:
        instance (`ChatModelBase`):
            The chat model instance making the request.
        kwargs (`Dict[str, Any]`):
            Keyword arguments including generation parameters such as
            temperature, top_p, top_k, max_tokens, presence_penalty,
            frequency_penalty, stop_sequences, seed, tools, and tool_choice.

    Returns:
        `Dict[str, Any]`:
            OpenTelemetry GenAI attributes with mixed-type values (``str``,
            ``int``, ``float``, or ``list``), including operation name,
            provider name, model name, generation parameters (e.g.
            temperature, max_tokens, stop_sequences), and tool definitions.
    """

    attributes = {
        # required attributes
        SpanAttributes.GEN_AI_OPERATION_NAME: OperationNameValues.CHAT,
        SpanAttributes.GEN_AI_PROVIDER_NAME: _get_provider_name(instance),
        # conditionally required attributes
        SpanAttributes.GEN_AI_REQUEST_MODEL: instance.model,
        # recommended attributes
        SpanAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get("temperature"),
        SpanAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get("p")
        or kwargs.get("top_p"),
        SpanAttributes.GEN_AI_REQUEST_TOP_K: kwargs.get("top_k"),
        SpanAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"),
        SpanAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get(
            "presence_penalty",
        ),
        SpanAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get(
            "frequency_penalty",
        ),
        SpanAttributes.GEN_AI_REQUEST_STOP_SEQUENCES: kwargs.get(
            "stop_sequences",
        ),
        SpanAttributes.GEN_AI_REQUEST_SEED: kwargs.get("seed"),
    }

    # Extract tool definitions if provided
    tool_definitions = _get_tool_definitions(
        tools=kwargs.get("tools"),
        tool_choice=kwargs.get("tool_choice"),
    )
    if tool_definitions:
        attributes[SpanAttributes.GEN_AI_TOOL_DEFINITIONS] = tool_definitions

    return {k: v for k, v in attributes.items() if v is not None}


def _get_llm_span_name(attributes: Dict[str, str]) -> str:
    """Generate span name for LLM operations.

    Args:
        attributes (`Dict[str, str]`):
            LLM request attributes dictionary containing operation name and
            model name.

    Returns:
        `str`:
            Formatted span name in the format "{operation} {model}",
            e.g., "chat gpt-4" or "chat qwen-plus".
    """
    return (
        f"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} "
        f"{attributes[SpanAttributes.GEN_AI_REQUEST_MODEL]}"
    )


def _get_llm_output_messages(
    chat_response: ChatResponse | None,
) -> list[dict[str, Any]]:
    """Extract and format LLM output messages for tracing.

    Converts ChatResponse objects to standardized message format compatible
    with OpenTelemetry GenAI specification.

    Args:
        chat_response (` ChatResponse | None`):
            Chat response object with content blocks. Should be a ChatResponse
            instance containing content blocks (text, tool_use, etc.).

    Returns:
        `list[dict[str, Any]]`:
            List containing a single formatted message dictionary with role,
            parts, and finish_reason. Returns the original response if it's
            not a ChatResponse instance, or an error message format if
            conversion fails.
    """
    try:
        if not isinstance(chat_response, ChatResponse):
            return [
                {
                    "role": "assistant",
                    "parts": [
                        {
                            "type": "text",
                            "content": str(chat_response),
                        },
                    ],
                    "finish_reason": "unknown",
                },
            ]

        parts = []
        finish_reason = "stop"  # Default finish reason

        for block in chat_response.content:
            part = _convert_block_to_part(block)
            if part:
                parts.append(part)

        output_message = {
            "role": "assistant",
            "parts": parts,
            "finish_reason": finish_reason,
        }

        return [output_message]

    except Exception:
        return [
            {
                "role": "assistant",
                "parts": [
                    {
                        "type": "text",
                        "content": "<error processing response>",
                    },
                ],
                "finish_reason": "error",
            },
        ]


def _get_llm_response_attributes(
    chat_response: ChatResponse | None,
) -> Dict[str, Any]:
    """Get LLM response attributes for OpenTelemetry tracing.

    Extracts response metadata and formats into GenAI attributes.

    Args:
        chat_response (`ChatResponse | None`):
            Chat response object with data and usage info. Should have
            attributes like id, usage (with input_tokens and output_tokens),
            and content blocks.

    Returns:
        `Dict[str, Any]`:
            OpenTelemetry GenAI response attributes including response ID,
            finish reasons, token usage (input/output tokens), and output
            messages.
    """
    attributes = {
        SpanAttributes.GEN_AI_RESPONSE_ID: getattr(
            chat_response,
            "id",
            "unknown_id",
        ),
        # FIXME: finish reason should be capture in chat response
        SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS: '["stop"]',
    }
    if hasattr(chat_response, "usage") and chat_response.usage:
        usage = chat_response.usage
        attributes[
            SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS
        ] = usage.input_tokens
        attributes[
            SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS
        ] = usage.output_tokens

        cache_input = usage.cache_input_tokens
        if cache_input:
            attributes[
                SpanAttributes.AGENTSCOPE_CACHE_INPUT_TOKENS
            ] = cache_input

        cache_creation = usage.cache_creation_input_tokens
        if cache_creation:
            attributes[
                SpanAttributes.AGENTSCOPE_CACHE_CREATION_INPUT_TOKENS
            ] = cache_creation

    output_messages = _get_llm_output_messages(chat_response)
    if output_messages:
        attributes[SpanAttributes.GEN_AI_OUTPUT_MESSAGES] = _serialize_to_str(
            output_messages,
        )

    return attributes


def _get_agent_messages(
    msg: Msg | list[Msg],
) -> list[dict[str, Any]]:
    """Convert AgentScope message(s) to standardized parts format.

    Transforms Msg objects into OpenTelemetry GenAI format.

    Args:
        msg (`Msg | list[Msg]`):
            AgentScope message object or list of message objects with
            content blocks.

    Returns:
        `list[dict[str, Any]]`:
            List of formatted message dictionaries with role, parts, name,
            and finish_reason.
    """
    try:
        if isinstance(msg, Msg):
            msg = [msg]

        formatted_msgs = []
        for m in msg:
            parts = []
            for block in m.get_content_blocks():
                part = _convert_block_to_part(block)
                if part:
                    parts.append(part)
            formatted_msg = {
                "role": m.role,
                "parts": parts,
                "name": m.name,
                "finish_reason": "stop",
            }
            formatted_msgs.append(formatted_msg)

        return formatted_msgs
    except Exception:
        # Fallback: try simple attribute access on the original object.
        # If msg was already converted to a list or lacks role/name, return
        # an empty list rather than raising a secondary exception.
        try:
            single = msg[0] if isinstance(msg, list) else msg
            return [
                {
                    "role": single.role,
                    "parts": [
                        {
                            "type": "text",
                            "content": (
                                str(single.content) if single.content else ""
                            ),
                        },
                    ],
                    "name": single.name,
                    "finish_reason": "stop",
                },
            ]
        except Exception:
            return []


def _get_agent_request_attributes(
    instance: "Agent",
    kwargs: Dict[str, Any],
) -> Dict[str, str]:
    """Get agent request attributes for OpenTelemetry tracing.

    Extracts agent metadata and input data into GenAI attributes.

    Args:
        instance (`Agent`):
            The agent instance making the request.
        kwargs (`Dict[str, Any]`):
            Keyword arguments passed to the agent's reply method.

    Returns:
        `Dict[str, str]`:
            OpenTelemetry GenAI attributes including operation name, agent ID,
            agent name, agent description, and input messages (if provided).
    """
    attributes = {
        SpanAttributes.GEN_AI_OPERATION_NAME: (
            OperationNameValues.INVOKE_AGENT
        ),
        SpanAttributes.GEN_AI_AGENT_NAME: instance.name,
        SpanAttributes.GEN_AI_AGENT_DESCRIPTION: inspect.getdoc(
            instance.__class__,
        )
        or "No description available",
    }

    inputs = kwargs.get("inputs")
    if inputs is not None:
        if isinstance(inputs, (Msg, list)):
            input_messages = _get_agent_messages(inputs)
            attributes[
                SpanAttributes.GEN_AI_INPUT_MESSAGES
            ] = _serialize_to_str(input_messages)
        elif isinstance(inputs, UserConfirmResultEvent):
            attributes[
                SpanAttributes.AGENTSCOPE_INCOMING_EVENT_TYPE
            ] = "user_confirm_result"
        elif isinstance(inputs, ExternalExecutionResultEvent):
            attributes[
                SpanAttributes.AGENTSCOPE_INCOMING_EVENT_TYPE
            ] = "external_execution_result"

    return attributes


def _get_agent_span_name(attributes: Dict[str, str]) -> str:
    """Generate span name for agent operations.

    Args:
        attributes (`Dict[str, str]`):
            Agent request attributes dictionary containing operation name and
            agent name.

    Returns:
        `str`:
            Formatted span name in the format "{operation} {agent_name}",
            e.g., "invoke_agent MyAgent".
    """
    return (
        f"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} "
        f"{attributes[SpanAttributes.GEN_AI_AGENT_NAME]}"
    )


def _get_agent_response_attributes(
    agent_response: Msg,
) -> Dict[str, str]:
    """Get agent response attributes for OpenTelemetry tracing.

    Args:
        agent_response (`Msg`):
            Response message returned by agent, containing content blocks.

    Returns:
        `Dict[str, str]`:
            OpenTelemetry GenAI response attributes including output messages.
    """
    attributes = {
        SpanAttributes.GEN_AI_OUTPUT_MESSAGES: _serialize_to_str(
            _get_agent_messages(agent_response),
        ),
    }
    return attributes


def _get_tool_request_attributes(
    instance: "Toolkit",
    tool_call: ToolCallBlock,
) -> Dict[str, str]:
    """Get tool request attributes for OpenTelemetry tracing.

    Extracts tool execution metadata into GenAI attributes.

    Args:
        instance (`Toolkit`):
            Toolkit instance with tool definitions. Used to extract tool
            description from the tool's JSON schema.
        tool_call (`ToolCallBlock`):
            Tool use block with call information including id, name, and input
            arguments.

    Returns:
        `Dict[str, str]`:
            OpenTelemetry GenAI tool attributes including operation name, tool
            call ID, tool name, tool description (if available), and tool call
            arguments.
    """
    attributes = {
        SpanAttributes.GEN_AI_OPERATION_NAME: (
            OperationNameValues.EXECUTE_TOOL
        ),
    }

    if tool_call:
        tool_name = tool_call.name
        attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID] = tool_call.id
        attributes[SpanAttributes.GEN_AI_TOOL_NAME] = tool_name
        # tool_call.input is already a JSON string; pass it directly to avoid
        # double-encoding (e.g. '{"city": "Beijing"}' → '"{\\"city\\"...}"')
        attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS] = tool_call.input

        if tool_name:
            registered = getattr(instance, "tools", {}).get(tool_name)
            if registered is not None:
                tool_obj = getattr(registered, "tool", None)
                description = getattr(tool_obj, "description", None)
                if description:
                    attributes[
                        SpanAttributes.GEN_AI_TOOL_DESCRIPTION
                    ] = description

    return attributes


def _get_tool_span_name(attributes: Dict[str, str]) -> str:
    """Generate span name for tool operations.

    Args:
        attributes (`Dict[str, str]`):
            Tool request attributes dictionary containing operation name and
            tool name.

    Returns:
        `str`:
            Formatted span name in the format "{operation} {tool_name}",
            e.g., "execute_tool search".
    """
    return (
        f"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} "
        f"{attributes[SpanAttributes.GEN_AI_TOOL_NAME]}"
    )


def _get_tool_response_attributes(
    tool_response: Any,
) -> Dict[str, str]:
    """Get tool response attributes for OpenTelemetry tracing.

    Args:
        tool_response (`Any`):
            Response object from tool execution. Can be any serializable object
            returned by the tool function.

    Returns:
        `Dict[str, str]`:
            OpenTelemetry GenAI response attributes including tool call result.
    """
    attributes = {
        SpanAttributes.GEN_AI_TOOL_CALL_RESULT: _serialize_to_str(
            tool_response,
        ),
    }
    return attributes
