"""Transition decorator generation for django-fsm integration."""

import logging
from collections.abc import Callable
from functools import wraps
from typing import Any

from django.db import models
from django_fsm import transition
from django_fsm_log.decorators import fsm_log_by

from .callback_base import (
    BaseTransitionCallback,
    CallbackStage,
    TransitionContext,
    callback_registry,
)
from .signals import (
    post_state_transition,
    pre_state_transition,
    state_transition_failed,
)

logger = logging.getLogger(__name__)


def with_callbacks(
    field_name: str = "status",
    emit_signals: bool = True,
) -> Callable:
    """
    Decorator that wraps FSM transition methods to execute callbacks.

    This decorator should be applied BEFORE the @transition decorator:

    Example:
        @with_callbacks(field_name='status')
        @fsm_log_by
        @transition(field='status', source='PENDING', target='APPROVED')
        def transition_to_approved(self, user=None, **kwargs):
            pass

    Args:
        field_name: The name of the FSM field for this transition.
        emit_signals: Whether to emit Django signals for the transition.

    Returns:
        Decorated function with callback execution.
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(instance, *args, **kwargs):
            # Extract user from kwargs
            user = kwargs.get("user")
            
            # Pass user as 'by' for django-fsm-log's @fsm_log_by decorator
            # This must be set before calling the inner func so the decorator can capture it
            if user is not None and 'by' not in kwargs:
                kwargs['by'] = user

            # Get source state before transition
            source_state = getattr(instance, field_name, None)

            # Get target state from the transition decorator
            # The @transition decorator sets _django_fsm attribute (may be dict or FSMMeta object)
            fsm_meta = getattr(func, "_django_fsm", None)
            target_state = None
            if fsm_meta is not None:
                if isinstance(fsm_meta, dict):
                    target_state = fsm_meta.get("target", None)
                elif hasattr(fsm_meta, "target"):
                    target_state = fsm_meta.target
                elif hasattr(fsm_meta, "transitions"):
                    # FSMMeta object - try to get target from first transition
                    try:
                        transitions = list(fsm_meta.transitions.values())
                        if transitions:
                            target_state = transitions[0].target if hasattr(transitions[0], 'target') else None
                    except (AttributeError, TypeError, StopIteration):
                        pass

            # If we can't determine the target from decorator metadata,
            # we'll capture it after the transition
            if target_state is None:
                # This happens when decorators are applied in wrong order
                logger.debug(f"Could not determine target state from decorator for {func.__name__}")

            # Create transition context
            context = TransitionContext(
                instance=instance,
                field_name=field_name,
                source_state=str(source_state) if source_state else "",
                target_state=str(target_state) if target_state else "",
                user=user,
                extra_data=dict(kwargs),
            )

            # Execute pre-transition callbacks
            pre_success, pre_failures = callback_registry.execute_callbacks(context, CallbackStage.PRE)

            # If pre-callbacks fail with continue_on_error=False, abort
            if not pre_success and pre_failures:
                for callback, exc in pre_failures:
                    if not callback.continue_on_error:
                        logger.error(f"Pre-transition callback {callback.name} failed, "
                                     f"aborting transition")
                        if exc:
                            raise exc
                        raise RuntimeError(f"Pre-transition callback {callback.name} failed")

            # Emit pre-transition signal
            if emit_signals:
                pre_state_transition.send(
                    sender=type(instance),
                    instance=instance,
                    source=context.source_state,
                    target=context.target_state,
                    user=user,
                    context=context,
                )

            try:
                # Execute the actual transition
                result = func(instance, *args, **kwargs)

                # Update context with actual target state after transition
                actual_target = getattr(instance, field_name, None)
                context.target_state = str(actual_target) if actual_target else ""

                # Execute post-transition callbacks
                post_success, post_failures = callback_registry.execute_callbacks(context, CallbackStage.POST)

                if not post_success:
                    for callback, _exc in post_failures:
                        logger.warning(f"Post-transition callback {callback.name} failed " f"for {context}")

                # Emit post-transition signal
                if emit_signals:
                    post_state_transition.send(
                        sender=type(instance),
                        instance=instance,
                        source=context.source_state,
                        target=context.target_state,
                        user=user,
                        context=context,
                    )

                return result

            except Exception as e:
                # Execute error callbacks
                error_success, error_failures = callback_registry.execute_callbacks(
                    context, CallbackStage.ERROR, exception=e
                )

                # Emit failure signal
                if emit_signals:
                    state_transition_failed.send(
                        sender=type(instance),
                        instance=instance,
                        source=context.source_state,
                        target=context.target_state,
                        user=user,
                        exception=e,
                        context=context,
                    )

                # Re-raise the original exception
                raise

        return wrapper

    return decorator


def generate_transition_decorator(
    source: str,
    target: str,
    field_name: str = "status",
    **kwargs: Any,
) -> Callable:
    """
    Generate a configured @transition decorator.

    Args:
        source: Source state value(s)
        target: Target state value
        field_name: Name of the FSM field
        **kwargs: Additional arguments for @transition decorator

    Returns:
        Configured transition decorator
    """
    return transition(field=field_name, source=source, target=target, **kwargs)


def create_transition_method(
    method_name: str,
    source: str,
    target: str,
    field_name: str,
    permission_guard: Callable | None = None,
    on_success: Callable | None = None,
    on_error: Callable | None = None,
    callbacks: list[BaseTransitionCallback] | None = None,
    enable_callbacks: bool = True,
    emit_signals: bool = True,
) -> Callable:
    """
    Generate a complete transition method with decorator.

    Args:
        method_name: Name for the transition method
        source: Source state value(s)
        target: Target state value
        field_name: Name of the FSM field
        permission_guard: Optional guard function for permissions
        on_success: Optional callback on successful transition
        on_error: Optional callback on transition error
        callbacks: Optional list of callback instances to register
        enable_callbacks: Whether to wrap with callback execution
        emit_signals: Whether to emit Django signals

    Returns:
        Configured transition method with logging via django-fsm-log
    """
    conditions = []
    if permission_guard:
        conditions.append(permission_guard)

    @fsm_log_by
    @transition(
        field=field_name,
        source=source,
        target=target,
        conditions=conditions,
        on_error=on_error,
    )
    def transition_method(instance, user=None, **kwargs):
        """Execute state transition."""
        if on_success:
            on_success(instance, user=user, **kwargs)

    transition_method.__name__ = method_name
    transition_method.__doc__ = f"Transition from {source} to {target} on field {field_name}"

    # Apply callback wrapper if enabled
    if enable_callbacks:
        transition_method = with_callbacks(
            field_name=field_name,
            emit_signals=emit_signals,
        )(transition_method)

    # Store metadata for callback registration
    transition_method._fsm_metadata = {
        "source": source,
        "target": target,
        "field_name": field_name,
        "callbacks": callbacks or [],
    }

    return transition_method


def register_method_callbacks(
    model_class: type[models.Model],
    method: Callable,
) -> None:
    """
    Register callbacks defined in a transition method's metadata.

    This should be called during model initialization or app ready.

    Args:
        model_class: The model class containing the method.
        method: The transition method with _fsm_metadata.
    """
    metadata = getattr(method, "_fsm_metadata", None)
    if not metadata or not metadata.get("callbacks"):
        return

    from .callback_base import CallbackStage, PreTransitionCallback

    for callback in metadata["callbacks"]:
        # Determine stage from callback type
        stage = CallbackStage.PRE if isinstance(callback, PreTransitionCallback) else CallbackStage.POST

        callback_registry.register(
            model_class=model_class,
            field_name=metadata["field_name"],
            source=metadata["source"],
            target=metadata["target"],
            callback=callback,
            stage=stage,
        )


class TransitionMethodFactory:
    """Factory for creating standard transition methods."""

    @staticmethod
    def create_approve_method(
        source: str,
        target: str,
        field=None,
        permission_guard: Callable | None = None,
        enable_callbacks: bool = True,
        emit_signals: bool = True,
    ) -> Callable:
        """
        Create an approval transition method.

        Args:
            source: Source state value(s)
            target: Target state value
            field: FSM field object (required for django-fsm 3.x)
            permission_guard: Optional permission guard
            enable_callbacks: Whether to wrap with callback execution
            emit_signals: Whether to emit Django signals

        Returns:
            Approval transition method
        """
        # Get field name for callback wrapper
        field_name = field.name if hasattr(field, 'name') else 'status'

        @fsm_log_by
        @transition(
            field=field,
            source=source,
            target=target,
            permission=permission_guard,
        )
        def approve(instance, user=None, comment: str = "", **kwargs):
            """Approve and transition to approved state."""
            # Pass user as 'by' for django-fsm-log's @fsm_log_by decorator
            if user is not None:
                kwargs['by'] = user
            if hasattr(instance, "approved_by_id"):
                instance.approved_by = user
            if hasattr(instance, "approval_comment"):
                instance.approval_comment = comment
            if hasattr(instance, "approved_at"):
                from django.utils import timezone

                instance.approved_at = timezone.now()

        # Apply callback wrapper if enabled
        if enable_callbacks:
            approve = with_callbacks(
                field_name=field_name,
                emit_signals=emit_signals,
            )(approve)

        return approve

    @staticmethod
    def create_reject_method(
        source: str,
        target: str,
        field=None,
        permission_guard: Callable | None = None,
        enable_callbacks: bool = True,
        emit_signals: bool = True,
    ) -> Callable:
        """
        Create a rejection transition method.

        Args:
            source: Source state value(s)
            target: Target state value
            field: FSM field object (required for django-fsm 3.x)
            permission_guard: Optional permission guard
            enable_callbacks: Whether to wrap with callback execution
            emit_signals: Whether to emit Django signals

        Returns:
            Rejection transition method
        """
        # Get field name for callback wrapper
        field_name = field.name if hasattr(field, 'name') else 'status'

        @fsm_log_by
        @transition(
            field=field,
            source=source,
            target=target,
            permission=permission_guard,
        )
        def reject(instance, user=None, reason: str = "", **kwargs):
            """Reject and transition to rejected state."""
            # Pass user as 'by' for django-fsm-log's @fsm_log_by decorator
            if user is not None:
                kwargs['by'] = user
            if hasattr(instance, "rejected_by_id"):
                instance.rejected_by = user
            if hasattr(instance, "rejection_reason"):
                instance.rejection_reason = reason
            if hasattr(instance, "rejected_at"):
                from django.utils import timezone

                instance.rejected_at = timezone.now()

        # Apply callback wrapper if enabled
        if enable_callbacks:
            reject = with_callbacks(
                field_name=field_name,
                emit_signals=emit_signals,
            )(reject)

        return reject

    @staticmethod
    def create_escalate_method(
        source: str,
        target: str,
        field=None,
        permission_guard: Callable | None = None,
        enable_callbacks: bool = True,
        emit_signals: bool = True,
    ) -> Callable:
        """
        Create an escalation transition method.

        Args:
            source: Source state value(s)
            target: Target state value
            field: FSM field object (required for django-fsm 3.x)
            permission_guard: Optional permission guard
            enable_callbacks: Whether to wrap with callback execution
            emit_signals: Whether to emit Django signals

        Returns:
            Escalation transition method
        """
        # Get field name for callback wrapper
        field_name = field.name if hasattr(field, 'name') else 'status'

        @fsm_log_by
        @transition(
            field=field,
            source=source,
            target=target,
            permission=permission_guard,
        )
        def escalate(instance, user=None, reason: str = "", **kwargs):
            """Escalate to higher authority."""
            # Pass user as 'by' for django-fsm-log's @fsm_log_by decorator
            if user is not None:
                kwargs['by'] = user
            if hasattr(instance, "escalated_by_id"):
                instance.escalated_by = user
            if hasattr(instance, "escalation_reason"):
                instance.escalation_reason = reason
            if hasattr(instance, "escalated_at"):
                from django.utils import timezone

                instance.escalated_at = timezone.now()

        # Apply callback wrapper if enabled
        if enable_callbacks:
            escalate = with_callbacks(
                field_name=field_name,
                emit_signals=emit_signals,
            )(escalate)

        return escalate

    @staticmethod
    def create_generic_transition_method(
        method_name: str,
        source: str,
        target: str,
        field=None,
        permission_guard: Callable | None = None,
        docstring: str | None = None,
        enable_callbacks: bool = True,
        emit_signals: bool = True,
    ) -> Callable:
        """
        Create a generic transition method.

        Args:
            method_name: Name for the method
            source: Source state value(s)
            target: Target state value
            field: FSM field object (required for django-fsm 3.x)
            permission_guard: Optional permission guard
            docstring: Optional docstring for the method
            enable_callbacks: Whether to wrap with callback execution
            emit_signals: Whether to emit Django signals

        Returns:
            Generic transition method
        """
        # Get field name for callback wrapper
        field_name = field.name if hasattr(field, 'name') else 'status'

        # Create the transition function with the correct name from the start
        # by using exec to define it dynamically. This ensures __name__ is correct
        # before decorators are applied, which is critical for django-fsm's
        # method registration.
        doc = docstring if docstring else f"Transition from {source} to {target}"
        
        # Define the function dynamically with the correct name
        # IMPORTANT: We set kwargs['by'] = user so that @fsm_log_by can capture
        # who performed the transition. The decorator looks for 'by' in kwargs.
        func_code = f'''
def {method_name}(instance, user=None, **kwargs):
    """{doc}"""
    # Pass user as 'by' for django-fsm-log's @fsm_log_by decorator
    if user is not None:
        kwargs['by'] = user
    pass
'''
        local_namespace: dict = {}
        exec(func_code, {}, local_namespace)
        inner_func = local_namespace[method_name]
        
        # Apply decorators in correct order (innermost first)
        # @fsm_log_by -> @transition -> inner_func
        decorated = transition(
            field=field,
            source=source,
            target=target,
            permission=permission_guard,
        )(inner_func)
        decorated = fsm_log_by(decorated)

        # Apply callback wrapper if enabled
        if enable_callbacks:
            decorated = with_callbacks(
                field_name=field_name,
                emit_signals=emit_signals,
            )(decorated)

        return decorated


def with_transition_logging(transition_method: Callable) -> Callable:
    """
    Decorator to add django-fsm-log logging to a transition method.

    Args:
        transition_method: The transition method to wrap

    Returns:
        Wrapped method with logging
    """

    @wraps(transition_method)
    def wrapper(instance, *args, **kwargs):
        try:
            from django_fsm_log.decorators import fsm_log_by

            logged_method = fsm_log_by(transition_method)
            return logged_method(instance, *args, **kwargs)
        except ImportError:
            # django-fsm-log not available, execute without logging
            return transition_method(instance, *args, **kwargs)

    return wrapper


__all__ = [
    "generate_transition_decorator",
    "create_transition_method",
    "register_method_callbacks",
    "TransitionMethodFactory",
    "with_callbacks",
    "with_transition_logging",
]
