#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

"""OpenAI Responses API adapter for Pipecat."""

import copy
from typing import Any, Dict, List, Optional, TypedDict

from openai._types import NotGiven as OpenAINotGiven
from openai.types.responses import FunctionToolParam, ResponseInputItemParam

from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.processors.aggregators.llm_context import (
    LLMContext,
    LLMContextMessage,
    LLMSpecificMessage,
    NotGiven,
)


class OpenAIResponsesLLMInvocationParams(TypedDict, total=False):
    """Context-based parameters for invoking OpenAI Responses API."""

    input: List[ResponseInputItemParam]
    tools: List[FunctionToolParam] | OpenAINotGiven
    instructions: str


class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParams]):
    """OpenAI Responses API adapter for Pipecat.

    Handles:

    - Converting LLMContext messages to Responses API input items
    - Converting Pipecat's standardized tools schema to Responses API function tool format
    - Extracting and sanitizing messages from the LLM context for logging
    """

    @property
    def id_for_llm_specific_messages(self) -> str:
        """Get the identifier used in LLMSpecificMessage instances."""
        return "openai_responses"

    def get_llm_invocation_params(
        self,
        context: LLMContext,
        *,
        system_instruction: Optional[str] = None,
    ) -> OpenAIResponsesLLMInvocationParams:
        """Get Responses API invocation parameters from a universal LLM context.

        Args:
            context: The LLM context containing messages, tools, etc.
            system_instruction: Optional system instruction from service settings.

        Returns:
            Dictionary of parameters for the Responses API.
        """
        messages = self.get_messages(context)

        # Check for conflict: system_instruction + initial system message
        if system_instruction and messages:
            first_msg = messages[0] if not isinstance(messages[0], LLMSpecificMessage) else None
            if first_msg and first_msg.get("role") == "system":
                self._resolve_system_instruction(
                    first_msg.get("content", ""),
                    system_instruction,
                    discard_context_system=False,
                )

        input_items = self._convert_messages_to_input(messages)

        params: OpenAIResponsesLLMInvocationParams = {
            "input": input_items,
            "tools": self.from_standard_tools(context.tools),
        }

        if system_instruction:
            # Compatibility: The Responses API requires at least one input
            # message when instructions are provided. Contexts that worked with
            # OpenAILLMService (system_instruction + empty messages) need the
            # instructions converted to an initial developer message.
            #
            # NOTE: if/when we support `previous_response_id` and/or
            # `conversation_id`, we'll need to revisit this logic, as it'll
            # be legit to provide instructions without input items. Worth
            # noting that OpenAI's docs suggest these parameters are primarily
            # for development convenience rather than performance (the model
            # still processes the full context), and come with the tradeoff
            # of requiring OpenAI-side 30-day conversation storage, which may
            # not be desirable for many users. But it could give folks an easy
            # way to store/switch between conversations without needing to
            # manage that storage themselves.
            if not input_items:
                params["input"] = [{"role": "developer", "content": system_instruction}]
            else:
                params["instructions"] = system_instruction

        return params

    def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[FunctionToolParam]:
        """Convert function schemas to Responses API function tool format.

        Args:
            tools_schema: The Pipecat tools schema to convert.

        Returns:
            List of Responses API function tool definitions.
        """
        functions_schema = tools_schema.standard_tools
        result = []
        for func in functions_schema:
            d = func.to_default_dict()
            tool: FunctionToolParam = {
                "type": "function",
                "name": d["name"],
                "parameters": d.get("parameters", {}),
                "strict": d.get("strict", None),
            }
            if "description" in d:
                tool["description"] = d["description"]
            result.append(tool)
        return result

    def get_messages_for_logging(self, context: LLMContext) -> List[Dict[str, Any]]:
        """Get messages from context in a format ready for logging.

        Removes or truncates sensitive data like image content for safe logging.

        Args:
            context: The LLM context containing messages.

        Returns:
            List of messages in a format ready for logging.
        """
        msgs = []
        for message in self.get_messages(context):
            msg = copy.deepcopy(message)
            if "content" in msg:
                if isinstance(msg["content"], list):
                    for item in msg["content"]:
                        if item.get("type") == "image_url":
                            if item["image_url"]["url"].startswith("data:image/"):
                                item["image_url"]["url"] = "data:image/..."
                        if item.get("type") == "input_audio":
                            item["input_audio"]["data"] = "..."
            msgs.append(msg)
        return msgs

    def _convert_messages_to_input(
        self, messages: List[LLMContextMessage]
    ) -> List[ResponseInputItemParam]:
        """Convert LLMContext messages to Responses API input items.

        Args:
            messages: Messages from the LLMContext.

        Returns:
            List of Responses API input items.
        """
        result: List[ResponseInputItemParam] = []

        for message in messages:
            if isinstance(message, LLMSpecificMessage):
                result.append(message.message)
                continue

            role = message.get("role")

            if role in ("system", "developer"):
                content = message.get("content", "")
                if isinstance(content, list):
                    content = self._convert_multimodal_content(content)
                result.append({"role": "developer", "content": content})

            elif role == "user":
                content = message.get("content", "")
                if isinstance(content, list):
                    content = self._convert_multimodal_content(content)
                result.append({"role": "user", "content": content})

            elif role == "assistant":
                tool_calls = message.get("tool_calls")
                if tool_calls:
                    for tc in tool_calls:
                        func = tc.get("function", {})
                        result.append(
                            {
                                "type": "function_call",
                                "call_id": tc.get("id", ""),
                                "name": func.get("name", ""),
                                "arguments": func.get("arguments", ""),
                            }
                        )
                else:
                    content = message.get("content", "")
                    if isinstance(content, list):
                        content = self._convert_multimodal_content(content)
                    result.append({"role": "assistant", "content": content})

            elif role == "tool":
                content = message.get("content", "")
                if not isinstance(content, str):
                    content = str(content)
                result.append(
                    {
                        "type": "function_call_output",
                        "call_id": message.get("tool_call_id", ""),
                        "output": content,
                    }
                )

        return result

    def _convert_multimodal_content(self, content: list) -> list:
        """Convert multimodal content parts to Responses API format.

        Args:
            content: List of content parts from the LLMContext message.

        Returns:
            List of content parts in Responses API format.
        """
        result = []
        for part in content:
            part_type = part.get("type")
            if part_type == "text":
                result.append({"type": "input_text", "text": part.get("text", "")})
            elif part_type == "image_url":
                image_url_obj = part.get("image_url", {})
                result.append(
                    {
                        "type": "input_image",
                        "image_url": image_url_obj.get("url", ""),
                        "detail": image_url_obj.get("detail", "auto"),
                    }
                )
            else:
                # Pass through other types as-is. Note: "input_audio" is not
                # yet supported by the Responses API (coming soon per OpenAI
                # docs) but the LLMContext format already matches the expected
                # shape, so it should work once support is enabled.
                result.append(part)
        return result
