"""
HookDispatcher: Single execution path for all hooks.

Provides deterministic, priority-ordered hook execution,
similar to Salesforce's hook framework.
"""

import logging

logger = logging.getLogger(__name__)


class HookDispatcher:
    """
    Single execution path for all hooks.

    Responsibilities:
    - Execute hooks in priority order
    - Filter records based on conditions
    - Provide ChangeSet context to hooks
    - Fail-fast error propagation
    - Manage complete operation lifecycle (VALIDATE, BEFORE, AFTER)
    """

    def __init__(self, registry):
        """
        Initialize the dispatcher.

        Args:
            registry: The hook registry (provides get_hooks method)
        """
        self.registry = registry

    def execute_operation_with_hooks(
        self,
        changeset,
        operation,
        event_prefix,
        bypass_hooks=False,
        bypass_validation=False,
    ):
        """
        Execute operation with full hook lifecycle.

        This is the high-level method that coordinates the complete lifecycle:
        1. VALIDATE_{event}
        2. BEFORE_{event}
        3. Actual operation
        4. AFTER_{event}

        Args:
            changeset: ChangeSet for the operation
            operation: Callable that performs the actual DB operation
            event_prefix: 'create', 'update', or 'delete'
            bypass_hooks: Skip all hooks if True
            bypass_validation: Skip validation hooks if True

        Returns:
            Result of operation
        """
        if bypass_hooks:
            return operation()

        # VALIDATE phase
        if not bypass_validation:
            self.dispatch(changeset, f"validate_{event_prefix}", bypass_hooks=False)

        # BEFORE phase
        self.dispatch(changeset, f"before_{event_prefix}", bypass_hooks=False)

        # Execute the actual operation
        result = operation()

        # AFTER phase - use result if operation returns modified data
        if result and isinstance(result, list) and event_prefix == "create":
            # For create, rebuild changeset with assigned PKs
            from django_bulk_hooks.helpers import build_changeset_for_create

            changeset = build_changeset_for_create(changeset.model_cls, result)

        self.dispatch(changeset, f"after_{event_prefix}", bypass_hooks=False)

        return result

    def dispatch(self, changeset, event, bypass_hooks=False):
        """
        Dispatch hooks for a changeset with deterministic ordering.

        This is the single execution path for ALL hooks in the system.

        Args:
            changeset: ChangeSet instance with record changes
            event: Event name (e.g., 'after_update', 'before_create')
            bypass_hooks: If True, skip all hook execution

        Raises:
            Exception: Any exception raised by a hook (fails fast)
            RecursionError: If hooks create an infinite loop (Python's built-in limit)
        """
        if bypass_hooks:
            return

        # Get hooks sorted by priority (deterministic order)
        hooks = self.registry.get_hooks(changeset.model_cls, event)

        if not hooks:
            return

        # Execute hooks in priority order
        logger.info(f"🔥 HOOKS: Executing {len(hooks)} hooks for {changeset.model_cls.__name__}.{event}")
        for handler_cls, method_name, condition, priority in hooks:
            logger.info(f"  → {handler_cls.__name__}.{method_name} (priority={priority})")
            self._execute_hook(handler_cls, method_name, condition, changeset)

    def _execute_hook(self, handler_cls, method_name, condition, changeset):
        """
        Execute a single hook with condition checking.

        Args:
            handler_cls: The hook handler class
            method_name: Name of the method to call
            condition: Optional condition to filter records
            changeset: ChangeSet with all record changes
        """
        # NEW: Preload relationships needed for condition evaluation
        if condition:
            condition_relationships = self._extract_condition_relationships(condition, changeset.model_cls)
            if condition_relationships:
                self._preload_condition_relationships(changeset, condition_relationships)

        # Filter records based on condition (now safe - relationships are preloaded)
        if condition:
            filtered_changes = [change for change in changeset.changes if condition.check(change.new_record, change.old_record)]

            if not filtered_changes:
                # No records match condition, skip this hook
                return

            # Create filtered changeset
            from django_bulk_hooks.changeset import ChangeSet

            filtered_changeset = ChangeSet(
                changeset.model_cls,
                filtered_changes,
                changeset.operation_type,
                changeset.operation_meta,
            )
        else:
            # No condition, use full changeset
            filtered_changeset = changeset

        # Use DI factory to create handler instance
        from django_bulk_hooks.factory import create_hook_instance

        handler = create_hook_instance(handler_cls)
        method = getattr(handler, method_name)

        # Check if method has @select_related decorator
        preload_func = getattr(method, "_select_related_preload", None)
        if preload_func:
            # Preload relationships to prevent N+1 queries
            try:
                model_cls_override = getattr(handler, "model_cls", None)

                # Get FK fields being updated to avoid preloading conflicting relationships
                skip_fields = changeset.operation_meta.get("fk_fields_being_updated", set())

                # Preload for new_records
                if filtered_changeset.new_records:
                    preload_func(
                        filtered_changeset.new_records,
                        model_cls=model_cls_override,
                        skip_fields=skip_fields,
                    )

                # Also preload for old_records (for conditions that check previous values)
                if filtered_changeset.old_records:
                    preload_func(
                        filtered_changeset.old_records,
                        model_cls=model_cls_override,
                        skip_fields=skip_fields,
                    )
            except Exception:
                pass  # Preload failed, continue without it

        # Execute hook with ChangeSet
        #
        # ARCHITECTURE NOTE: Hook Contract
        # ====================================
        # All hooks must accept **kwargs for forward compatibility.
        # We pass: changeset, new_records, old_records
        #
        # Old hooks that don't use changeset: def hook(self, new_records, old_records, **kwargs)
        # New hooks that do use changeset:    def hook(self, changeset, new_records, old_records, **kwargs)
        #
        # This is standard Python framework design (see Django signals, Flask hooks, etc.)
        logger.info(f"    🚀 Executing: {handler_cls.__name__}.{method_name}")
        try:
            method(
                changeset=filtered_changeset,
                new_records=filtered_changeset.new_records,
                old_records=filtered_changeset.old_records,
            )
            logger.info(f"    ✅ Completed: {handler_cls.__name__}.{method_name}")
        except Exception as e:
            # Fail-fast: re-raise to rollback transaction
            logger.error(
                f"Hook {handler_cls.__name__}.{method_name} failed: {e}",
                exc_info=True,
            )
            raise

    def _extract_condition_relationships(self, condition, model_cls):
        """
        Extract relationship paths that a condition might access.

        Args:
            condition: HookCondition instance
            model_cls: The model class

        Returns:
            set: Set of relationship field names to preload
        """
        relationships = set()

        # Guard against Mock objects and non-condition objects
        if not hasattr(condition, 'check') or hasattr(condition, '_mock_name'):
            return relationships

        # Handle different condition types
        if hasattr(condition, 'field'):
            # Extract relationships from field path (e.g., "status__value" -> "status")
            field_path = condition.field
            if isinstance(field_path, str):
                if '__' in field_path:
                    # Take the first part before __ (the relationship to preload)
                    rel_field = field_path.split('__')[0]

                    # Normalize FK field names: business_id -> business
                    if rel_field.endswith('_id'):
                        potential_field_name = rel_field[:-3]  # Remove '_id'
                        if self._is_relationship_field(model_cls, potential_field_name):
                            rel_field = potential_field_name

                    relationships.add(rel_field)
                else:
                    # Handle single field (no __ notation)
                    rel_field = field_path

                    # Normalize FK field names: business_id -> business
                    if rel_field.endswith('_id'):
                        potential_field_name = rel_field[:-3]  # Remove '_id'
                        if self._is_relationship_field(model_cls, potential_field_name):
                            rel_field = potential_field_name

                    # Only add if it's actually a relationship field
                    if self._is_relationship_field(model_cls, rel_field):
                        relationships.add(rel_field)

        # Handle composite conditions (AndCondition, OrCondition)
        if hasattr(condition, 'cond1') and hasattr(condition, 'cond2'):
            relationships.update(self._extract_condition_relationships(condition.cond1, model_cls))
            relationships.update(self._extract_condition_relationships(condition.cond2, model_cls))

        # Handle NotCondition
        if hasattr(condition, 'cond'):
            relationships.update(self._extract_condition_relationships(condition.cond, model_cls))

        return relationships

    def _is_relationship_field(self, model_cls, field_name):
        """Check if a field is a relationship field."""
        try:
            field = model_cls._meta.get_field(field_name)
            return field.is_relation and not field.many_to_many
        except:
            return False

    def _preload_condition_relationships(self, changeset, relationships):
        """
        Preload relationships needed for condition evaluation.

        Args:
            changeset: ChangeSet with records
            relationships: Set of relationship field names to preload
        """
        if not relationships or not changeset.new_records:
            return

        # Use Django's select_related to preload relationships
        relationship_list = list(relationships)

        # Preload for new_records
        if changeset.new_records:
            # Use select_related on the queryset
            ids = [obj.pk for obj in changeset.new_records if obj.pk is not None]
            if ids:
                preloaded = changeset.model_cls.objects.filter(pk__in=ids).select_related(*relationship_list).in_bulk()
                # Update the objects in changeset with preloaded relationships
                for obj in changeset.new_records:
                    if obj.pk and obj.pk in preloaded:
                        preloaded_obj = preloaded[obj.pk]
                        for rel in relationship_list:
                            if hasattr(preloaded_obj, rel):
                                setattr(obj, rel, getattr(preloaded_obj, rel))

        # Also handle unsaved objects by preloading their FK targets
        for obj in changeset.new_records:
            if obj.pk is None:  # Unsaved object
                for rel in relationship_list:
                    if hasattr(obj, f'{rel}_id'):
                        rel_id = getattr(obj, f'{rel}_id')
                        if rel_id:
                            # Load the related object
                            rel_model = getattr(changeset.model_cls._meta.get_field(rel).remote_field, 'model')
                            rel_obj = rel_model.objects.get(pk=rel_id)
                            setattr(obj, rel, rel_obj)


# Global dispatcher instance
_dispatcher: HookDispatcher | None = None


def get_dispatcher():
    """
    Get the global dispatcher instance.

    Creates the dispatcher on first access (singleton pattern).

    Returns:
        HookDispatcher instance
    """
    global _dispatcher
    if _dispatcher is None:
        # Import here to avoid circular dependency
        from django_bulk_hooks.registry import get_registry

        # Create dispatcher with the registry instance
        _dispatcher = HookDispatcher(get_registry())
    return _dispatcher


def reset_dispatcher():
    """
    Reset the global dispatcher instance.

    Useful for testing to ensure clean state between tests.
    """
    global _dispatcher
    _dispatcher = None
