# Copyright (c) Microsoft. All rights reserved.

import json
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel

from semantic_kernel.const import DEFAULT_SERVICE_NAME

if TYPE_CHECKING:
    from collections.abc import Iterable

    from _typeshed import SupportsKeysAndGetItem

    from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings


class KernelArguments(dict):
    """The arguments sent to the KernelFunction."""

    def __init__(
        self,
        settings: (
            "PromptExecutionSettings | list[PromptExecutionSettings] | dict[str, PromptExecutionSettings] | None"
        ) = None,
        **kwargs: Any,
    ):
        """Initializes a new instance of the KernelArguments class.

        This is a dict-like class with the additional field for the execution_settings.

        This class is derived from a dict, hence behaves the same way,
        just adds the execution_settings as a dict, with service_id and the settings.

        Args:
            settings (PromptExecutionSettings | List[PromptExecutionSettings] | None):
                The settings for the execution.
                If a list is given, make sure all items in the list have a unique service_id
                as that is used as the key for the dict.
            **kwargs (dict[str, Any]): The arguments for the function invocation, works similar to a regular dict.
        """
        super().__init__(**kwargs)
        settings_dict = None
        if settings:
            settings_dict = {}
            if isinstance(settings, dict):
                settings_dict = settings
            elif isinstance(settings, list):
                settings_dict = {s.service_id or DEFAULT_SERVICE_NAME: s for s in settings}
            else:
                settings_dict = {settings.service_id or DEFAULT_SERVICE_NAME: settings}
        self.execution_settings: dict[str, "PromptExecutionSettings"] | None = settings_dict

    def __bool__(self) -> bool:
        """Returns True if the arguments have any values."""
        has_arguments = self.__len__() > 0
        has_execution_settings = self.execution_settings is not None and len(self.execution_settings) > 0
        return has_arguments or has_execution_settings

    def __or__(self, value: dict) -> "KernelArguments":
        """Merges a KernelArguments with another KernelArguments or dict.

        This implements the `|` operator for KernelArguments.
        """
        if not isinstance(value, dict):
            raise TypeError(
                f"TypeError: unsupported operand type(s) for |: '{type(self).__name__}' and '{type(value).__name__}'"
            )

        # Merge execution settings
        new_execution_settings = (self.execution_settings or {}).copy()
        if isinstance(value, KernelArguments) and value.execution_settings:
            new_execution_settings |= value.execution_settings
        # Create a new KernelArguments with merged dict values
        return KernelArguments(settings=new_execution_settings, **(dict(self) | dict(value)))

    def __ror__(self, value: dict) -> "KernelArguments":
        """Merges a dict with a KernelArguments.

        This implements the right-side `|` operator for KernelArguments.
        """
        if not isinstance(value, dict):
            raise TypeError(
                f"TypeError: unsupported operand type(s) for |: '{type(value).__name__}' and '{type(self).__name__}'"
            )

        # Merge execution settings
        new_execution_settings = {}
        if isinstance(value, KernelArguments) and value.execution_settings:
            new_execution_settings = value.execution_settings.copy()
        if self.execution_settings:
            new_execution_settings |= self.execution_settings

        # Create a new KernelArguments with merged dict values
        return KernelArguments(settings=new_execution_settings, **(dict(value) | dict(self)))

    def __ior__(self, value: "SupportsKeysAndGetItem[Any, Any] | Iterable[tuple[Any, Any]]") -> "KernelArguments":
        """Merges into this KernelArguments with another KernelArguments or dict (in-place)."""
        self.update(value)

        # In-place merge execution settings
        if isinstance(value, KernelArguments) and value.execution_settings:
            if self.execution_settings:
                self.execution_settings.update(value.execution_settings)
            else:
                self.execution_settings = value.execution_settings.copy()

        return self

    def dumps(self, include_execution_settings: bool = False) -> str:
        """Serializes the KernelArguments to a JSON string."""
        data = dict(self)
        if include_execution_settings and self.execution_settings:
            data["execution_settings"] = self.execution_settings

        def default(obj):
            if isinstance(obj, BaseModel):
                return obj.model_dump()

            return str(obj)

        return json.dumps(data, default=default)
