# Copyright (c) Microsoft. All rights reserved.

import sys
from collections import deque
from collections.abc import AsyncIterable
from copy import deepcopy

if sys.version_info >= (3, 12):
    from typing import override  # pragma: no cover
else:
    from typing_extensions import override  # pragma: no cover

from typing import TYPE_CHECKING, Any, ClassVar, Deque

from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.contents import ChatMessageContent
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.utils.feature_stage_decorator import experimental

if TYPE_CHECKING:
    from semantic_kernel.agents.agent import Agent
    from semantic_kernel.agents.chat_completion.chat_completion_agent import ChatHistoryAgentThread
    from semantic_kernel.contents.chat_history import ChatHistory
    from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent


@experimental
class ChatHistoryChannel(AgentChannel, ChatHistory):
    """An AgentChannel specialization for that acts upon a ChatHistoryHandler."""

    thread: "ChatHistoryAgentThread"

    ALLOWED_CONTENT_TYPES: ClassVar[tuple[type, ...]] = (
        ImageContent,
        FunctionCallContent,
        FunctionResultContent,
        StreamingTextContent,
        TextContent,
    )

    @override
    async def invoke(
        self,
        agent: "Agent",
        **kwargs: Any,
    ) -> AsyncIterable[tuple[bool, ChatMessageContent]]:
        """Perform a discrete incremental interaction between a single Agent and AgentChat.

        Args:
            agent: The agent to interact with.
            kwargs: The keyword arguments.

        Returns:
            An async iterable of ChatMessageContent.
        """
        message_count = len(self.messages)
        mutated_history = set()
        message_queue: Deque[ChatMessageContent] = deque()

        async for response in agent.invoke(
            messages=self.messages[-1],
            thread=self.thread,
        ):
            # Capture all messages that have been included in the mutated history.
            for message_index in range(message_count, len(self.messages)):
                mutated_message = self.messages[message_index]
                mutated_history.add(mutated_message)
                message_queue.append(mutated_message)

            # Update the message count pointer to reflect the current history.
            message_count = len(self.messages)

            # Avoid duplicating any message included in the mutated history and also returned by the enumeration result.
            if response.message not in mutated_history:
                self.messages.append(response.message)
                message_queue.append(response.message)

            # Dequeue the next message to yield.
            yield_message = message_queue.popleft()
            yield (
                self._is_message_visible(message=yield_message, message_queue_count=len(message_queue)),
                yield_message,
            )

        # Dequeue any remaining messages to yield.
        while message_queue:
            yield_message = message_queue.popleft()
            yield (
                self._is_message_visible(message=yield_message, message_queue_count=len(message_queue)),
                yield_message,
            )

    @override
    async def invoke_stream(
        self, agent: "Agent", messages: list[ChatMessageContent], **kwargs: Any
    ) -> AsyncIterable["StreamingChatMessageContent"]:
        """Perform a discrete incremental stream interaction between a single Agent and AgentChat.

        Args:
            agent: The agent to interact with.
            messages: The history of messages in the conversation.
            kwargs: The keyword arguments

        Returns:
            An async iterable of ChatMessageContent.
        """
        message_count = len(self.messages)

        async for response_message in agent.invoke_stream(
            messages=self.messages[-1],
            thread=self.thread,
        ):
            if response_message.message.content:
                yield response_message.message

        for message_index in range(message_count, len(self.messages)):
            messages.append(self.messages[message_index])

    def _is_message_visible(self, message: ChatMessageContent, message_queue_count: int) -> bool:
        """Determine if a message is visible to the user."""
        return (
            not any(isinstance(item, (FunctionCallContent, FunctionResultContent)) for item in message.items)
            or message_queue_count == 0
        )

    @override
    async def receive(
        self,
        history: list[ChatMessageContent],
    ) -> None:
        """Receive the conversation messages.

        Do not include messages that only contain file references.

        Args:
            history: The history of messages in the conversation.
        """
        filtered_history: list[ChatMessageContent] = []
        for message in history:
            new_message = deepcopy(message)
            if new_message.items is None:
                new_message.items = []
            allowed_items = [item for item in new_message.items if isinstance(item, self.ALLOWED_CONTENT_TYPES)]
            if not allowed_items:
                continue
            new_message.items.clear()
            new_message.items.extend(allowed_items)
            filtered_history.append(new_message)
        self.messages.extend(filtered_history)

    @override
    async def get_history(  # type: ignore
        self,
    ) -> AsyncIterable[ChatMessageContent]:
        """Retrieve the message history specific to this channel.

        Returns:
            An async iterable of ChatMessageContent.
        """
        for message in reversed(self.messages):
            yield message

    @override
    async def reset(self) -> None:
        """Reset the channel state."""
        self.messages.clear()
