"""
Copyright 2024 HaiyangLi

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.
"""

"""This module defines abstract base classes for LionAGI."""

from abc import ABC, abstractmethod
from collections.abc import Generator
from typing import Any, Iterator, TypeVar

from pydantic import Field, BaseModel, field_validator

from .component import LionIDable, get_lion_id
from .exceptions import LionTypeError

T = TypeVar("T")


class Record(ABC):
    """
    Abstract base class for managing a collection of unique LionAGI items.

    Accepts LionIDable for retrieval and requires Component instances for
    addition.

    Methods:
        keys: Return an iterator over the ln_id of items in the record.
        values: Return an iterator over items in the record.
        get: Retrieve an item by identifier.
        __getitem__: Return an item using a LionIDable identifier.
        __setitem__: Add or update an item in the record.
        __contains__: Check if an item is in the record.
        __len__: Return the number of items in the record.
        __iter__: Iterate over items in the record.
    """

    @abstractmethod
    def keys(self) -> Generator[str, None, None]:
        """Return an iterator over the ln_id of items in the record."""

    @abstractmethod
    def values(self) -> Generator[T, None, None]:
        """Return an iterator over items in the record."""

    @abstractmethod
    def get(self, item: LionIDable, /, default: Any = None) -> T:
        """
        Retrieve an item by identifier.

        Accepts a LionIDable object. Returns the default if the item is not
        found.

        Args:
            item (LionIDable): The identifier of the item to retrieve.
            default (Any): The default value to return if the item is not found.

        Returns:
            T: The retrieved item or the default value.
        """

    @abstractmethod
    def __getitem__(self, item: LionIDable) -> T:
        """
        Return an item using a LionIDable identifier.

        Raises:
            KeyError: If the item ID is not found.

        Args:
            item (LionIDable): The identifier of the item to retrieve.

        Returns:
            T: The retrieved item.
        """

    @abstractmethod
    def __setitem__(self, item: LionIDable, value: T) -> None:
        """
        Add or update an item in the record.

        The value must be a Component instance.

        Args:
            item (LionIDable): The identifier of the item to add or update.
            value (T): The Component instance to add or update.
        """

    @abstractmethod
    def __contains__(self, item: LionIDable) -> bool:
        """
        Check if an item is in the record, using either an ID or an object.

        Args:
            item (LionIDable): The identifier or object to check.

        Returns:
            bool: True if the item is in the record, False otherwise.
        """

    @abstractmethod
    def __len__(self) -> int:
        """
        Return the number of items in the record.

        Returns:
            int: The number of items in the record.
        """

    @abstractmethod
    def __iter__(self) -> Iterator[T]:
        """
        Iterate over items in the record.

        Yields:
            T: The items in the record.
        """


class Ordering(ABC):
    """Represents sequencing of certain order."""

    @abstractmethod
    def __len__(self) -> int:
        """
        Return the number of item ids in the ordering.

        Or the number of orderings in another ordering.
        """

    @abstractmethod
    def __contains__(self, item: Any) -> bool:
        """Check if an item id is in the ordering."""


class Condition(ABC):
    """Represents a condition in a given context."""

    @abstractmethod
    async def applies(self, value: Any, /, *args: Any, **kwargs: Any) -> Any:
        """Asynchronously determine if the condition applies to the given value.

        Args:
            value (Any): The value to check against the condition.
            *args: Additional positional arguments.
            **kwargs: Additional keyword arguments.

        Returns:
            Any: The result of applying the condition to the value.
        """


class Actionable(ABC):
    """Represents an action that can be invoked with arguments."""

    @abstractmethod
    async def invoke(self, /, *args: Any, **kwargs: Any) -> Any:
        """
        Invoke the action asynchronously with the given arguments.

        Args:
            *args: Positional arguments for invoking the action.
            **kwargs: Keyword arguments for invoking the action.

        Returns:
            Any: The result of invoking the action.
        """


class Progressable(ABC):
    """Represents a process that can progress forward asynchronously."""

    @abstractmethod
    async def forward(self, /, *args: Any, **kwargs: Any) -> None:
        """Move the process forward asynchronously.

        Args:
            *args: Positional arguments for moving the process forward.
            **kwargs: Keyword arguments for moving the process forward.
        """


class Relatable(ABC):
    """Defines a relationship that can be established with arguments."""

    @abstractmethod
    def relate(self, /, *args: Any, **kwargs: Any) -> None:
        """Establish a relationship based on the provided arguments.

        Args:
            *args: Positional arguments for establishing the relationship.
            **kwargs: Keyword arguments for establishing the relationship.
        """


class Sendable(BaseModel, ABC):
    """Represents an object that can be sent with a sender and recipient."""

    sender: str = Field(
        "N/A",
        title="Sender",
        description=("The id of the sender node, or 'system', 'user', or 'assistant'."),
    )

    recipient: str = Field(
        "N/A",
        title="Recipient",
        description=(
            "The id of the recipient node, or 'system', 'user', or 'assistant'."
        ),
    )

    @field_validator("sender", "recipient", mode="before")
    def _validate_sender_recipient(cls, value):
        """Validate the sender and recipient fields.

        Args:
            value (Any): The value to validate.

        Returns:
            str: The validated value.

        Raises:
            LionTypeError: If the value is invalid.
        """
        if value is None:
            return "N/A"

        if value in ["system", "user", "N/A", "assistant"]:
            return value

        a = get_lion_id(value)
        if not isinstance(a, str) or len(a) != 32:
            raise LionTypeError(
                "Invalid sender or recipient value. "
                "Expected a valid node id or one of "
                "'system' or 'user'."
            )
        return a


class Executable(ABC):
    """Represents an object that can be executed with arguments."""

    @abstractmethod
    async def execute(self, /, *args: Any, **kwargs: Any) -> Any:
        """Execute the object with the given arguments asynchronously.

        Args:
            *args: Positional arguments for executing the object.
            **kwargs: Keyword arguments for executing the object.

        Returns:
            Any: The result of executing the object.
        """


class Directive(ABC):
    """Represents a directive that can be directed with arguments."""

    # @abstractmethod
    # async def direct(self, *args, **kwargs):
    #     """Direct the directive with the given arguments asynchronously.

    #     Args:
    #         *args: Positional arguments for directing the directive.
    #         **kwargs: Keyword arguments for directing the directive.
    #     """

    @property
    def class_name(self) -> str:
        """Get the class name of the directive.

        Returns:
            str: The class name of the directive.
        """
        return self._class_name()

    @classmethod
    def _class_name(cls) -> str:
        """Get the class name of the directive.

        Returns:
            str: The class name of the directive.
        """
        return cls.__name__
