# Generated by Django 4.2.17 on 2025-01-22 21:53
import collections
import itertools
import logging
from typing import TYPE_CHECKING

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.db.migrations.state import StateApps

import sas.models

if TYPE_CHECKING:
    import core.models

# NB : tous les commentaires sont écrits en français,
#      parce qu'on est sur des opérations qui sont complexes,
#      et qui sont surtout DANGEREUSES.
#      Ici, la clarté des explications prime sur toute autre considération.


def copy_albums_and_pictures(apps: StateApps, schema_editor):
    SithFile: type[core.models.SithFile] = apps.get_model("core", "SithFile")
    Album: type[sas.models.Album] = apps.get_model("sas", "Album")
    Picture: type[sas.models.Picture] = apps.get_model("sas", "Picture")
    logger = logging.getLogger("django")

    # Il y a environ 1800 albums, 257k photos et 488k identifications
    # d'utilisateurs dans la db de prod.
    # En supposant qu'une insertion prenne 10ms (ce qui est très optimiste),
    # migrer tous les enregistrements de la db prendrait plus de 2h.
    # C'est trop long.
    # Mais d'un autre côté, j'ai pas assez confiance dans les capacités de nos
    # machines pour charger presque un million d'objets en mémoire.
    # Pour faire un compromis, les albums sont migrés individuellement un à un,
    # mais tous les objets liés à ces albums
    # (photos, groupes de vue, groupe d'édition, identification d'utilisateurs)
    # sont migrés en tas.
    #
    # Ordre des opérations :
    # 1. On migre les albums 1 à 1 (il y en a 1800, donc c'est relativement court)
    # 2. On migre les photos par paquet de 2500 (soit ~une centaine d'opérations)
    # 3. On migre tous les groupes de vue et tous les groupes d'édition des albums
    #
    # Au total, la migration devrait demander aux alentours de 2000 insertions,
    # ce qui est un compromis acceptable entre une migration
    # pas trop longue et une RAM pas trop surchargée.
    #
    # Pour ce qui est de la répartition des tables, quatre nouvelles tables
    # sont créées : sas_album, sas_picture,
    # sas_pictureviewgroups et sas_picture_editgroups.
    # Tous les albums et toutes les photos qui sont dans core_sithfile
    # vont être copiés dans ces tables.
    # Comme les albums sont migrés un à un, ils recevront une nouvelle
    # clef primaire.
    # Pour les photos, en revanche, c'est beaucoup plus sûr de leur donner
    # le même id que celui qu'il y avait dans core_sithfile.
    #
    # Les identifications des photos ne sont pas migrées pour l'instant.
    # Ce qu'on va faire, c'est qu'on va changer la contrainte de clef étrangère
    # sur la colonne des photos pour pointer vers sas_picture
    # au lieu de core_sithfile.
    # Cependant, pour que ça marche,
    # il faut qu'au moment où ce changement est effectué,
    # toutes les clefs primaires référencées existent à la fois dans
    # les deux tables, sinon les contraintes d'intégrité ne sont pas respectées.
    # La migration de ce fichier va donc s'occuper de créer les nouvelles tables
    # et d'y copier les données nécessaires.
    # Puis une deuxième migration s'occupera de changer les contraintes.
    # Et enfin une troisième migration supprimera les anciennes données.
    #
    # Pavé César

    albums = SithFile.objects.filter(is_in_sas=True, is_folder=True).prefetch_related(
        "view_groups", "edit_groups"
    )
    old_albums = collections.deque(
        albums.filter(parent_id=settings.SITH_SAS_ROOT_DIR_ID)
    )

    # Changement de représentation en DB.
    # Dans l'ancien système, un fichier était dans le SAS si
    # un fichier spécial (le SAS_ROOT) était parmi ses ancêtres.
    # Comme maintenant les fichiers du SAS sont dans des tables à part,
    # il ne peut plus y avoir de confusion.
    # Les photos ont donc obligatoirement un parent (qui est un album)
    # et les albums peuvent avoir un parent null.
    # Un album sans parent est considéré comme se trouvant à la racine
    # de l'arborescence.
    # En quelque sorte, None est le nouveau SITH_SAS_ROOT_DIR_ID
    album_id_old_to_new = {settings.SITH_SAS_ROOT_DIR_ID: None}

    logger.info(f"migrating {albums.count()} albums")
    while len(old_albums) > 0:
        # Comme les albums référencent leur parent, les albums doivent être migrés
        # par ordre croissant de profondeur dans l'arborescence.
        # Chaque album est donc pris par la gauche de la file
        # et ses enfants ajoutés sur la droite.
        old_album = old_albums.popleft()
        old_albums.extend(list(albums.filter(parent=old_album)))
        new_album = Album.objects.create(
            parent_id=album_id_old_to_new[old_album.parent_id],
            event_date=old_album.date.date(),
            name=old_album.name,
            thumbnail=(old_album.file or None),
            is_moderated=old_album.is_moderated,
        )
        # on garde un dictionnaire qui associe les id des albums dans l'ancienne table
        # à leur id dans la nouvelle table, pour pouvoir recréer
        # les liens de parenté entre albums
        album_id_old_to_new[old_album.id] = new_album.id

    pictures = SithFile.objects.filter(is_in_sas=True, is_folder=False)
    nb_pictures = pictures.count()
    logger.info(f"migrating {nb_pictures} pictures")
    for i, pictures_batch in enumerate(itertools.batched(pictures, 2500), start=1):
        Picture.objects.bulk_create(
            [
                Picture(
                    id=p.id,
                    name=p.name,
                    parent_id=album_id_old_to_new[p.parent_id],
                    thumbnail=p.thumbnail,
                    compressed=p.compressed,
                    original=p.file,
                    owner_id=p.owner_id,
                    created_at=p.date,
                    is_moderated=p.is_moderated,
                    asked_for_removal=p.asked_for_removal,
                    moderator_id=p.moderator_id,
                )
                for p in pictures_batch
            ]
        )
        logger.info(f"Migrated {min(i * 2500, nb_pictures)} / {nb_pictures} pictures")

    logger.info("Migrating album groups")
    albums = SithFile.objects.filter(is_in_sas=True, is_folder=True).exclude(
        id=settings.SITH_SAS_ROOT_DIR_ID
    )
    Album.edit_groups.through.objects.bulk_create(
        [
            Album.view_groups.through(
                album=album_id_old_to_new[g.sithfile_id], group_id=g.group_id
            )
            for g in SithFile.view_groups.through.objects.filter(sithfile__in=albums)
        ]
    )
    Album.edit_groups.through.objects.bulk_create(
        [
            Album.view_groups.through(
                album=album_id_old_to_new[g.sithfile_id], group_id=g.group_id
            )
            for g in SithFile.view_groups.through.objects.filter(sithfile__in=albums)
        ]
    )


class Migration(migrations.Migration):
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ("core", "0044_alter_userban_options"),
        ("sas", "0004_picturemoderationrequest_and_more"),
    ]

    operations = [
        # les relations et les demandes de modération étaient liées à SithFile,
        # via le model proxy Picture.
        # Pour que la migration marche malgré la disparition du modèle Proxy,
        # on change la relation pour qu'elle pointe directement vers SithFile
        migrations.AlterField(
            model_name="peoplepicturerelation",
            name="picture",
            field=models.ForeignKey(
                on_delete=django.db.models.deletion.CASCADE,
                related_name="people",
                to="core.sithfile",
                verbose_name="picture",
            ),
        ),
        migrations.AlterField(
            model_name="picturemoderationrequest",
            name="picture",
            field=models.ForeignKey(
                on_delete=django.db.models.deletion.CASCADE,
                related_name="moderation_requests",
                to="core.sithfile",
                verbose_name="Picture",
            ),
        ),
        migrations.DeleteModel(name="Album"),
        migrations.DeleteModel(name="Picture"),
        migrations.DeleteModel(name="SasFile"),
        migrations.CreateModel(
            name="Album",
            fields=[
                (
                    "id",
                    models.AutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "thumbnail",
                    models.FileField(
                        max_length=256,
                        upload_to=sas.models.get_thumbnail_directory,
                        verbose_name="thumbnail",
                    ),
                ),
                ("name", models.CharField(max_length=100, verbose_name="name")),
                (
                    "event_date",
                    models.DateField(
                        default=django.utils.timezone.localdate,
                        help_text="The date on which the photos in this album were taken",
                        verbose_name="event date",
                    ),
                ),
                (
                    "is_moderated",
                    models.BooleanField(default=False, verbose_name="is moderated"),
                ),
                (
                    "edit_groups",
                    models.ManyToManyField(
                        related_name="editable_albums",
                        to="core.group",
                        verbose_name="edit groups",
                    ),
                ),
                (
                    "parent",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.CASCADE,
                        related_name="children",
                        to="sas.album",
                        verbose_name="parent",
                    ),
                ),
                (
                    "view_groups",
                    models.ManyToManyField(
                        related_name="viewable_albums",
                        to="core.group",
                        verbose_name="view groups",
                    ),
                ),
            ],
            options={"verbose_name": "album"},
        ),
        migrations.CreateModel(
            name="Picture",
            fields=[
                (
                    "id",
                    models.AutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "thumbnail",
                    models.FileField(
                        unique=True,
                        upload_to=sas.models.get_thumbnail_directory,
                        verbose_name="thumbnail",
                        max_length=256,
                    ),
                ),
                ("name", models.CharField(max_length=256, verbose_name="file name")),
                (
                    "original",
                    models.FileField(
                        unique=True,
                        upload_to=sas.models.get_directory,
                        verbose_name="original image",
                        max_length=256,
                    ),
                ),
                (
                    "compressed",
                    models.FileField(
                        unique=True,
                        upload_to=sas.models.get_compressed_directory,
                        verbose_name="compressed image",
                        max_length=256,
                    ),
                ),
                ("created_at", models.DateTimeField(default=django.utils.timezone.now)),
                (
                    "is_moderated",
                    models.BooleanField(default=False, verbose_name="is moderated"),
                ),
                (
                    "asked_for_removal",
                    models.BooleanField(
                        default=False, verbose_name="asked for removal"
                    ),
                ),
                (
                    "moderator",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="moderated_pictures",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
                (
                    "owner",
                    models.ForeignKey(
                        on_delete=django.db.models.deletion.PROTECT,
                        related_name="owned_pictures",
                        to=settings.AUTH_USER_MODEL,
                        verbose_name="owner",
                    ),
                ),
                (
                    "parent",
                    models.ForeignKey(
                        on_delete=django.db.models.deletion.CASCADE,
                        related_name="pictures",
                        to="sas.album",
                        verbose_name="album",
                    ),
                ),
            ],
            options={"abstract": False, "verbose_name": "picture"},
        ),
        migrations.AddConstraint(
            model_name="picture",
            constraint=models.UniqueConstraint(
                fields=("name", "parent"), name="sas_picture_unique_per_album"
            ),
        ),
        migrations.AddConstraint(
            model_name="album",
            constraint=models.UniqueConstraint(
                fields=("name", "parent"), name="unique_album_name_if_same_parent"
            ),
        ),
        migrations.RunPython(
            copy_albums_and_pictures,
            reverse_code=migrations.RunPython.noop,
            elidable=True,
        ),
    ]