# Generated by Django 2.2.11 on 2021-06-14 09:08
import re

from django.db import migrations, models
from django.db.models import Count, F, Q


# noinspection PyPep8Naming,PyUnusedLocal
def forward(apps, schema_editor):
    Table = apps.get_model("database", "Table")
    Field = apps.get_model("database", "Field")

    fix_fields_with_reserved_names(Field, Table)
    fix_fields_with_duplicate_names(Field, Table)


def fix_fields_with_duplicate_names(Field, Table):
    tables_with_duplicate_fields = (
        Table.objects.annotate(duplicate_name_count=Count("field__name"))
        .filter(Q(duplicate_name_count__gt=1))
        .distinct()
        .values_list("id", "field__name")
        .order_by()
    )
    for table_id, name_to_fix in tables_with_duplicate_fields.iterator(chunk_size=2000):
        # Rename duplicate fields to Dup, Dup_2, Dupe_3, Dupe_4
        # We set the start index here to 1 so the first duplicate is not modified and
        # instead left as Dup.
        rename_non_unique_names_in_table(
            Field, table_id, name_to_fix, 1, 2, name_to_fix
        )


def fix_fields_with_reserved_names(Field, Table):
    reserved_fields = {"id", "order", ""}
    tables_with_reserved_fields = (
        Table.objects.values("id", "field__name")
        .filter(Q(field__name__in=reserved_fields))
        .distinct()
        .values_list("id", "field__name")
        .order_by()
    )
    for table_id, name_to_fix in tables_with_reserved_fields.iterator(chunk_size=2000):
        if name_to_fix == "":
            # Rename blank fields to Field_1, Field_2, Field_3 etc
            new_name_prefix = "Field"
            next_name_number = 1
        else:
            # Rename other reserved fields to order_2, order_3 etc
            next_name_number = 2
            new_name_prefix = name_to_fix
        rename_non_unique_names_in_table(
            Field, table_id, name_to_fix, 0, next_name_number, new_name_prefix
        )


def rename_non_unique_names_in_table(
    Field, table_id, name_to_fix, start_index, next_name_number, new_name_prefix
):
    """
    Given a table and a field name in that table this function will update all fields
    in with that name in table to be unique. It does this by appending an _Number to
    the field names where Number starts from next_name_number. This method will also
    ensure that new duplicates will not be created if say an existing
    FieldName_Number field exists.

    :param Field: The Field model to use.
    :param table_id: The table id to fix the fields for.
    :param name_to_fix: The name used to find all the fields in the table to fix.
    :param start_index: The starting index to start fixing fields from, so 0 would fix
        all fields matching name_to_fix, 1 would not fix the first field (ordered by id)
        but would fix all following fields etc.
    :param next_name_number: The number to start appending onto the new field names
        from. So the first fixed field will be called XXX_1 if next_name_number=1 (
        and there isn't an existing XXX_1 field).
    :param new_name_prefix: The name before the _Number to rename fixed fields to, so
        usually this should be just the field name, but say you want to rename a
        weird field name like " " to "Field_2" etc then set new_name_prefix to "Field".
    """

    fields_to_fix = Field.objects.filter(table_id=table_id, name=name_to_fix).order_by(
        "id"
    )

    escaped_name = re.escape(new_name_prefix)
    existing_collisions = set(
        Field.objects.filter(table_id=table_id, name__regex=rf"^{escaped_name}_\d+$")
        .order_by("name")
        .distinct()
        .values_list("name", flat=True)
    )
    # Skip the field with the smallest ID as we want to leave the first one
    # with the duplicate name unchanged and fix the following ones not to
    # clash.
    fields = []
    for field in fields_to_fix[start_index:]:
        new_name, next_name_number = find_next_unused_field_name(
            new_name_prefix, next_name_number, existing_collisions
        )
        field.old_name = field.name
        field.name = new_name
        fields.append(field)
    Field.objects.bulk_update(fields, ["name", "old_name"])


def find_next_unused_field_name(field_name, start_index, existing_collisions):
    """
    Finds a unused field name in the provided table starting with field_name.
    If field_name is not taken then it will be returned, if it is taken then the
    next name appended with an _X where X is a positive integer which is free will
    be returned.

    :param existing_collisions: A set of existing field names to skip over when finding
        the next free field name.
    :param start_index: The number to start looking for fields from.
    :param field_name: The field_name to find a unused name for.
    :return: A free field name starting with field_name possibly followed by an
        _X where X is a positive integer.
    """

    original_field_name = field_name
    i = start_index
    while True:
        field_name = f"{original_field_name}_{i}"
        i += 1
        if field_name not in existing_collisions:
            break
    return field_name, i


# noinspection PyPep8Naming,PyUnusedLocal
def reverse(apps, schema_editor):
    Field = apps.get_model("database", "Field")

    Field.objects.filter(old_name__isnull=False).update(name=F("old_name"))


class Migration(migrations.Migration):
    dependencies = [
        ("database", "0032_trash"),
    ]

    operations = [
        migrations.AddField(
            model_name="field",
            name="old_name",
            field=models.CharField(blank=True, max_length=255, null=True),
        ),
        migrations.RunPython(forward, reverse),
    ]
