# Generated by Django 5.2.1 on 2025-09-24 07:56

from collections import defaultdict

import django.db.models.deletion
import teamvault.apps.secrets.models
from django.conf import settings
from django.db import migrations, models
from django.utils.timezone import now
from hashids import Hashids


PAYLOAD_CATEGORIES = {'secret_changed', 'secret_restored'}


def snapshot_metadata(secret) -> dict:
    access_policy = getattr(secret, 'access_policy', None)
    status = getattr(secret, 'status', None)
    return {
        'name': getattr(secret, 'name', None),
        'description': getattr(secret, 'description', None),
        'username': getattr(secret, 'username', None),
        'url': getattr(secret, 'url', None),
        'filename': getattr(secret, 'filename', None),
        'access_policy': int(access_policy) if access_policy is not None else None,
        'needs_changing_on_leave': getattr(secret, 'needs_changing_on_leave', False),
        'status': int(status) if status is not None else None,
    }


def coalesce_timestamp(*candidates):
    for value in candidates:
        if value is not None:
            return value
    return now()


def build_events(secret, revisions_by_id, secret_logs):
    events = []
    usage_counter = defaultdict(int)

    for log in secret_logs:
        revision = revisions_by_id.get(log.secret_revision_id)
        if revision is None:
            continue
        actor_id = (
            getattr(log, 'actor_id', None)
            or getattr(revision, 'set_by_id', None)
            or getattr(secret, 'created_by_id', None)
        )
        events.append(
            {
                'revision_id': revision.id,
                'actor_id': actor_id,
                'created': getattr(log, 'time', None),
                'category': log.category,
                'log_id': log.id,
            }
        )
        usage_counter[revision.id] += 1

    for revision in revisions_by_id.values():
        if usage_counter.get(revision.id):
            continue
        actor_id = getattr(revision, 'set_by_id', None) or getattr(secret, 'created_by_id', None)
        events.append(
            {
                'revision_id': revision.id,
                'actor_id': actor_id,
                'created': getattr(revision, 'created', None),
                'category': 'seed_from_revision',
                'log_id': None,
            }
        )

    secret_created = getattr(secret, 'created', None)
    events.sort(
        key=lambda e: (
            coalesce_timestamp(e['created'], secret_created),
            0 if e['log_id'] is not None else 1,
            e['revision_id'],
        )
    )
    return events


def backfill_secret_changes(apps, schema_editor):
    db_alias = schema_editor.connection.alias
    SecretModel = apps.get_model('secrets', 'Secret')
    SecretRevisionModel = apps.get_model('secrets', 'SecretRevision')
    SecretChangeModel = apps.get_model('secrets', 'SecretChange')
    LogEntryModel = apps.get_model('audit', 'LogEntry')

    hashids = Hashids(
        min_length=settings.HASHID_MIN_LENGTH,
        salt='SecretChange' + settings.HASHID_SALT,
    )

    logs_by_secret: dict[int, list] = defaultdict(list)
    logs_qs = (
        LogEntryModel.objects.using(db_alias)
        .filter(
            category__in=PAYLOAD_CATEGORIES,
            secret__isnull=False,
            secret_revision__isnull=False,
        )
        .order_by('secret_id', 'time', 'id')
    )
    for log in logs_qs.iterator():
        logs_by_secret[log.secret_id].append(log)

    for secret in SecretModel.objects.using(db_alias).all().iterator():
        if SecretChangeModel.objects.using(db_alias).filter(secret_id=secret.id).exists():
            continue

        revisions_qs = SecretRevisionModel.objects.using(db_alias).filter(secret_id=secret.id)
        revisions_by_id = {rev.id: rev for rev in revisions_qs}
        if not revisions_by_id:
            continue

        events = build_events(secret, revisions_by_id, logs_by_secret.get(secret.id, []))
        if not events:
            continue

        metadata = snapshot_metadata(secret)
        parent_id = None
        last_change_for_revision: dict[int, int] = {}

        for event in events:
            revision = revisions_by_id[event['revision_id']]
            actor_id = event['actor_id'] or getattr(secret, 'created_by_id', None)
            created_ts = coalesce_timestamp(
                event['created'],
                getattr(revision, 'created', None),
                getattr(secret, 'created', None),
            )

            restored_from_id = None
            if event['category'] == 'secret_restored':
                restored_from_id = last_change_for_revision.get(revision.id)

            change = SecretChangeModel.objects.using(db_alias).create(
                secret_id=secret.id,
                revision_id=revision.id,
                actor_id=actor_id,
                parent_id=parent_id,
                restored_from_id=restored_from_id,
                created=created_ts,
                **metadata,
            )
            SecretChangeModel.objects.using(db_alias).filter(pk=change.pk).update(
                created=created_ts,
                hashid=hashids.encode(change.id),
            )

            last_change_for_revision[revision.id] = change.id
            parent_id = change.id


class Migration(migrations.Migration):

    dependencies = [
        ('secrets', '0037_change_secretrevision_plaintextdata_key_of_password_type'),
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('audit', '0009_alter_logentry_category'),
    ]

    operations = [
        migrations.AddField(
            model_name='secretrevision',
            name='last_read',
            field=models.DateTimeField(blank=True, null=True),
        ),
        migrations.CreateModel(
            name='SecretChange',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('hashid', models.CharField(max_length=24, null=True, unique=True)),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(max_length=92)),
                ('description', models.TextField(blank=True, null=True)),
                ('username', models.CharField(blank=True, max_length=255, null=True)),
                ('url', models.CharField(blank=True, max_length=255, null=True, validators=[teamvault.apps.secrets.models.validate_url])),
                ('filename', models.CharField(blank=True, max_length=255, null=True)),
                ('access_policy', models.PositiveSmallIntegerField(choices=[(1, 'discoverable'), (2, 'everyone'), (3, 'hidden')])),
                ('needs_changing_on_leave', models.BooleanField(default=True)),
                ('status', models.PositiveSmallIntegerField(choices=[(1, 'OK'), (2, 'needs changing'), (3, 'deleted')], default=1)),
                ('actor', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='secret_changes', to=settings.AUTH_USER_MODEL)),
                ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='secrets.secretchange')),
                ('restored_from', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='restores', to='secrets.secretchange')),
                ('revision', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='changes', to='secrets.secretrevision')),
                ('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changes', to='secrets.secret')),
            ],
            options={
                'ordering': ('created',),
                'indexes': [models.Index(fields=['secret', 'created'], name='secretchange_secret_create_idx'), models.Index(fields=['actor', 'created'], name='secretchange_actor_create_idx'), models.Index(fields=['revision'], name='secretchange_revision_idx'), models.Index(fields=['parent'], name='secretchange_parent_idx')],
                'constraints': [models.CheckConstraint(condition=models.Q(('restored_from', models.F('id')), _negated=True), name='secretchange_no_self_restore'), models.CheckConstraint(condition=models.Q(('parent', models.F('id')), _negated=True), name='secretchange_no_self_parent')],
            },
        ),
        migrations.RunPython(backfill_secret_changes, migrations.RunPython.noop),
    ]
