# Generated by Django 3.2.9 on 2022-01-25 19:01
import logging

from django.db import migrations
from django.db.models import CharField, Count, Value
from django.db.models.functions import Concat

logger = logging.getLogger(__name__)


def get_value(attribute_value):
    typ = attribute_value.attribute.type
    field_name = "value_%s" % typ
    value = getattr(attribute_value, field_name)
    if hasattr(value, "all"):
        value = value.all()
    return value

def remove_duplicate_attributes(apps, schema_editor):
    """
    Removes duplicate attributes that have the same code and product class.
    """
    ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
    ProductAttributeValue = apps.get_model('catalogue', 'ProductAttributeValue')
    ProductClass = apps.get_model("catalogue", "ProductClass")

    # Instead of iterating over all attributes, we concat the code and product class pk
    # with a "|" so we can find duplicate attributes in one query.
    duplicate_attributes = ProductAttribute.objects.filter(product_class__isnull=False).annotate(
        code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
    ).values('code_and_product_class').annotate(
        same_code_count=Count('code_and_product_class')
    ).filter(same_code_count__gt=1)

    for attribute in duplicate_attributes:
        attribute_code, product_class_pk = attribute["code_and_product_class"].split("|")
        product_class = ProductClass.objects.get(pk=product_class_pk)
        attributes = ProductAttribute.objects.filter(
            code=attribute_code,
            product_class=product_class
        )
        used_attributes = attributes.filter(productattributevalue__isnull=False)
        used_attribute_count = used_attributes.distinct().count()

        # In most cases, the used attributes count will be one or zero as
        # the dashboard will always show one attribute. If the used attribute
        # count is one, we exclude this from attributes and remove the others.
        # If it's zero, we pick the last created and delete others.
        if used_attribute_count == 1:
            attributes.exclude(pk=used_attributes.first().pk).delete()
            continue
        elif used_attribute_count == 0:
            attributes.exclude(pk=attributes.last().pk).delete()
            continue

        # If we found multiple attributes that have values linked to them,
        # we must move them to one attribute and then delete the others.
        # We can only do this if the value_types are all the same!
        ASSERTION_MESSAGE = """Duplicate attribute found with code: %s but different types!
        You could fix this by renaming the duplicate codes or by matching all types to one
        type and update the attribute values accordingly for their new type. After that you can
        re-run the migration.""" % attribute_code
        assert used_attributes.values("type").distinct().count() == 1, ASSERTION_MESSAGE

        # Choose one attribute that will be used to move to and others to be deleted.
        to_be_used_attribute = used_attributes.first()
        to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)

        for attribute in to_be_deleted_attributes:
            for attribute_value in attribute.productattributevalue_set.all():
                product = attribute_value.product

                # ProductAttributeValue has a unique together constraint on 'product' and 'attribute'.
                # This means, if the product of the current 'attribute_value' already has a ProductAttributeValue
                # linked to the 'to_be_used_attribute' attribute, we can't update the attribute on the
                # 'attribute_value' as this would raise an IntegrityError.
                to_be_used_attribute_value = to_be_used_attribute.productattributevalue_set.filter(product=product).first()
                if not to_be_used_attribute_value:
                    attribute_value.attribute = to_be_used_attribute
                    attribute_value.save()
                else:
                    msg = """Product with ID '%s' had more than one attribute value linked to an attribute
                    with code '%s'. We've kept the value '%s' and removed the value '%s' as this is the one you
                    would see in the dashboard when editing the product.
                    """ % (
                        product.id,
                        attribute.code,
                        get_value(to_be_used_attribute_value),
                        get_value(attribute_value)
                    )
                    logger.warning(msg)

            # Once the attribute values have been updated, we can safely remove the attribute instance.
            attribute.delete()



class Migration(migrations.Migration):

    dependencies = [
        ('catalogue', '0023_auto_20210824_1414'),
    ]

    operations = [
        migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
    ]
