from functools import wraps

from django.conf import settings

from auditlog import get_logentry_model
from auditlog.context import auditlog_disabled
from auditlog.diff import model_instance_diff
from auditlog.models import _get_manager_from_settings
from auditlog.signals import post_log, pre_log


def check_disable(signal_handler):
    """
    Decorator that passes along disabled in kwargs if any of the following is true:
    - 'auditlog_disabled' from threadlocal is true
    - raw = True and AUDITLOG_DISABLE_ON_RAW_SAVE is True
    """

    @wraps(signal_handler)
    def wrapper(*args, **kwargs):
        try:
            auditlog_disabled_value = auditlog_disabled.get()
        except LookupError:
            auditlog_disabled_value = False
        if not auditlog_disabled_value and not (
            kwargs.get("raw") and settings.AUDITLOG_DISABLE_ON_RAW_SAVE
        ):
            signal_handler(*args, **kwargs)

    return wrapper


@check_disable
def log_create(sender, instance, created, **kwargs):
    """
    Signal receiver that creates a log entry when a model instance is first saved to the database.

    Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
    """
    if created:
        _create_log_entry(
            action=get_logentry_model().Action.CREATE,
            instance=instance,
            sender=sender,
            diff_old=None,
            diff_new=instance,
            use_json_for_changes=settings.AUDITLOG_STORE_JSON_CHANGES,
        )


@check_disable
def log_update(sender, instance, **kwargs):
    """
    Signal receiver that creates a log entry when a model instance is changed and saved to the database.

    Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
    """
    if not instance._state.adding and instance.pk is not None:
        update_fields = kwargs.get("update_fields", None)
        old = _get_manager_from_settings(sender).filter(pk=instance.pk).first()
        _create_log_entry(
            action=get_logentry_model().Action.UPDATE,
            instance=instance,
            sender=sender,
            diff_old=old,
            diff_new=instance,
            fields_to_check=update_fields,
            use_json_for_changes=settings.AUDITLOG_STORE_JSON_CHANGES,
        )


@check_disable
def log_delete(sender, instance, **kwargs):
    """
    Signal receiver that creates a log entry when a model instance is deleted from the database.

    Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
    """
    if instance.pk is not None:
        _create_log_entry(
            action=get_logentry_model().Action.DELETE,
            instance=instance,
            sender=sender,
            diff_old=instance,
            diff_new=None,
            use_json_for_changes=settings.AUDITLOG_STORE_JSON_CHANGES,
        )


def log_access(sender, instance, **kwargs):
    """
    Signal receiver that creates a log entry when a model instance is accessed in a AccessLogDetailView.

    Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
    """
    if instance.pk is not None:
        _create_log_entry(
            action=get_logentry_model().Action.ACCESS,
            instance=instance,
            sender=sender,
            diff_old=None,
            diff_new=None,
            force_log=True,
            use_json_for_changes=settings.AUDITLOG_STORE_JSON_CHANGES,
        )


def _create_log_entry(
    action,
    instance,
    sender,
    diff_old,
    diff_new,
    fields_to_check=None,
    force_log=False,
    use_json_for_changes=False,
):
    pre_log_results = pre_log.send(
        sender,
        instance=instance,
        action=action,
    )

    if any(item[1] is False for item in pre_log_results):
        return
    LogEntry = get_logentry_model()

    error = None
    log_entry = None
    changes = None
    try:
        changes = model_instance_diff(
            diff_old,
            diff_new,
            fields_to_check=fields_to_check,
            use_json_for_changes=use_json_for_changes,
        )

        if force_log or changes:
            log_entry = LogEntry.objects.log_create(
                instance,
                action=action,
                changes=changes,
                force_log=force_log,
            )
    except BaseException as e:
        error = e
    finally:
        if log_entry or error:
            post_log.send(
                sender,
                instance=instance,
                instance_old=diff_old,
                action=action,
                error=error,
                pre_log_results=pre_log_results,
                changes=changes,
                log_entry=log_entry,
                log_created=log_entry is not None,
                use_json_for_changes=settings.AUDITLOG_STORE_JSON_CHANGES,
            )
        if error:
            raise error


def make_log_m2m_changes(field_name):
    """Return a handler for m2m_changed with field_name enclosed."""

    @check_disable
    def log_m2m_changes(signal, action, **kwargs):
        """Handle m2m_changed and call LogEntry.objects.log_m2m_changes as needed."""
        if action not in ["post_add", "post_clear", "post_remove"]:
            return
        LogEntry = get_logentry_model()

        model_manager = _get_manager_from_settings(kwargs["model"])

        if action == "post_clear":
            changed_queryset = model_manager.all()
        else:
            changed_queryset = model_manager.filter(pk__in=kwargs["pk_set"])

        if action in ["post_add"]:
            LogEntry.objects.log_m2m_changes(
                changed_queryset,
                kwargs["instance"],
                "add",
                field_name,
            )
        elif action in ["post_remove", "post_clear"]:
            LogEntry.objects.log_m2m_changes(
                changed_queryset,
                kwargs["instance"],
                "delete",
                field_name,
            )

    return log_m2m_changes
