from dapr_agents.types.message import (
    BaseMessage,
    SystemMessage,
    UserMessage,
    AssistantMessage,
    ToolMessage,
)
from dapr_agents.prompt.utils.jinja import (
    render_jinja_template,
    extract_jinja_variables,
)
from dapr_agents.prompt.utils.fstring import (
    render_fstring_template,
    extract_fstring_variables,
)
from typing import Any, Dict, List, Tuple, Union, Optional
import re
import logging

logger = logging.getLogger(__name__)

DEFAULT_FORMATTER_MAPPING = {
    "f-string": render_fstring_template,
    "jinja2": render_jinja_template,
}

DEFAULT_VARIABLE_EXTRACTOR_MAPPING = {
    "f-string": extract_fstring_variables,
    "jinja2": extract_jinja_variables,
}


class ChatPromptHelper:
    """
    Utility class for handling various operations on chat prompt messages, such as
    formatting, normalizing, and extracting variables.

    Attributes:
        _ROLE_MAP (Dict[str, Type[BaseMessage]]): A mapping of role names to message classes.
    """

    _ROLE_MAP = {
        "system": SystemMessage,
        "user": UserMessage,
        "assistant": AssistantMessage,
        "tool": ToolMessage,
    }

    @classmethod
    def normalize_chat_messages(cls, variable_value: Any) -> List[BaseMessage]:
        """
        Normalize the variable value into a list of BaseMessages, handling strings, dictionaries, and lists.

        Args:
            variable_value (Any): The value associated with a placeholder variable to normalize.

        Returns:
            List[BaseMessage]: A list of normalized BaseMessage instances.

        Raises:
            ValueError: If an unsupported type is encountered within the list or variable.
        """
        normalized_messages = []

        def validate_and_create_message(
            role: str, content: str, message_data: dict
        ) -> BaseMessage:
            if role not in cls._ROLE_MAP:
                raise ValueError(
                    f"Unrecognized role '{role}' in message: {message_data}"
                )
            return cls.create_message(role, content, message_data)

        if isinstance(variable_value, str):
            normalized_messages.append(cls.create_message("user", variable_value, {}))
        elif isinstance(variable_value, list):
            for item in variable_value:
                if isinstance(item, str):
                    normalized_messages.append(cls.create_message("user", item, {}))
                elif isinstance(item, BaseMessage):
                    normalized_messages.append(item)
                elif isinstance(item, dict):
                    # For tool messages and assistant messages with tool calls,
                    # preserve all fields by passing the entire dict as message_data
                    role = item.get("role", "user")
                    content = item.get("content", "")
                    if role == "tool" or (
                        role == "assistant" and item.get("tool_calls")
                    ):
                        normalized_messages.append(
                            validate_and_create_message(role, content, item)
                        )
                    else:
                        # For other messages, create a new message with just role and content
                        normalized_messages.append(
                            validate_and_create_message(role, content, {})
                        )
                else:
                    raise ValueError(
                        f"Unsupported type in list for variable: {type(item)}"
                    )
        elif isinstance(variable_value, dict):
            role = variable_value.get("role", "user")
            content = variable_value.get("content", "")
            if role == "tool" or (
                role == "assistant" and variable_value.get("tool_calls")
            ):
                # For tool messages and assistant messages with tool calls,
                # preserve all fields by passing the entire dict as message_data
                normalized_messages.append(
                    validate_and_create_message(role, content, variable_value)
                )
            else:
                # For other messages, create a new message with just role and content
                normalized_messages.append(
                    validate_and_create_message(role, content, {})
                )
        else:
            raise ValueError(f"Unsupported type for variable: {type(variable_value)}")

        return normalized_messages

    @classmethod
    def format_message(
        cls,
        message: Union[Tuple[str, str], Dict[str, Any], BaseMessage],
        template_format: str,
        **kwargs: Any,
    ) -> BaseMessage:
        """
        Format a single message by replacing template variables based on the specified format.

        Args:
            message (Union[Tuple[str, str], Dict[str, Any], BaseMessage]): The message to format.
            template_format (str): The format for rendering ('f-string' or 'jinja2').
            **kwargs: Variables used to populate placeholders within the message.

        Returns:
            BaseMessage: The message with variables replaced as per the template format.
        """
        role, content = cls.extract_role_and_content(message)
        content = cls.format_content(content, template_format=template_format, **kwargs)
        return cls.create_message(role, content, message)

    @staticmethod
    def format_content(content: str, template_format: str, **kwargs: Any) -> str:
        """
        Apply template formatting to the content string using the specified format.

        Args:
            content (str): The content string to format.
            template_format (str): Template format ('f-string' or 'jinja2').
            **kwargs: Variables for populating placeholders within the content.

        Returns:
            str: The formatted content.
        """
        formatter = DEFAULT_FORMATTER_MAPPING.get(template_format)
        if not formatter:
            raise ValueError(f"Unsupported template format: {template_format}")
        return formatter(content, **kwargs)

    @classmethod
    def extract_role_and_content(
        cls, message: Union[Tuple[str, str], Dict[str, Any], BaseMessage]
    ) -> Tuple[str, str]:
        """
        Extract role and content from a message.

        Args:
            message (Union[Tuple[str, str], Dict[str, Any], BaseMessage]): A message object in the form of a tuple, dictionary, or BaseMessage.

        Returns:
            Tuple[str, str]: Extracted role and content.

        Raises:
            ValueError: If the message is not in a supported format.
        """
        if isinstance(message, tuple) and len(message) == 2:
            return message[0], message[1]
        elif isinstance(message, dict):
            return message.get("role"), message.get("content", "")
        elif isinstance(message, BaseMessage):
            return message.role, message.content
        else:
            raise ValueError(
                "Message must be a tuple (role, content), a dict with 'role' and 'content', or a BaseMessage instance."
            )

    @classmethod
    def create_message(
        cls, role: str, content: str, message_data: Dict[str, Any]
    ) -> BaseMessage:
        """
        Create a BaseMessage instance based on role.

        Args:
            role (str): Role of the message (system, user, assistant, tool).
            content (str): Message content.
            message_data (Dict[str, Any]): Additional data (e.g., tool_call_id for tool messages).

        Returns:
            BaseMessage: Formatted message instance.

        Raises:
            ValueError: If the role is not recognized.
        """
        if role not in cls._ROLE_MAP:
            raise ValueError(f"Invalid message role: {role}")

        message_class = cls._ROLE_MAP[role]
        if role == "tool":
            return message_class(
                content=content, tool_call_id=message_data.get("tool_call_id")
            )
        elif role == "assistant" and message_data.get("tool_calls") is not None:
            # Pass tool_calls to AssistantMessage if present
            return message_class(
                content=content, tool_calls=message_data.get("tool_calls")
            )
        return message_class(content=content)

    @classmethod
    def get_message_class(cls, role: str) -> Union[BaseMessage, None]:
        """Get the message class for a given role."""
        role = role.lower()
        return cls._ROLE_MAP.get(role, None)

    @staticmethod
    def parse_role_content(content: str) -> Tuple[List[str], Optional[str]]:
        """
        Parse the formatted content into role-based chunks and any remaining plain text.
        - `role_chunks`: List of chunks containing roles and their messages.
        - `plain_text`: Text that does not match any role pattern.

        Returns:
            Tuple[List[str], Optional[str]]: Role-based chunks and any remaining plain text.
        """
        # Regex to split on role definitions, capturing role-based content.
        role_pattern = (
            r"(?i)^\s*#?\s*("
            + "|".join(ChatPromptHelper._ROLE_MAP.keys())
            + r")\s*:\s*\n"
        )

        # First, check for role-based patterns. If none, return the entire content as plain_text.
        if not re.search(role_pattern, content, flags=re.MULTILINE):
            return [], content.strip()

        # Split the content on the role pattern.
        chunks = re.split(role_pattern, content, flags=re.MULTILINE)

        # Extract plain text that is not part of role-based content, if any.
        plain_text = None
        if (
            chunks
            and chunks[0].strip()
            and chunks[0].lower() not in ChatPromptHelper._ROLE_MAP
        ):
            plain_text = chunks.pop(0).strip()

        # Filter out empty or whitespace-only chunks from role-based content.
        role_chunks = [chunk.strip() for chunk in chunks if chunk.strip()]

        return role_chunks, plain_text

    @staticmethod
    def to_message(role: str, content: str) -> BaseMessage:
        """
        Parse a single chunk of content into a message object.
        """
        role = role.strip().lower()
        content = content.strip()

        logger.debug(f"Parsing role: '{role}', content: '{content[:30]}...'")

        # Map role to a message class
        message_class = ChatPromptHelper.get_message_class(role)
        if not message_class:
            raise ValueError(f"Invalid message role: '{role}'")

        if not content:
            raise ValueError(f"Content missing for role: '{role}'")

        return message_class(content=content)

    @staticmethod
    def parse_as_messages(
        content: str,
    ) -> Tuple[Optional[List[BaseMessage]], Optional[str]]:
        """
        Parse the content into a list of role-based messages and any unstructured plain text.

        Returns:
            Tuple[List[BaseMessage], Optional[str]]: Parsed messages if role-based chunks are found,
            and any remaining plain text if detected.
        """
        role_chunks, plain_text = ChatPromptHelper.parse_role_content(content)

        # If there are no role-based chunks, return the plain_text directly.
        if not role_chunks:
            logger.debug("No role-based content found; returning plain text.")
            return [], plain_text

        # Parse each role-based chunk to extract messages.
        messages = []
        role = None

        for chunk in role_chunks:
            if chunk.lower() in ChatPromptHelper._ROLE_MAP.keys():
                role = chunk  # Assign role if chunk matches a defined role.
            elif (
                role
            ):  # If a role is set, treat this chunk as the content for that role.
                messages.append(ChatPromptHelper.to_message(role, chunk))
                role = None
            else:
                raise ValueError(f"Unexpected content without a role: {chunk}")

        return messages, plain_text
