"""
Declarative Pydantic-based transition system for FSM engine.

This module provides a framework for defining state transitions as first-class
Pydantic models with built-in validation, context passing, and middleware-like
functionality for enhanced declarative state management.
"""

from abc import ABC, abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, TypeVar

from django.db.models import Model
from pydantic import BaseModel, ConfigDict, Field

if TYPE_CHECKING:
    from fsm.state_models import BaseState

    # Type variables for generic transition context
    EntityType = TypeVar('EntityType', bound=Model)
    StateModelType = TypeVar('StateModelType', bound=BaseState)
else:
    EntityType = TypeVar('EntityType')
    StateModelType = TypeVar('StateModelType')


class TransitionContext(BaseModel, Generic[EntityType, StateModelType]):
    """
    Context object passed to all transitions containing middleware-like information.

    This provides access to current state, entity, user, and other contextual
    information needed for transition validation and execution.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    # Core context information
    entity: Any = Field(..., description='The entity being transitioned')
    current_user: Optional[Any] = Field(None, description='User triggering the transition (request user)')
    current_state_object: Optional[Any] = Field(None, description='Full current state object')
    current_state: Optional[str] = Field(None, description='Current state as string')
    target_state: Optional[str] = Field(
        None, description='Target state for this transition (None for side-effect only transitions)'
    )

    # Timing and metadata
    timestamp: datetime = Field(default_factory=datetime.now, description='When transition was initiated')
    transition_name: Optional[str] = Field(None, description='Name of the transition method')

    # Additional context data
    request_data: Dict[str, Any] = Field(default_factory=dict, description='Additional request/context data')
    metadata: Dict[str, Any] = Field(default_factory=dict, description='Transition-specific metadata')

    # Organizational context
    organization_id: Optional[int] = Field(None, description='Organization context for the transition')

    # Validation context, for cases where we want to skip validation for the transition
    skip_validation: Optional[bool] = Field(default=False, description='Whether to skip validation for the transition')

    # Reason override - if provided, takes precedence over Transition.get_reason()
    # This allows callers to provide context-specific reasons for transitions
    # (e.g., "Project moved from Sandbox to FSM Testing workspace")
    reason: Optional[str] = Field(
        None, description='Override reason for this transition (takes precedence over get_reason)'
    )

    # Additional context data to be merged with transition's context_data
    # This allows callers to add extra data to be stored in the state record's JSONB context_data
    # (e.g., workspace_from_id, workspace_to_id for workspace change transitions)
    context_data: Dict[str, Any] = Field(
        default_factory=dict, description='Additional context data to store with state record'
    )

    @property
    def has_current_state(self) -> bool:
        """Check if entity has a current state"""
        return self.current_state is not None

    @property
    def is_initial_transition(self) -> bool:
        """Check if this is the first state transition for the entity"""
        return not self.has_current_state


class TransitionValidationError(Exception):
    """Exception raised when transition validation fails"""

    def __init__(self, message: str, context: Optional[Dict[str, Any]] = None):
        super().__init__(message)
        self.context = context or {}


class BaseTransition(BaseModel, ABC, Generic[EntityType, StateModelType]):
    """
    Abstract base class for all declarative state transitions.

    This provides the framework for implementing transitions as first-class Pydantic
    models with built-in validation, context handling, and execution logic.

    Example usage:
        class StartTaskTransition(BaseTransition[Task, TaskState]):
            assigned_user_id: int = Field(..., description="User assigned to start the task")
            estimated_duration: Optional[int] = Field(None, description="Estimated completion time in hours")

            def get_target_state(self, context: Optional[TransitionContext[Task, TaskState]]) -> str:
                return TaskStateChoices.IN_PROGRESS

            def validate_transition(self, context: TransitionContext[Task, TaskState]) -> bool:
                if context.current_state == TaskStateChoices.COMPLETED:
                    raise TransitionValidationError("Cannot start an already completed task")
                return True

            def transition(self, context: TransitionContext[Task, TaskState]) -> Dict[str, Any]:
                return {
                    "assigned_user_id": self.assigned_user_id,
                    "estimated_duration": self.estimated_duration,
                    "started_at": context.timestamp.isoformat()
                }
    """

    model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True, use_enum_values=True)

    def __init__(self, **data):
        super().__init__(**data)
        self.__context: Optional[TransitionContext[EntityType, StateModelType]] = None

    @property
    def context(self) -> Optional[TransitionContext[EntityType, StateModelType]]:
        """Access the current transition context"""
        return getattr(self, '_BaseTransition__context', None)

    @context.setter
    def context(self, value: TransitionContext[EntityType, StateModelType]):
        """Set the transition context"""
        self.__context = value

    @abstractmethod
    def get_target_state(
        self, context: Optional[TransitionContext[EntityType, StateModelType]] = None
    ) -> Optional[str]:
        """
        Get the target state this transition leads to.

        Can optionally use context to compute target_state dynamically.
        If context is None, should return a static target_state or None.

        Args:
            context: Optional transition context (may be minimal with just entity)

        Returns:
            String representation of the target state, or None for side-effect only transitions
            that don't create state records (e.g., audit-only or notification-only transitions)
        """
        pass

    @property
    def transition_name(self) -> str:
        """
        Name of this transition for audit purposes.

        Returns the registered transition name from the decorator, or falls back
        to the class name in snake_case.
        """
        # Use the registered name if available (set by @register_state_transition decorator)
        if hasattr(self.__class__, '_transition_name'):
            return self.__class__._transition_name

        # Fallback to class name in snake_case for backward compatibility
        class_name = self.__class__.__name__
        result = ''
        for i, char in enumerate(class_name):
            if char.isupper() and i > 0:
                result += '_'
            result += char.lower()
        return result

    @classmethod
    def can_transition_from_state(cls, context: TransitionContext[EntityType, StateModelType]) -> bool:
        """
        Class-level validation for whether this transition type is allowed from the current state.

        This method checks if the transition is structurally valid (e.g., allowed state transitions)
        without needing the actual transition data. Override this to implement state-based rules.

        Args:
            context: The transition context containing entity, user, and state information

        Returns:
            True if transition type is allowed from current state, False otherwise
        """
        return True

    def validate_transition(self, context: TransitionContext[EntityType, StateModelType]) -> bool:
        """
        Validate whether this specific transition instance can be performed.

        This method validates both the transition type (via can_transition_from_state)
        and the specific transition data. Override to add data-specific validation.

        Args:
            context: The transition context containing entity, user, and state information

        Returns:
            True if transition is valid, False otherwise

        Raises:
            TransitionValidationError: If transition validation fails with specific reason
        """
        # First check if this transition type is allowed
        if not self.can_transition_from_state(context):
            return False

        # Then perform instance-specific validation
        return True

    def pre_transition_hook(self, context: TransitionContext[EntityType, StateModelType]) -> None:
        """
        Hook called before the transition is executed.

        Use this for any setup or preparation needed before state change.
        Override in subclasses as needed.

        Args:
            context: The transition context
        """
        pass

    @abstractmethod
    def transition(self, context: TransitionContext[EntityType, StateModelType]) -> Dict[str, Any]:
        """
        Execute the transition and return context data for the state record.

        This is the core method that implements the transition logic.
        Must be implemented by all concrete transition classes.

        Args:
            context: The transition context containing all necessary information

        Returns:
            Dictionary of context data to be stored with the state record

        Raises:
            TransitionValidationError: If transition cannot be completed
        """
        pass

    def post_transition_hook(
        self, context: TransitionContext[EntityType, StateModelType], state_record: StateModelType
    ) -> None:
        """
        Hook called after the transition has been successfully executed.

        Use this for any cleanup, notifications, or side effects after state change.
        Override in subclasses as needed.

        Args:
            context: The transition context
            state_record: The newly created state record
        """
        pass

    def get_reason(self, context: TransitionContext[EntityType, StateModelType]) -> str:
        """
        Get a human-readable reason for this transition.

        Override in subclasses to provide more specific reasons.

        Note: If `context.reason` is set, it takes precedence over this method.
        This allows callers to provide context-specific reasons when executing
        transitions (e.g., "Project moved from Sandbox to shared workspace").

        Args:
            context: The transition context

        Returns:
            Human-readable reason string
        """
        user_info = f'by {context.current_user}' if context.current_user else 'automatically'
        return f'{self.__class__.__name__} executed {user_info}'

    def prepare_and_validate(self, context: TransitionContext[EntityType, StateModelType]) -> Dict[str, Any]:
        """
        Prepare and validate the transition, returning the transition data.

        This method handles the preparation phase of the transition:
        1. Set context on the transition instance
        2. Validate the transition if not skipped
        3. Execute pre-transition hooks
        4. Perform the actual transition logic

        Args:
            context: The transition context

        Returns:
            Dictionary of transition data to be stored with the state record

        Raises:
            TransitionValidationError: If validation fails
        """
        # Set context for access during transition
        self.context = context

        # Update context with transition name
        context.transition_name = self.transition_name

        try:
            # Validate transition
            if not context.skip_validation and not self.validate_transition(context):
                raise TransitionValidationError(
                    f'Transition validation failed for {self.transition_name}',
                    {'current_state': context.current_state, 'target_state': context.target_state},
                )

            # Pre-transition hook
            self.pre_transition_hook(context)

            # Execute the transition logic
            transition_data = self.transition(context)

            return transition_data

        except Exception:
            # Clear context on error
            self.context = None
            raise

    def finalize(self, context: TransitionContext[EntityType, StateModelType], state_record: StateModelType) -> None:
        """
        Finalize the transition after the state record has been created.

        This method handles post-transition activities:
        1. Execute post-transition hooks
        2. Clear the context

        Args:
            context: The transition context
            state_record: The newly created state record
        """
        try:
            # Post-transition hook
            self.post_transition_hook(context, state_record)
        finally:
            # Always clear context when done
            self.context = None


class ModelChangeTransition(BaseTransition, Generic[EntityType, StateModelType]):
    """
    Specialized transition class for model-triggered state changes.

    This class extends BaseTransition with additional context about model changes,
    making it ideal for transitions triggered by FsmHistoryStateModel.save() operations.

    Features:
    - Access to changed fields (old vs new values)
    - Knowledge of whether entity is being created or updated
    - Automatic integration with FsmHistoryStateModel lifecycle
    - Declarative trigger field specification

    Example usage:
        @register_state_transition('task', 'task_created', triggers_on_create=True)
        class TaskCreatedTransition(ModelChangeTransition[Task, TaskState]):
            def get_target_state(self, context: Optional[TransitionContext] = None) -> str:
                return 'CREATED'

            def transition(self, context: TransitionContext) -> Dict[str, Any]:
                return {'reason': 'Task created'}

        @register_state_transition('task', 'task_labeled', triggers_on=['is_labeled'])
        class TaskLabeledTransition(ModelChangeTransition[Task, TaskState]):
            def get_target_state(self, context: Optional[TransitionContext] = None) -> str:
                return 'ANNOTATION_COMPLETE'

            def transition(self, context: TransitionContext) -> Dict[str, Any]:
                return {'reason': 'Task became labeled'}
    """

    # Additional fields specific to model changes
    changed_fields: Dict[str, Dict[str, Any]] = Field(
        default_factory=dict, description="Fields that changed: {field_name: {'old': value, 'new': value}}"
    )
    is_creating: bool = Field(default=False, description='Whether this is a new entity creation')

    # Class-level metadata for trigger configuration (set by decorator)
    _triggers_on_create: bool = False
    _triggers_on_update: bool = True
    _trigger_fields: list = []  # Fields that trigger this transition

    def should_execute(self, context: TransitionContext[EntityType, StateModelType]) -> bool:
        """
        Determine if this transition should execute based on model changes.

        Override in subclasses to provide specific logic based on:
        - Whether entity is being created (is_creating)
        - Which fields changed (changed_fields)
        - Current and target states

        Default implementation always returns True.

        Args:
            context: The transition context with entity and state information

        Returns:
            True if transition should execute, False to skip

        Example:
            def should_execute(self, context: TransitionContext) -> bool:
                # Only execute if is_labeled changed to True
                if 'is_labeled' in self.changed_fields:
                    old_val = self.changed_fields['is_labeled']['old']
                    new_val = self.changed_fields['is_labeled']['new']
                    return not old_val and new_val
                return False
        """
        return True

    def validate_transition(self, context: TransitionContext[EntityType, StateModelType]) -> bool:
        """
        Validate whether this transition should execute.

        Extends parent validation with should_execute() check.

        Args:
            context: The transition context

        Returns:
            True if transition is valid and should execute

        Raises:
            TransitionValidationError: If validation fails
        """
        # First check parent validation
        if not super().validate_transition(context):
            return False

        # Then check if we should execute based on model changes
        if not self.should_execute(context):
            return False

        return True

    @classmethod
    def from_model_change(
        cls, is_creating: bool, changed_fields: Dict[str, tuple], **extra_data
    ) -> 'ModelChangeTransition':
        """
        Factory method to create a transition from model change data.

        This is called by FsmHistoryStateModel when a transition needs to be executed.

        Args:
            is_creating: Whether the model is being created
            changed_fields: Dict of changed fields (field_name -> (old, new))
            **extra_data: Additional data to pass to the transition

        Returns:
            Configured transition instance
        """
        # Convert changed_fields from tuple format to dict format
        converted_fields = {
            field_name: {'old': old_val, 'new': new_val} for field_name, (old_val, new_val) in changed_fields.items()
        }

        return cls(is_creating=is_creating, changed_fields=converted_fields, **extra_data)

    def get_reason(self, context: TransitionContext[EntityType, StateModelType]) -> str:
        """
        Get a human-readable reason for this model change transition.

        Override to provide more specific reasons based on model changes.

        Args:
            context: The transition context

        Returns:
            Human-readable reason string
        """
        if self.is_creating:
            return f'{context.entity.__class__.__name__} created'

        if self.changed_fields:
            fields = ', '.join(self.changed_fields.keys())
            return f'{context.entity.__class__.__name__} updated ({fields} changed)'

        return f'{context.entity.__class__.__name__} modified'
