# Generated by Django 3.2.16 on 2022-11-19 23:15

from collections import namedtuple

from django.db import migrations, models

from nautobot.core.choices import ColorChoices
from nautobot.ipam.choices import IPAddressRoleChoices

# Role Models that would be integrated into this Role model are referred to as RelatedRoleModels.
# For Example: DeviceRole, RackRole e.t.c.
# while implemented_by refers to the model that actually implements the Related Model.
# e.g role = models.ForeignKey(to=DeviceRole, ...)
RelatedRoleModel = namedtuple("RelatedRoleModel", ["model", "implemented_by"])

# Using this, Choices is made to resemble a Role Queryset.
ChoicesQuerySet = namedtuple("ChoicesQuerySet", ["name", "slug", "description", "color"])

color_map = {
    "default": ColorChoices.COLOR_GREY,
    "primary": ColorChoices.COLOR_BLUE,
    "warning": ColorChoices.COLOR_AMBER,
    "success": ColorChoices.COLOR_GREEN,
}


def create_equivalent_roles_of_virtualmachine_device_role(apps):
    """Create equivalent roles for the VirtualMachine DeviceRole."""
    DeviceRole = apps.get_model("dcim", "DeviceRole")
    roles_to_create = DeviceRole.objects.filter(vm_role=True)
    create_roles(apps, roles_to_create, ["virtualization.virtualmachine"])


def create_equivalent_roles_of_related_role_model(apps):
    """Create equivalent roles for the related role model."""
    related_role_models = (
        RelatedRoleModel("dcim.DeviceRole", ["dcim.device"]),
        RelatedRoleModel("dcim.RackRole", ["dcim.rack"]),
        RelatedRoleModel("ipam.Role", ["ipam.prefix", "ipam.vlan"]),
    )

    for related_role_model in related_role_models:
        app_name, model_class = related_role_model.model.split(".")
        related_role_model_class = apps.get_model(app_name, model_class)

        roles_to_create = related_role_model_class.objects.all()
        create_roles(apps, roles_to_create, related_role_model.implemented_by)


def create_equivalent_roles_of_ipaddress_role_choices(apps):
    """Create equivalent roles for the IPAddressRoleChoices."""
    roles_to_create = []

    for value, label in IPAddressRoleChoices.CHOICES:
        color = IPAddressRoleChoices.CSS_CLASSES[value]
        choiceset = ChoicesQuerySet(name=label, slug=value, color=color_map[color], description="")
        roles_to_create.append(choiceset)
    create_roles(apps, roles_to_create, ["ipam.ipaddress"])


def create_roles(apps, roles_to_create, content_types):
    """Create/retrieve Role records as needed and set their contenttypes."""
    Role = apps.get_model("extras", "Role")
    ContentType = apps.get_model("contenttypes", "ContentType")
    ObjectChange = apps.get_model("extras", "ObjectChange")

    old_role_ct = None
    new_role_ct = None

    if hasattr(roles_to_create, "model"):
        old_role_ct = ContentType.objects.get_for_model(roles_to_create.model)
        # No sense fetching this if we don't have an old role content type
        new_role_ct = ContentType.objects.get_for_model(Role)

    for old_role in roles_to_create:
        new_role, created = Role.objects.get_or_create(
            name=old_role.name,
            defaults={
                "description": old_role.description,
                "color": getattr(old_role, "color", color_map["default"]),
                "weight": getattr(old_role, "weight", None),
                "_custom_field_data": getattr(old_role, "_custom_field_data", {}),
            },
        )
        if created:
            print(f"    Created Role {new_role.name!r}")
        else:
            updated = False
            # Fix up in the case of overlapping roles
            if new_role.weight is None and hasattr(old_role, "weight"):
                new_role.weight = old_role.weight
                updated = True
            if old_role.description and not new_role.description:
                new_role.description = old_role.description
                updated = True
            if hasattr(old_role, "_custom_field_data"):
                for field, value in old_role._custom_field_data.items():
                    if field in new_role._custom_field_data:
                        if new_role._custom_field_data[field] != value:
                            print(
                                f"    Value conflict for custom field {field!r} for role {new_role.name!r} - DATA LOSS"
                            )
                    else:
                        new_role._custom_field_data[field] = value
                        updated = True
            if updated:
                print(f"    Updated Role {new_role.name!r}")
                new_role.save()
            else:
                print(f"    No changes to already-created Role {new_role.name!r}")

        if old_role_ct and hasattr(old_role, "pk"):
            # Move over existing object change records to the new role we created
            updated_count = ObjectChange.objects.filter(
                changed_object_type=old_role_ct, changed_object_id=old_role.pk
            ).update(changed_object_type=new_role_ct, changed_object_id=new_role.pk)
            print(
                f"    Updated {updated_count} ObjectChanges from the existing {old_role.name!r} {old_role._meta.label}"
            )

    # This is for all of the change records for roles which no longer exist
    if old_role_ct:
        updated_count = ObjectChange.objects.filter(changed_object_type=old_role_ct).update(
            changed_object_type=new_role_ct
        )
        print(f"    Updated {updated_count} leftover ObjectChanges from deleted {old_role_ct.model} records.")

    roles = Role.objects.filter(name__in=[roles.name for roles in roles_to_create])

    # Add content_types to the roles
    filter_ct_by = models.Q()
    for app_and_model in content_types:
        app_label, model_name = app_and_model.split(".")
        filter_ct_by |= models.Q(app_label=app_label, model=model_name)

    content_types = ContentType.objects.filter(filter_ct_by)
    for role in roles:
        role.content_types.add(*[content_type.id for content_type in content_types])


def populate_roles_from_related_app_roles(apps, schema_editor):
    """Populate Role models using records from other related role models or choices from different apps."""
    create_equivalent_roles_of_related_role_model(apps)
    create_equivalent_roles_of_virtualmachine_device_role(apps)
    create_equivalent_roles_of_ipaddress_role_choices(apps)


def clear_populated_roles(apps, schema_editor):
    Role = apps.get_model("extras", "Role")
    Role.objects.all().delete()
    # TODO: currently we do NOT recreate DeviceRole/RackRole records, nor do we revert ObjectChange records.


class Migration(migrations.Migration):
    dependencies = [
        ("contenttypes", "0002_remove_content_type_name"),
        ("dcim", "0001_initial_part_1"),
        ("extras", "0061_role_and_alter_status"),
    ]

    operations = [
        migrations.RunPython(populate_roles_from_related_app_roles, clear_populated_roles),
    ]
