"""
Generic audit-trail hook.

Call `register_audit(ModelClass, project_field="project")` once per model
that should be tracked, and every create / update / delete will be written
to core.AuditLog automatically, together with the user who did it (pulled
from the thread-local set by CurrentUserMiddleware) and a before/after diff
for updates.
"""
from django.db.models.signals import post_save, pre_save, post_delete

from core.middleware import get_current_user

FIELDS_TO_SKIP = {"updated_at", "created_at", "id"}

_registry = {}


def _model_label(instance):
    return f"{instance._meta.app_label}.{instance._meta.model_name}"


def _snapshot(instance):
    data = {}
    for field in instance._meta.fields:
        if field.name in FIELDS_TO_SKIP:
            continue
        try:
            value = getattr(instance, field.name)
        except Exception:
            continue
        data[field.name] = str(value) if value is not None else None
    return data


def _pre_save_handler(sender, instance, **kwargs):
    if not instance.pk:
        return
    try:
        old = sender.objects.get(pk=instance.pk)
    except sender.DoesNotExist:
        return
    instance._audit_old_snapshot = _snapshot(old)


def _post_save_handler(sender, instance, created, **kwargs):
    from core.models import AuditLog

    user = get_current_user()
    user = user if user and getattr(user, "is_authenticated", False) else None
    project = _resolve_project(instance)

    if created:
        AuditLog.objects.create(
            project=project,
            user=user,
            action="create",
            model_name=_model_label(instance),
            object_id=str(instance.pk),
            object_repr=str(instance)[:255],
            changes=_snapshot(instance),
        )
        return

    old_snapshot = getattr(instance, "_audit_old_snapshot", None)
    new_snapshot = _snapshot(instance)
    if old_snapshot is None:
        return
    diff = {
        field: {"old": old_snapshot.get(field), "new": new_val}
        for field, new_val in new_snapshot.items()
        if old_snapshot.get(field) != new_val
    }
    if not diff:
        return
    AuditLog.objects.create(
        project=project,
        user=user,
        action="update",
        model_name=_model_label(instance),
        object_id=str(instance.pk),
        object_repr=str(instance)[:255],
        changes=diff,
    )


def _post_delete_handler(sender, instance, **kwargs):
    from core.models import AuditLog

    user = get_current_user()
    user = user if user and getattr(user, "is_authenticated", False) else None
    AuditLog.objects.create(
        project=_resolve_project(instance),
        user=user,
        action="delete",
        model_name=_model_label(instance),
        object_id=str(instance.pk),
        object_repr=str(instance)[:255],
        changes=_snapshot(instance),
    )


def _resolve_project(instance):
    """Resolve the control-database Project without querying it through the
    tenant database.

    Tenant models live in the company's database, while ``core.Project``
    lives in ``default``.  Accessing ``instance.project`` on a tenant model
    makes Django try to SELECT ``core_project`` from the tenant DB, which
    causes MySQL error 1146.  We therefore extract the project primary key
    first and explicitly load the Project from the control database.
    """
    from core.models import Project

    project_id = getattr(instance, "project_id", None)

    # Models such as Contribution reach Project through Member. The Member
    # itself is a tenant record, so reading member.project is safe only if
    # we stop at its raw project_id and do not dereference the Project FK.
    if project_id is None:
        member_id = getattr(instance, "member_id", None)
        if member_id:
            try:
                from members.models import Member
                member = Member.objects.only("project").get(pk=member_id)
                project_id = member.project_id
            except Exception:
                project_id = None

    if project_id is None:
        return None

    try:
        return Project.objects.using("default").get(pk=project_id)
    except Project.DoesNotExist:
        return None


def register_audit(model_cls):
    if model_cls in _registry:
        return
    pre_save.connect(_pre_save_handler, sender=model_cls, weak=False)
    post_save.connect(_post_save_handler, sender=model_cls, weak=False)
    post_delete.connect(_post_delete_handler, sender=model_cls, weak=False)
    _registry[model_cls] = True
