import json
from typing import Any, ClassVar, Dict, Optional, Type, cast

from vellum.workflows.constants import VellumIntegrationProviderType
from vellum.workflows.references.trigger import TriggerAttributeReference
from vellum.workflows.triggers.base import BaseTriggerMeta
from vellum.workflows.triggers.integration import IntegrationTrigger
from vellum.workflows.types import ComposioIntegrationTriggerExecConfig
from vellum.workflows.utils.uuids import uuid4_from_hash


class VellumIntegrationTriggerMeta(BaseTriggerMeta):
    """
    Custom metaclass for VellumIntegrationTrigger that supports dynamic attribute discovery.

    This metaclass extends BaseTriggerMeta to enable class-level access to attributes
    that aren't statically defined. When accessing an undefined attribute on a
    VellumIntegrationTrigger class, this metaclass will create a TriggerAttributeReference
    dynamically, allowing triggers to work with attributes discovered from integration
    APIs or event payloads.

    Note: This metaclass intentionally deviates from the standard metaclass pattern used
    in BaseNodeMeta and BaseWorkflowMeta, which generate nested classes (Outputs, Inputs)
    in __new__. Since VellumIntegrationTrigger uses a factory pattern with dynamic
    attributes rather than predefined nested classes, this metaclass focuses on
    attribute discovery via __getattribute__ during workflow definition (when the
    developer references trigger attributes in their workflow code) instead of class
    generation in __new__. This architectural difference is necessary to support the
    dynamic nature of integration triggers where attributes are not known until the
    integration is queried or event data is received.
    """

    def __getattribute__(cls, name: str) -> Any:
        """
        Override attribute access to support dynamic attribute discovery.

        For VellumIntegrationTrigger classes generated by the factory, this method
        allows access to any attribute name, creating TriggerAttributeReference objects
        on demand. This enables usage like:

            SlackMessage = VellumIntegrationTrigger.for_trigger(
                integration_name="SLACK",
                slug="slack_new_message",
                trigger_nano_id="abc123"
            )
            text = SlackMessage.message  # Creates reference even though 'message' isn't pre-defined

        Args:
            name: The attribute name being accessed

        Returns:
            The attribute value or a dynamically created TriggerAttributeReference
        """
        # Let BaseTriggerMeta handle internal attributes and known attributes
        try:
            return super().__getattribute__(name)
        except AttributeError:
            # For VellumIntegrationTrigger factory-generated classes, create dynamic references
            # Only enable dynamic attribute creation for factory-generated classes, not the base
            # VellumIntegrationTrigger class itself. We check the internal __name__ attribute
            # (e.g., "VellumIntegrationTrigger_COMPOSIO_SLACK_slack_new_message"), not the user-facing
            # variable name (e.g., "SlackNewMessage").
            try:
                is_factory_class = super().__getattribute__("__name__").startswith(
                    "VellumIntegrationTrigger_"
                ) and not name.startswith("_")
            except AttributeError:
                is_factory_class = False

            if is_factory_class:
                trigger_cls = cast(Type["VellumIntegrationTrigger"], cls)

                # Check cache first
                cache = super().__getattribute__("__trigger_attribute_cache__")
                if name in cache:
                    return cache[name]

                # Generate and store deterministic UUID for this attribute if not already present.
                # This ensures consistent IDs across multiple accesses to the same attribute,
                # which is critical for serialization and state resolution.
                # Use semantic identity (provider|integration|slug) instead of __qualname__ for stability.
                attribute_ids = super().__getattribute__("__trigger_attribute_ids__")
                if name not in attribute_ids:
                    # Generate stable ID from trigger semantics, not class naming
                    provider = super().__getattribute__("provider")
                    integration_name = super().__getattribute__("integration_name")
                    slug = super().__getattribute__("slug")
                    trigger_identity = f"{provider.value}|{integration_name}|{slug}"
                    attribute_ids[name] = uuid4_from_hash(f"{trigger_identity}|{name}")

                # Create a new dynamic reference for this attribute
                types = (object,)
                reference = TriggerAttributeReference(name=name, types=types, instance=None, trigger_class=trigger_cls)
                cache[name] = reference
                return reference

            # Not a factory class or starts with _, re-raise the AttributeError
            raise


class VellumIntegrationTrigger(IntegrationTrigger, metaclass=VellumIntegrationTriggerMeta):
    """
    Factory-based trigger for Vellum-managed integration events.

    VellumIntegrationTrigger provides a pure factory pattern for creating trigger
    classes dynamically based on integration provider, integration name, slug, and
    trigger nano ID. Unlike predefined trigger classes, these triggers are created
    on-demand and support dynamic attribute discovery from the integration API.

    This design ensures parity with VellumIntegrationToolDefinition and allows users to
    work with any integration trigger without requiring SDK updates for new integrations.

    Examples:
        Create triggers dynamically for different integrations:
            >>> SlackNewMessage = VellumIntegrationTrigger.for_trigger(
            ...     integration_name="SLACK",
            ...     slug="slack_new_message",
            ...     trigger_nano_id="abc123def456"
            ... )
            >>>
            >>> GithubPush = VellumIntegrationTrigger.for_trigger(
            ...     integration_name="GITHUB",
            ...     slug="github_push_event",
            ...     trigger_nano_id="xyz789ghi012"
            ... )

        Use in workflow graph:
            >>> class MyWorkflow(BaseWorkflow):
            ...     graph = SlackNewMessage >> ProcessMessageNode

        Reference trigger attributes in nodes:
            >>> class ProcessNode(BaseNode):
            ...     class Outputs(BaseNode.Outputs):
            ...         text = SlackNewMessage.message
            ...         channel = SlackNewMessage.channel

        Instantiate for testing:
            >>> trigger = SlackNewMessage(event_data={
            ...     "message": "Hello world",
            ...     "channel": "C123456"
            ... })
            >>> trigger.message
            'Hello world'

    Note:
        The factory method generates unique classes with proper __name__ and __module__
        for correct attribute ID generation and serialization. Each factory call with
        the same parameters returns the same class instance (cached).
    """

    # Class variables that identify this trigger
    provider: ClassVar[VellumIntegrationProviderType]
    integration_name: ClassVar[str]
    slug: ClassVar[str]
    trigger_nano_id: ClassVar[str]
    attributes: ClassVar[Dict[str, Any]]

    # Cache for generated trigger classes to ensure consistency
    _trigger_class_cache: ClassVar[Dict[tuple, Type["VellumIntegrationTrigger"]]] = {}

    @classmethod
    def _freeze_attributes(cls, attributes: Dict[str, Any]) -> str:
        """
        Convert attributes dict to hashable string for caching.

        Attributes must be JSON-serializable since they're sent to the backend
        via ComposioIntegrationTriggerExecConfig. This method fails fast if
        attributes contain non-JSON-serializable types.

        Args:
            attributes: Dictionary of trigger attributes

        Returns:
            JSON string representation with sorted keys for deterministic hashing

        Raises:
            ValueError: If attributes are not JSON-serializable
        """
        if not attributes:
            return ""

        try:
            # Use json.dumps with sort_keys for deterministic output
            return json.dumps(attributes, sort_keys=True)
        except (TypeError, ValueError) as e:
            raise ValueError(
                f"Trigger attributes must be JSON-serializable (str, int, float, bool, None, list, dict). "
                f"Got non-serializable value: {e}"
            ) from e

    def __init__(self, event_data: dict):
        """
        Initialize trigger with event data from the integration.

        The trigger dynamically populates its attributes based on the event_data
        dictionary keys. Any key in event_data becomes an accessible attribute.

        Args:
            event_data: Raw event data from the integration. Keys become trigger attributes.

        Examples:
            >>> SlackMessage = VellumIntegrationTrigger.for_trigger(
            ...     integration_name="SLACK",
            ...     slug="slack_new_message",
            ...     trigger_nano_id="abc123"
            ... )
            >>> trigger = SlackMessage(event_data={
            ...     "message": "Hello",
            ...     "channel": "C123",
            ...     "user": "U456"
            ... })
            >>> trigger.message
            'Hello'
            >>> trigger.channel
            'C123'
        """
        super().__init__(event_data)

        # Dynamically populate instance attributes from event_data.
        # This allows any key in event_data to become an accessible attribute:
        # event_data={"message": "Hi"} → trigger.message == "Hi"
        for key, value in event_data.items():
            setattr(self, key, value)

    def to_trigger_attribute_values(self) -> Dict["TriggerAttributeReference[Any]", Any]:
        """
        Materialize attribute descriptor/value pairs for this trigger instance.

        For VellumIntegrationTrigger, this includes all dynamic attributes from event_data.
        """
        attribute_values: Dict["TriggerAttributeReference[Any]", Any] = {}

        # Unlike the base class which iterates over type(self) (predefined annotations),
        # we iterate over event_data keys since our attributes are discovered dynamically
        # from the actual event data received during workflow execution.
        # The base class approach: for reference in type(self)
        # Our approach: for attr_name in self._event_data.keys()
        for attr_name in self._event_data.keys():
            # Get the class-level reference for this attribute
            # This will create it via our custom metaclass if it doesn't exist
            reference = getattr(type(self), attr_name)
            if isinstance(reference, TriggerAttributeReference):
                attribute_values[reference] = getattr(self, attr_name)

        return attribute_values

    @classmethod
    def to_exec_config(cls) -> ComposioIntegrationTriggerExecConfig:
        """
        Generate execution configuration for serialization.

        This method creates a ComposioIntegrationTriggerExecConfig from the trigger
        class's configuration, which is used during serialization to the backend.

        Returns:
            ComposioIntegrationTriggerExecConfig with all required fields

        Raises:
            AttributeError: If called on base VellumIntegrationTrigger (not factory class)

        Examples:
            >>> SlackMessage = VellumIntegrationTrigger.for_trigger(
            ...     integration_name="SLACK",
            ...     slug="slack_new_message",
            ...     trigger_nano_id="abc123",
            ...     attributes={"channel": "C123456"}
            ... )
            >>> exec_config = SlackMessage.to_exec_config()
            >>> exec_config.slug
            'slack_new_message'
            >>> exec_config.attributes
            {'channel': 'C123456'}
        """
        if not hasattr(cls, "slug"):
            raise AttributeError(
                "to_exec_config() can only be called on factory-generated trigger classes. "
                "Use VellumIntegrationTrigger.for_trigger() to create a trigger class first."
            )

        return ComposioIntegrationTriggerExecConfig(
            provider=cls.provider,
            integration_name=cls.integration_name,
            slug=cls.slug,
            trigger_nano_id=cls.trigger_nano_id,
            attributes=cls.attributes,
        )

    @classmethod
    def for_trigger(
        cls,
        integration_name: str,
        slug: str,
        trigger_nano_id: str,
        provider: str = "COMPOSIO",
        attributes: Optional[Dict[str, Any]] = None,
    ) -> Type["VellumIntegrationTrigger"]:
        """
        Factory method to create a new trigger class for a specific integration trigger.

        This method generates a unique trigger class that can be used in workflow graphs
        and node definitions. Each unique combination of provider, integration_name,
        slug, and trigger_nano_id produces the same class instance (cached).

        Args:
            integration_name: The integration identifier (e.g., "SLACK", "GITHUB")
            slug: The slug of the integration trigger in Composio (e.g., "slack_new_message")
            trigger_nano_id: Composio's unique trigger identifier used for event matching
            provider: The integration provider (default: "COMPOSIO")
            attributes: Optional dict of trigger-specific configuration attributes
                used for filtering events. For example, {"channel": "C123456"} to only
                match events from a specific Slack channel.

        Returns:
            A new trigger class configured for the specified integration trigger

        Examples:
            >>> SlackNewMessage = VellumIntegrationTrigger.for_trigger(
            ...     integration_name="SLACK",
            ...     slug="slack_new_message",
            ...     trigger_nano_id="abc123def456",
            ...     attributes={"channel": "C123456"}
            ... )
            >>> type(SlackNewMessage).__name__
            'VellumIntegrationTrigger_COMPOSIO_SLACK_slack_new_message'
            >>>
            >>> # Use in workflow
            >>> class MyWorkflow(BaseWorkflow):
            ...     graph = SlackNewMessage >> ProcessNode

        Note:
            The generated class has proper __name__, __module__, and __qualname__
            for correct serialization and attribute ID generation.
        """
        # Validate and normalize provider
        provider_enum = VellumIntegrationProviderType(provider)

        # Normalize attributes
        attrs = attributes or {}

        # Create cache key - include all identifying parameters including attributes
        # Convert attributes dict to a hashable representation for caching
        frozen_attrs = cls._freeze_attributes(attrs)
        cache_key = (
            provider_enum.value,
            integration_name,
            slug,
            trigger_nano_id,
            frozen_attrs,
        )

        # Return cached class if it exists
        if cache_key in cls._trigger_class_cache:
            return cls._trigger_class_cache[cache_key]

        # Generate unique class name including provider to avoid collisions across providers
        class_name = f"VellumIntegrationTrigger_{provider_enum.value}_{integration_name}_{slug}"

        # Create the new trigger class
        trigger_class = type(
            class_name,
            (cls,),
            {
                "provider": provider_enum,
                "integration_name": integration_name,
                "slug": slug,
                "trigger_nano_id": trigger_nano_id,
                "attributes": attrs,
                "__module__": cls.__module__,
                # Explicitly set __qualname__ to match __name__ for deterministic UUID generation.
                # UUIDs are generated from __qualname__, so this must be consistent and unique
                # across different trigger configurations to prevent ID collisions.
                "__qualname__": class_name,
                # Initialize cache attributes that would normally be set by BaseTriggerMeta.__new__
                # Since we're using type() directly, we need to set these ourselves
                "__trigger_attribute_ids__": {},
                "__trigger_attribute_cache__": {},
            },
        )

        # Cache the generated class
        cls._trigger_class_cache[cache_key] = trigger_class

        return trigger_class
