"""
Extracted and slightly modified from django-extensions

https://github.com/django-extensions/django-extensions/blob/main/django_extensions/management/commands/merge_model_instances.py
"""

from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction, DatabaseError


def get_generic_fields() -> list[GenericForeignKey]:
    """Return a list of all GenericForeignKeys in all models."""
    generic_fields = []
    for model in apps.get_models():
        for field_name, field in model.__dict__.items():
            if isinstance(field, GenericForeignKey):
                generic_fields.append(field)
    return generic_fields


def merge_model_instances(primary_object, alias_objects, skip_relations=(), delete_alias=True):
    """
    Merge several model instances into one, the `primary_object`.
    Use this function to merge model objects and migrate all of the related
    fields from the alias objects the primary object.

    :param primary_object: Object that all other objects will be merged into.
    :param alias_objects: Iterable of objects that will be merged and deleted.
    :param skip_relations: Iterable of field relation names that will not merged.
    :param delete_alias: Delete or retain original alias record.
    """
    generic_fields = get_generic_fields()

    related_fields = [
        f
        for f in primary_object._meta.get_fields()
        if (f.one_to_many or f.one_to_one)
        and not f.concrete
        and not f.name.startswith(skip_relations)
    ]

    many_to_many_fields = list(filter(lambda x: x.many_to_many is True, related_fields))
    related_fields = list(filter(lambda x: x.many_to_many is False, related_fields))

    # Loop through all alias objects and migrate their references to the primary object
    deleted_objects = []
    deleted_objects_count = 0
    for alias_object in alias_objects:
        if alias_object == primary_object or not alias_object:
            continue

        # Migrate all foreign key references from alias object to primary object.
        for many_to_many_field in many_to_many_fields:
            alias_varname = many_to_many_field.name
            related_objects = getattr(alias_object, alias_varname)
            for obj in related_objects.all():
                try:
                    # Handle regular M2M relationships.
                    getattr(alias_object, alias_varname).remove(obj)
                    getattr(primary_object, alias_varname).add(obj)
                except AttributeError:
                    # Handle M2M relationships with a 'through' model.
                    # This does not delete the 'through model.
                    # TODO: Allow the user to delete a duplicate 'through' model.
                    through_model = getattr(alias_object, alias_varname).through
                    kwargs = {
                        many_to_many_field.m2m_reverse_field_name(): obj,
                        many_to_many_field.m2m_field_name(): alias_object,
                    }
                    through_model_instances = through_model.objects.filter(**kwargs)
                    for instance in through_model_instances:
                        try:
                            # Re-attach the through model to the primary_object
                            setattr(instance, many_to_many_field.m2m_field_name(), primary_object)
                            with transaction.atomic():
                                instance.save()
                        except DatabaseError:
                            pass

        for related_field in related_fields:
            if related_field.one_to_many:
                alias_varname = related_field.get_accessor_name()
                related_objects = getattr(alias_object, alias_varname)
                for obj in related_objects.all():
                    try:
                        field_name = related_field.field.name
                        setattr(obj, field_name, primary_object)
                        with transaction.atomic():
                            obj.save()
                    except DatabaseError:
                        try:
                            obj.delete(force=True)
                        except TypeError:
                            obj.delete()

            elif related_field.one_to_one or related_field.many_to_one:
                alias_varname = related_field.name
                try:
                    related_object = getattr(alias_object, alias_varname)
                except (AttributeError, ObjectDoesNotExist):
                    continue
                try:
                    primary_related_object = getattr(primary_object, alias_varname)
                except (AttributeError, ObjectDoesNotExist):
                    primary_related_object = None
                try:
                    with transaction.atomic():
                        if primary_related_object is None:
                            setattr(primary_object, alias_varname, related_object)
                            primary_object.save()
                        elif related_field.one_to_one:
                            related_object.delete()
                except DatabaseError:
                    pass

        for field in generic_fields:
            filter_kwargs = {
                field.fk_field: alias_object._get_pk_val(),
                field.ct_field: field.get_content_type(alias_object),
            }
            related_objects = field.model.objects.filter(**filter_kwargs)
            for generic_related_object in related_objects:
                setattr(generic_related_object, field.name, primary_object)
                try:
                    with transaction.atomic():
                        generic_related_object.save()
                except DatabaseError:
                    continue

        if delete_alias and alias_object.id:
            deleted_objects += [alias_object]
            try:
                with transaction.atomic():
                    alias_object.delete()
                    deleted_objects_count += 1
            except DatabaseError:
                pass

    return primary_object, deleted_objects, deleted_objects_count


__all__ = [
    'merge_model_instances',
]
