import inspect
from collections.abc import AsyncGenerator, AsyncIterator
from dataclasses import fields, is_dataclass
from typing import Any, Callable, Dict, get_args, get_origin, get_type_hints

from ...api.noun_phrase import NounPhrase
from ...reflection import ConceptDescriptor
from ...reflection.factory.types import ConceptTypeFactory


class PollRule:
    """Base class for poll validation rules."""

    def validate(self, _book_class: type, poll_func: Callable[..., Any]) -> None:
        """
        Validate the poll function and its associated stop function.

        Args:
            _book_class: The book class containing the poll
            poll_func: The function decorated with @poll

        Raises:
            ValueError: If validation fails
            TypeError: If the wrong type is provided
        """


class PollConfigurationDescriptionRule(PollRule):
    """Rule to validate that all poll configuration parameters have descriptions."""

    def validate(self, _book_class: type, poll_func: Callable[..., Any]) -> None:  # type: ignore[override]
        poll = getattr(poll_func, "__poll__", None)
        if not poll:
            raise ValueError(f"Function '{poll_func.__name__}' is missing __poll__ attribute. " f"Did you forget to apply the @poll decorator?")

        # Check that all configuration parameters have descriptions
        missing_descriptions = []
        for param_name, concept in poll.configuration.items():
            if not concept.description:
                missing_descriptions.append(param_name)

        if missing_descriptions:
            raise ValueError(
                f"Poll '{poll.name}' configuration parameters missing descriptions: " f"{', '.join(missing_descriptions)}. " f"Add descriptions in the poll function docstring."
            )


class PollReturnTypeRule(PollRule):
    """Rule to validate that the poll function returns AsyncIterator[T] or AsyncGenerator[T, None]."""

    def validate(self, _book_class: type, poll_func: Callable[..., Any]) -> None:  # type: ignore[override]
        sig = inspect.signature(poll_func, eval_str=True)
        return_annotation = sig.return_annotation

        if return_annotation == inspect.Signature.empty:
            raise TypeError(f"Poll function '{poll_func.__name__}' must have a return type annotation. " f"Expected: AsyncIterator[YourType] or AsyncGenerator[YourType, None]")

        origin = get_origin(return_annotation)
        if origin not in (AsyncIterator, AsyncGenerator):
            raise TypeError(f"Poll function '{poll_func.__name__}' must return " f"AsyncIterator[T] or AsyncGenerator[T, None], got {return_annotation}")

        args = get_args(return_annotation)
        if not args:
            raise TypeError(f"Poll function '{poll_func.__name__}' must specify the yielded type: " f"AsyncIterator[YourType]")


class PollEventExtractionRule(PollRule):
    """Extracts event structure from poll's yielded type."""

    def validate(self, _book_class: type, poll_func: Callable[..., Any]) -> None:  # type: ignore[override]
        # This rule doesn't raise errors, it just ensures the poll is callable
        if not callable(poll_func):
            raise TypeError(f"Poll function '{poll_func.__name__}' must be callable.")

    @staticmethod
    def extract_event(_book_class: type, poll_func: Callable[..., Any]) -> Dict[str, ConceptDescriptor]:
        """
        Extract event fields from the poll's yielded type if it's a @poll_event dataclass.

        Args:
            book_class: The book class containing the poll
            poll_func: The poll function to extract event from

        Returns:
            Dictionary mapping event field names to ConceptDescriptors
        """
        sig = inspect.signature(poll_func, eval_str=True)
        return_annotation = sig.return_annotation

        event = {}

        # Extract the yielded type from AsyncIterator[T] or AsyncGenerator[T, None]
        if return_annotation and return_annotation != inspect.Signature.empty:
            origin = get_origin(return_annotation)

            # Check if it's AsyncIterator or AsyncGenerator
            if origin in (AsyncIterator, AsyncGenerator):
                args = get_args(return_annotation)

                if args:
                    yielded_type = args[0]

                    # Check if the yielded type is a dataclass with @poll_event
                    if is_dataclass(yielded_type) and getattr(yielded_type, "__poll_event__", False):
                        # Get resolved type hints for the dataclass to handle forward references
                        type_hints = get_type_hints(yielded_type)

                        # Extract fields from the dataclass
                        for field in fields(yielded_type):
                            # Use resolved type hint if available, otherwise use field.type
                            field_type = type_hints.get(field.name, field.type)
                            concept_type = ConceptTypeFactory.from_type(field_type)

                            event[field.name] = ConceptDescriptor(
                                noun_phrases=[NounPhrase(head=field.name, modifiers=[])],
                                type=concept_type,
                                description=None,
                                default_value=field.default if field.default is not field.default_factory else None,
                            )

        # Return event dict (empty if not a @poll_event dataclass)
        return event


def check_and_resolve_poll(book_class: type, poll_func: Callable[..., Any], rules: list[PollRule]) -> Dict[str, ConceptDescriptor]:
    """
    Apply all poll validation rules and return extracted event.

    Args:
        book_class: The book class containing the poll
        poll_func: The poll function to validate and process
        rules: List of rules to apply

    Returns:
        Dictionary mapping event field names to ConceptDescriptors
    """
    # Apply all validation rules
    for rule in rules:
        rule.validate(book_class, poll_func)

    # Extract event information using the PollEventExtractionRule
    for rule in rules:
        if isinstance(rule, PollEventExtractionRule):
            return rule.extract_event(book_class, poll_func)

    # If no extraction rule found, return empty dict
    return {}
