#
# Copyright 2026 The Dapr Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from dapr_agents.types.llm import DaprInferenceClientConfig
from dapr_agents.llm.base import LLMClientBase
from dapr.clients import DaprClient
from dapr.clients.grpc import conversation as dapr_conversation
from typing import Dict, Any, List, Optional
from pydantic import model_validator
from google.protobuf import json_format
from google.protobuf.struct_pb2 import Struct as GrpcStruct

import json
import logging

logger = logging.getLogger(__name__)


class DaprInferenceClient:
    def __init__(self):
        pass  # No persistent client - use per-call context manager

    def get_metadata(self):
        """Fetch Dapr sidecar metadata using a fresh per-call client."""
        with DaprClient() as client:
            return client.get_metadata()

    # ──────────────────────────────────────────────────────────────────────────
    # Alpha2 (Tool Calling) support
    # ──────────────────────────────────────────────────────────────────────────
    def _convert_openai_tools_to_conversation_tools(
        self, tools: Optional[List[Dict[str, Any]]]
    ) -> Optional[List[dapr_conversation.ConversationTools]]:
        """
        Convert OpenAI-style tools (type=function, function={name, description, parameters})
        into Dapr ConversationTools objects for Alpha2.
        """
        if not tools:
            return None
        converted: List[dapr_conversation.ConversationTools] = []
        for tool in tools:
            fn = tool.get("function", {}) if isinstance(tool, dict) else {}
            name = fn.get("name")
            description = fn.get("description")
            parameters = fn.get("parameters")
            function_spec = dapr_conversation.ConversationToolsFunction(
                name=name or "",
                description=description or "",
                parameters=parameters or {},
            )
            conv_tool = dapr_conversation.ConversationTools(function=function_spec)
            converted.append(conv_tool)
        return converted

    def chat_completion_alpha2(
        self,
        *,
        llm: str,
        inputs: List[dapr_conversation.ConversationInputAlpha2],
        scrub_pii: Optional[bool] = None,
        temperature: Optional[float] = None,
        tools: Optional[List[Dict[str, Any]]] = None,
        tool_choice: Optional[str] = None,
        context_id: Optional[str] = None,
        parameters: Optional[Dict[str, Any]] = None,
        response_format: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Invoke Dapr Conversation API Alpha2 with optional tool-calling support and
        convert the response into a simplified OpenAI-like JSON envelope.
        """
        conv_tools = self._convert_openai_tools_to_conversation_tools(tools)

        # TODO: Remove when langchaningo is updated in contrib to latest version with a fix for openai-like temperature
        if temperature is None:
            temperature = 1

        with DaprClient() as client:
            kwargs: Dict[str, Any] = dict(
                name=llm,
                inputs=inputs,
                context_id=context_id,
                parameters=parameters,
                scrub_pii=scrub_pii,
                temperature=temperature,
                tools=conv_tools,
                tool_choice=tool_choice,
            )
            if response_format is not None:
                kwargs["response_format"] = json_format.ParseDict(
                    response_format, GrpcStruct()
                )

            response_alpha2 = client.converse_alpha2(**kwargs)

            outputs: List[Dict[str, Any]] = []
            for output in getattr(response_alpha2, "outputs", []) or []:
                choices_list: List[Dict[str, Any]] = []
                for choice in getattr(output, "choices", []) or []:
                    msg = getattr(choice, "message", None)
                    content = getattr(msg, "content", None) if msg else None

                    # Convert tool calls if present
                    tool_calls_json: Optional[List[Dict[str, Any]]] = None
                    if msg and getattr(msg, "tool_calls", None):
                        tool_calls_json = []
                        for tc in msg.tool_calls:
                            fn = getattr(tc, "function", None)
                            arguments = getattr(fn, "arguments", None) if fn else None
                            if isinstance(arguments, (dict, list)):
                                try:
                                    arguments = json.dumps(arguments)
                                except Exception:
                                    arguments = str(arguments)
                            elif arguments is None:
                                arguments = ""

                            tool_calls_json.append(
                                {
                                    "id": getattr(tc, "id", ""),
                                    "type": "function",
                                    "function": {
                                        "name": getattr(fn, "name", "") if fn else "",
                                        "arguments": arguments,
                                    },
                                }
                            )

                    choices_list.append(
                        {
                            "message": {
                                "role": "assistant",
                                "content": content,
                                **(
                                    {"tool_calls": tool_calls_json}
                                    if tool_calls_json
                                    else {}
                                ),
                            },
                            "finish_reason": getattr(choice, "finish_reason", "stop"),
                        }
                    )
                outputs.append({"choices": choices_list})

            return {
                "context_id": getattr(response_alpha2, "context_id", None),
                "outputs": outputs,
            }


class DaprInferenceClientBase(LLMClientBase):
    """
    Base class for managing Dapr Inference API clients.
    Handles client initialization, configuration, and shared logic.
    """

    @model_validator(mode="before")
    def validate_and_initialize(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        return values

    def model_post_init(self, __context: Any) -> None:
        """
        Initializes private attributes after validation.
        """
        self._provider = "dapr"

        # Set up the private config and client attributes
        self._config = self.get_config()
        self._client = self.get_client()
        return super().model_post_init(__context)

    def get_config(self) -> DaprInferenceClientConfig:
        """
        Returns the appropriate configuration for the Dapr Conversation API.
        """
        return DaprInferenceClientConfig()

    def get_client(self) -> DaprInferenceClient:
        """
        Initializes and returns the Dapr Inference client.
        """
        return DaprInferenceClient()

    @classmethod
    def from_config(
        cls, client_options: DaprInferenceClientConfig, timeout: float = 1500
    ):
        """
        Initializes the DaprInferenceClientBase using DaprInferenceClientConfig.

        Args:
            client_options: The configuration options for the client.
            timeout: Timeout for requests (default is 1500 seconds).

        Returns:
            DaprInferenceClientBase: The initialized client instance.
        """
        return cls()

    @property
    def config(self) -> Dict[str, Any]:
        return self._config

    @property
    def client(self) -> DaprInferenceClient:
        return self._client
