"""
Callback system infrastructure for FSM state transitions.

This module provides the core classes and registry for managing callbacks
that execute during state machine transitions. Callbacks enable side effects
like notifications, cache invalidation, and related model updates.

Key Components:
    - CallbackStage: Enum defining when callbacks execute (pre/post/error)
    - TransitionContext: Data class with all transition information
    - BaseTransitionCallback: Abstract base class for all callbacks
    - TransitionCallbackRegistry: Singleton registry for callback management

Callback Lifecycle:
    1. PRE callbacks execute before the state change (can abort transition)
    2. State transition occurs
    3. POST callbacks execute after successful transition
    4. ERROR callbacks execute if transition fails

Example Usage:
    Define a custom callback::

        from apps.core.state_machine.callback_base import (
            PostTransitionCallback,
            TransitionContext,
            register_post_callback
        )

        class AuditLogCallback(PostTransitionCallback):
            name = "AuditLogCallback"

            def execute(self, context: TransitionContext) -> bool:
                log_entry = AuditLog.objects.create(
                    model_name=context.model_name,
                    object_id=context.instance.pk,
                    from_state=context.source_state,
                    to_state=context.target_state,
                    user=context.user,
                    timestamp=context.timestamp,
                )
                return True

        # Register the callback
        register_post_callback(
            model_class=EditSubmission,
            field_name='status',
            source='*',  # Any source state
            target='APPROVED',  # Only for approvals
            callback=AuditLogCallback()
        )

    Conditional callback execution::

        class HighPriorityNotification(PostTransitionCallback):
            def should_execute(self, context: TransitionContext) -> bool:
                # Only execute for high priority items
                return getattr(context.instance, 'priority', None) == 'HIGH'

            def execute(self, context: TransitionContext) -> bool:
                # Send high priority notification
                ...

See Also:
    - apps.core.state_machine.callbacks.notifications: Notification callbacks
    - apps.core.state_machine.callbacks.cache: Cache invalidation callbacks
    - apps.core.state_machine.callbacks.related_updates: Related model callbacks
"""

import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Optional

from django.db import models

logger = logging.getLogger(__name__)


class CallbackStage(Enum):
    """
    Stages at which callbacks can be executed during a transition.

    Attributes:
        PRE: Execute before the state change. Can prevent transition by returning False.
        POST: Execute after successful state change. Cannot prevent transition.
        ERROR: Execute when transition fails. Used for cleanup and error logging.

    Example:
        Register callbacks at different stages::

            from apps.core.state_machine.callback_base import (
                CallbackStage,
                callback_registry
            )

            # PRE callback - validate before transition
            callback_registry.register(
                model_class=Ride,
                field_name='status',
                source='OPERATING',
                target='SBNO',
                callback=ValidationCallback(),
                stage=CallbackStage.PRE
            )

            # POST callback - notify after transition
            callback_registry.register(
                model_class=Ride,
                field_name='status',
                source='*',
                target='CLOSED_PERM',
                callback=NotifyCallback(),
                stage=CallbackStage.POST
            )
    """

    PRE = "pre"
    POST = "post"
    ERROR = "error"


@dataclass
class TransitionContext:
    """
    Context object passed to callbacks containing transition metadata.

    Provides all relevant information about the transition being executed,
    including the model instance, state values, user, and timing.

    Attributes:
        instance: The model instance undergoing the transition
        field_name: Name of the FSM field (e.g., 'status')
        source_state: The state before transition (e.g., 'PENDING')
        target_state: The state after transition (e.g., 'APPROVED')
        user: The user performing the transition (may be None)
        timestamp: When the transition occurred
        extra_data: Additional data passed to the transition

    Properties:
        model_class: The model class of the instance
        model_name: String name of the model class

    Example:
        Access context in a callback::

            def execute(self, context: TransitionContext) -> bool:
                # Access transition details
                print(f"Transitioning {context.model_name} #{context.instance.pk}")
                print(f"From: {context.source_state} -> To: {context.target_state}")
                print(f"By user: {context.user}")

                # Access the instance
                if hasattr(context.instance, 'notes'):
                    context.instance.notes = "Processed by callback"

                # Use extra data
                if context.extra_data.get('urgent'):
                    self.send_urgent_notification(context)

                return True
    """

    instance: models.Model
    field_name: str
    source_state: str
    target_state: str
    user: Any | None = None
    timestamp: datetime = field(default_factory=datetime.now)
    extra_data: dict[str, Any] = field(default_factory=dict)

    @property
    def model_class(self) -> type[models.Model]:
        """Get the model class of the instance."""
        return type(self.instance)

    @property
    def model_name(self) -> str:
        """Get the model class name."""
        return self.model_class.__name__

    def __str__(self) -> str:
        return f"TransitionContext({self.model_name}.{self.field_name}: " f"{self.source_state} → {self.target_state})"


class BaseTransitionCallback(ABC):
    """
    Abstract base class for all transition callbacks.

    Subclasses must implement the execute method to define callback behavior.
    """

    # Priority determines execution order (lower = earlier)
    priority: int = 100

    # Whether to continue execution if this callback fails
    continue_on_error: bool = True

    # Human-readable name for logging/debugging
    name: str = "BaseCallback"

    def __init__(
        self,
        priority: int | None = None,
        continue_on_error: bool | None = None,
        name: str | None = None,
    ):
        if priority is not None:
            self.priority = priority
        if continue_on_error is not None:
            self.continue_on_error = continue_on_error
        if name is not None:
            self.name = name

    @abstractmethod
    def execute(self, context: TransitionContext) -> bool:
        """
        Execute the callback.

        Args:
            context: TransitionContext containing all transition information.

        Returns:
            True if successful, False otherwise.
        """
        pass

    def should_execute(self, context: TransitionContext) -> bool:
        """
        Determine if this callback should execute for the given context.

        Override this method to add conditional execution logic.

        Args:
            context: TransitionContext containing all transition information.

        Returns:
            True if the callback should execute, False to skip.
        """
        return True

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(name={self.name}, priority={self.priority})"


class PreTransitionCallback(BaseTransitionCallback):
    """
    Callback executed before the state transition occurs.

    Can be used to validate preconditions or prepare resources.
    If execute() returns False, the transition will be aborted.
    """

    name: str = "PreTransitionCallback"

    # By default, pre-transition callbacks abort on error
    continue_on_error: bool = False


class PostTransitionCallback(BaseTransitionCallback):
    """
    Callback executed after a successful state transition.

    Used for side effects like notifications, cache invalidation,
    and updating related models.
    """

    name: str = "PostTransitionCallback"

    # By default, post-transition callbacks continue on error
    continue_on_error: bool = True


class ErrorTransitionCallback(BaseTransitionCallback):
    """
    Callback executed when a transition fails.

    Used for cleanup, logging, or error notifications.
    """

    name: str = "ErrorTransitionCallback"

    # Error callbacks should always continue
    continue_on_error: bool = True

    def execute(self, context: TransitionContext, exception: Exception | None = None) -> bool:
        """
        Execute the error callback.

        Args:
            context: TransitionContext containing all transition information.
            exception: The exception that caused the transition to fail.

        Returns:
            True if successful, False otherwise.
        """
        pass


@dataclass
class CallbackRegistration:
    """Represents a registered callback with its configuration."""

    callback: BaseTransitionCallback
    model_class: type[models.Model]
    field_name: str
    source: str  # Can be '*' for wildcard
    target: str  # Can be '*' for wildcard
    stage: CallbackStage

    def matches(
        self,
        model_class: type[models.Model],
        field_name: str,
        source: str,
        target: str,
    ) -> bool:
        """Check if this registration matches the given transition."""
        if self.model_class != model_class:
            return False
        if self.field_name != field_name:
            return False
        if self.source != "*" and self.source != source:
            return False
        return not (self.target != "*" and self.target != target)


class TransitionCallbackRegistry:
    """
    Singleton registry for managing transition callbacks.

    Provides methods to register callbacks and retrieve/execute them
    for specific transitions.
    """

    _instance: Optional["TransitionCallbackRegistry"] = None
    _initialized: bool = False

    def __new__(cls) -> "TransitionCallbackRegistry":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if self._initialized:
            return
        self._callbacks: dict[CallbackStage, list[CallbackRegistration]] = {
            CallbackStage.PRE: [],
            CallbackStage.POST: [],
            CallbackStage.ERROR: [],
        }
        self._initialized = True

    def register(
        self,
        model_class: type[models.Model],
        field_name: str,
        source: str,
        target: str,
        callback: BaseTransitionCallback,
        stage: CallbackStage | str = CallbackStage.POST,
    ) -> None:
        """
        Register a callback for a specific transition.

        Args:
            model_class: The model class the callback applies to.
            field_name: The FSM field name.
            source: Source state (use '*' for any source).
            target: Target state (use '*' for any target).
            callback: The callback instance to register.
            stage: When to execute the callback (pre/post/error).
        """
        if isinstance(stage, str):
            stage = CallbackStage(stage)

        registration = CallbackRegistration(
            callback=callback,
            model_class=model_class,
            field_name=field_name,
            source=source,
            target=target,
            stage=stage,
        )

        self._callbacks[stage].append(registration)

        # Keep callbacks sorted by priority
        self._callbacks[stage].sort(key=lambda r: r.callback.priority)

        logger.debug(
            f"Registered {stage.value} callback: {callback.name} for "
            f"{model_class.__name__}.{field_name} ({source} → {target})"
        )

    def register_bulk(
        self,
        model_class: type[models.Model],
        field_name: str,
        callbacks_config: dict[tuple[str, str], list[BaseTransitionCallback]],
        stage: CallbackStage | str = CallbackStage.POST,
    ) -> None:
        """
        Register multiple callbacks for multiple transitions.

        Args:
            model_class: The model class the callbacks apply to.
            field_name: The FSM field name.
            callbacks_config: Dict mapping (source, target) tuples to callback lists.
            stage: When to execute the callbacks.
        """
        for (source, target), callbacks in callbacks_config.items():
            for callback in callbacks:
                self.register(model_class, field_name, source, target, callback, stage)

    def get_callbacks(
        self,
        model_class: type[models.Model],
        field_name: str,
        source: str,
        target: str,
        stage: CallbackStage | str = CallbackStage.POST,
    ) -> list[BaseTransitionCallback]:
        """
        Get all callbacks matching the given transition.

        Args:
            model_class: The model class.
            field_name: The FSM field name.
            source: Source state.
            target: Target state.
            stage: The callback stage to retrieve.

        Returns:
            List of matching callbacks, sorted by priority.
        """
        if isinstance(stage, str):
            stage = CallbackStage(stage)

        matching = []
        for registration in self._callbacks[stage]:
            if registration.matches(model_class, field_name, source, target):
                matching.append(registration.callback)

        return matching

    def execute_callbacks(
        self,
        context: TransitionContext,
        stage: CallbackStage | str = CallbackStage.POST,
        exception: Exception | None = None,
    ) -> tuple[bool, list[tuple[BaseTransitionCallback, Exception | None]]]:
        """
        Execute all callbacks for a transition.

        Args:
            context: The transition context.
            stage: The callback stage to execute.
            exception: Exception that occurred (for error callbacks).

        Returns:
            Tuple of (overall_success, list of (callback, exception) for failures).
        """
        if isinstance(stage, str):
            stage = CallbackStage(stage)

        callbacks = self.get_callbacks(
            context.model_class,
            context.field_name,
            context.source_state,
            context.target_state,
            stage,
        )

        failures: list[tuple[BaseTransitionCallback, Exception | None]] = []
        overall_success = True

        for callback in callbacks:
            try:
                # Check if callback should execute
                if not callback.should_execute(context):
                    logger.debug(f"Skipping callback {callback.name} - " f"should_execute returned False")
                    continue

                # Execute callback
                logger.debug(f"Executing {stage.value} callback: {callback.name}")

                if stage == CallbackStage.ERROR:
                    result = callback.execute(context, exception=exception)
                else:
                    result = callback.execute(context)

                if not result:
                    logger.warning(f"Callback {callback.name} returned False for {context}")
                    failures.append((callback, None))
                    overall_success = False

                if not callback.continue_on_error:
                        logger.error(
                            f"Aborting callback chain - {callback.name} failed "
                            f"and continue_on_error=False"
                        )
                        break

            except Exception as e:
                logger.exception(f"Callback {callback.name} raised exception for {context}: {e}")
                failures.append((callback, e))
                overall_success = False

                if not callback.continue_on_error:
                    logger.error(
                        f"Aborting callback chain - {callback.name} raised exception "
                        f"and continue_on_error=False"
                    )
                    break

        return overall_success, failures

    def clear(self, model_class: type[models.Model] | None = None) -> None:
        """
        Clear registered callbacks.

        Args:
            model_class: If provided, only clear callbacks for this model.
                        If None, clear all callbacks.
        """
        if model_class is None:
            for stage in CallbackStage:
                self._callbacks[stage] = []
        else:
            for stage in CallbackStage:
                self._callbacks[stage] = [r for r in self._callbacks[stage] if r.model_class != model_class]

    def get_all_registrations(
        self,
        model_class: type[models.Model] | None = None,
    ) -> dict[CallbackStage, list[CallbackRegistration]]:
        """
        Get all registered callbacks, optionally filtered by model class.

        Args:
            model_class: If provided, only return callbacks for this model.

        Returns:
            Dict mapping stages to lists of registrations.
        """
        if model_class is None:
            return dict(self._callbacks)

        filtered = {}
        for stage, registrations in self._callbacks.items():
            filtered[stage] = [r for r in registrations if r.model_class == model_class]
        return filtered

    @classmethod
    def reset_instance(cls) -> None:
        """Reset the singleton instance. Mainly for testing."""
        cls._instance = None
        cls._initialized = False


# Global registry instance
callback_registry = TransitionCallbackRegistry()


# Convenience functions for common operations
def register_callback(
    model_class: type[models.Model],
    field_name: str,
    source: str,
    target: str,
    callback: BaseTransitionCallback,
    stage: CallbackStage | str = CallbackStage.POST,
) -> None:
    """Convenience function to register a callback."""
    callback_registry.register(model_class, field_name, source, target, callback, stage)


def register_pre_callback(
    model_class: type[models.Model],
    field_name: str,
    source: str,
    target: str,
    callback: PreTransitionCallback,
) -> None:
    """Convenience function to register a pre-transition callback."""
    callback_registry.register(model_class, field_name, source, target, callback, CallbackStage.PRE)


def register_post_callback(
    model_class: type[models.Model],
    field_name: str,
    source: str,
    target: str,
    callback: PostTransitionCallback,
) -> None:
    """Convenience function to register a post-transition callback."""
    callback_registry.register(model_class, field_name, source, target, callback, CallbackStage.POST)


def register_error_callback(
    model_class: type[models.Model],
    field_name: str,
    source: str,
    target: str,
    callback: ErrorTransitionCallback,
) -> None:
    """Convenience function to register an error callback."""
    callback_registry.register(model_class, field_name, source, target, callback, CallbackStage.ERROR)
