"""
This module provides classes and utilities for managing metadata
associated with decorated methods. It is designed to facilitate the
handling of metadata entries, which include instrument types and their
requirement status, as part of a method's runtime behaviour.

**Key Features**

- `AnnouncementMetadataItem`: Represents a single metadata entry,
  encapsulating an instrument class and its requirement status
  (required or optional).
- `AnnouncementMetadata`: Manages metadata for methods, supporting
  operations such as adding metadata entries, iterating over entries,
  and retrieving the number of entries.

**Classes**

- `AnnouncementMetadataItem`: A dataclass to store details about
  individual metadata items.
- `AnnouncementMetadata`: A utility class for managing collections of
  metadata items for specific methods.

**Usage**

Decorate methods with metadata and manage their metadata
programmatically. `AnnouncementMetadata` allows for the addition,
retrieval, and iteration of metadata entries associated with methods.

**Example**

>>> class SomeInstrument:
...     pass
...
>>> # Define a class with a method
>>> class Foo:
...     def bar(self):
...         pass
...
>>> # Create metadata for the method
>>> from domprob.announcements import metadata
>>> meta = metadata.AnnouncementMetadata(Foo.bar)
>>>
>>> meta.add(SomeInstrument, required=True)
AnnouncementMetadata(method=<function Foo.bar at 0x...>)
>>>
>>> list(meta)
[AnnouncementMetadataEntry(instrument_cls=<class '...SomeInstrument'>, required=True)]
"""

from collections.abc import Callable, Generator
from dataclasses import dataclass
from typing import Any


@dataclass
class AnnouncementMetadataEntry:
    """Represents metadata entry for an announcement's method. Includes
    the instrument class and its requirement status.

    Args:
        instrument_cls (type[`BaseInstrument`]): The type of
            instrument for which the announcements should be executed.
        required (`bool`), optional: Whether the instrument instance
            at runtime is required or optional for the announcements.
            Defaults to `True` if not provided during instantiation.

    Examples:
        >>> class SomeInstrument:
        ...     pass
        ...
        >>> # Define a class with a method
        >>> class Foo:
        ...     def bar(self):
        ...         pass
        ...
        >>> # Create metadata for the method
        >>> from domprob.announcements import metadata
        >>> entry = metadata.AnnouncementMetadataEntry(SomeInstrument, required=False)
        >>> entry
        AnnouncementMetadataEntry(instrument_cls=<class '...SomeInstrument'>, required=False)
        >>> entry.instrument_cls
        <class '...SomeInstrument'>
        >>> entry.required
        False
    """

    instrument_cls: type[Any]
    required: bool = True


class AnnouncementMetadata:
    """Stores and manages metadata for an instance method.

    Args:
        method (`Callable[..., Any]`): The method for which the
            metadata is to be managed.

    Examples:
        >>> # Define a class with a method
        >>> class Foo:
        ...     def bar(self):
        ...         pass
        ...
        >>> # Create metadata for the method
        >>> from domprob.announcements import metadata
        >>> meta = metadata.AnnouncementMetadata(Foo.bar)
        >>>
        >>> meta
        AnnouncementMetadata(method=<function Foo.bar at 0x...>)
    """

    # The attribute name where the metadata will be saved to on the
    # method.
    METADATA_ATTR: str = "__announcement_metadata__"

    def __init__(self, method: Callable[..., Any]) -> None:
        self._method = method

    def __len__(self) -> int:
        """Returns the number of metadata entries.

        Returns:
            int: The number of metadata entries recorded for the
                method.

        Examples:
            >>> # Define a class with a method
            >>> class Foo:
            ...     def bar(self):
            ...         pass
            ...
            >>> # Create metadata for the method
            >>> from domprob.announcements import metadata
            >>> meta = metadata.AnnouncementMetadata(Foo.bar)
            >>>
            >>> len(meta)
            0
            >>> # Define an instrument
            >>> class SomeInstrument:
            ...     pass
            ...
            >>> meta.add(SomeInstrument, required=True)
            AnnouncementMetadata(method=<function Foo.bar at 0x...>)
            >>> len(meta)
            1
        """
        return len(getattr(self._method, self.METADATA_ATTR, []))

    def __iter__(self) -> Generator[AnnouncementMetadataEntry, None, None]:
        """Iterates over all metadata entries recorded for the method.

        Yields:
            AnnoMetadataItem: Metadata items associated with the
                method.

        Examples:
            >>> # Define a class with a method
            >>> class Foo:
            ...     def bar(self):
            ...         pass
            ...
            >>> # Create metadata for the method
            >>> from domprob.announcements import metadata
            >>> meta = metadata.AnnouncementMetadata(Foo.bar)
            >>>
            >>> # Define an instrument
            >>> class SomeInstrument:
            ...     pass
            ...
            >>> # Add entries to the metadata
            >>> meta.add(SomeInstrument, True).add(SomeInstrument, False)
            AnnouncementMetadata(method=<function Foo.bar at 0x...>)
            >>>
            >>> meta_iter = iter(meta)
            >>> next(meta_iter)
            AnnouncementMetadataEntry(instrument_cls=<class '...SomeInstrument'>, required=True)
            >>> next(meta_iter)
            AnnouncementMetadataEntry(instrument_cls=<class '...SomeInstrument'>, required=False)
        """
        yield from tuple(getattr(self._method, self.METADATA_ATTR, []))

    def __eq__(self, other: Any) -> bool:
        """Equality operator to check if two `AnnouncementMetadata`
        instances are equivalent.

        Args:
            other (Any): The object to compare with the current
                `AnnouncementMetadata` instance. Typically expected
                to be another `AnnouncementMetadata` object.

        Returns:
            bool: Returns `True` if both operands reference the
                metadata of the same instance method

        Examples:
            >>> # Define a class with a method
            >>> class Foo:
            ...     def bar(self):
            ...         pass
            ...
            >>> # Create metadata for the method
            >>> from domprob.announcements import metadata
            >>> meta_1 = metadata.AnnouncementMetadata(Foo.bar)
            >>> meta_1 == "string"
            False
            >>> meta_2 = metadata.AnnouncementMetadata(Foo.bar)
            >>> meta_1 == meta_2
            True
            >>>
            >>> # Define an instrument
            >>> class SomeInstrument:
            ...     pass
            ...
            >>> meta_1.add(SomeInstrument, True)
            AnnouncementMetadata(method=<function Foo.bar at 0x...>)
            >>> meta_1 == meta_2  # Both reference the same method
            True
        """
        if not isinstance(other, AnnouncementMetadata):
            return False
        return self._method == other._method

    def add(self, instrument: Any, required: bool) -> "AnnouncementMetadata":
        """Adds an announcements metadata entry to the method.

        Args:
            instrument (type[`BaseInstrument`]): The instrument class
                to add to the method's metadata.
            required (`bool`): Whether the instrument is required.

        Returns:
            AnnouncementMetadata: The updated metadata instance.

        Examples:
            >>> # Define a class with a method
            >>> class Foo:
            ...     def bar(self):
            ...         pass
            ...
            >>> # Create metadata for the method
            >>> from domprob.announcements import metadata
            >>> meta = metadata.AnnouncementMetadata(Foo.bar)
            >>>
            >>> len(meta)
            0
            >>> # Define an instrument
            >>> class SomeInstrument:
            ...     pass
            ...
            >>> meta.add(SomeInstrument, required=True)
            AnnouncementMetadata(method=<function Foo.bar at 0x...>)
            >>> len(meta)
            1
        """
        item = AnnouncementMetadataEntry(instrument, required=required)
        meth_metadata = list(self)
        meth_metadata.append(item)
        setattr(self._method, self.METADATA_ATTR, meth_metadata)
        return self

    def __repr__(self) -> str:
        """Returns a string representation of the metadata instance.

        Returns:
            str: The string representation.

        Examples:
            >>> # Define a class with a method to decorate
            >>> class Foo:
            ...     def bar(self):
            ...         pass
            ...
            >>> # Create metadata for the method
            >>> from domprob.announcements import metadata
            >>> meta = metadata.AnnouncementMetadata(Foo.bar)
            >>> repr(meta)
            'AnnouncementMetadata(method=<function Foo.bar at 0x...>)'
        """
        return f"{self.__class__.__name__}(method={self._method!r})"
