"""Backfill images from the legacy R2 storage onto the unified files backend.

One-time / idempotent migration helper. For every Image that still has a
legacy ``file_key`` and no linked ``file`` (FileUpload), this command:

  1. Downloads the object from R2 (via the same R2Storage client).
  2. Writes it to FILE_UPLOADS_FOLDER using the exact same persistence logic
     as regular file uploads (temp file + fsync + byte-count verification).
  3. Creates a FileUpload record and links it to the Image (``image.file``).
  4. Clears ``image.file_key`` so the record is fully on the files backend.

Nothing is deleted from R2 — the bucket keeps its objects until the operator
decides to purge them (run with ``--delete-r2`` for that).

Usage:
    python manage.py migrate_image_storage          # dry-run report
    python manage.py migrate_image_storage --commit # actually backfill
    python manage.py migrate_image_storage --commit --delete-r2
"""
from io import BytesIO

from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.management.base import BaseCommand

from links.file_views import _save_uploaded_file
from links.models import Image
from links.storage import R2Storage


class Command(BaseCommand):
    help = "Migrate legacy R2-backed images onto the unified files backend."

    def add_arguments(self, parser):
        parser.add_argument(
            '--commit', action='store_true',
            help='Actually perform the migration (default is a dry-run report).',
        )
        parser.add_argument(
            '--delete-r2', action='store_true',
            help='After a successful backfill, delete the legacy object from R2.',
        )

    def handle(self, *args, **options):
        commit = options['commit']
        delete_r2 = options['delete_r2']

        pending = Image.objects.filter(file__isnull=True).exclude(file_key='')
        total = pending.count()
        self.stdout.write(f'Images pending R2→files backfill: {total}')

        if not commit:
            self.stdout.write(self.style.WARNING(
                'Dry-run: re-run with --commit to perform the migration.'))
            return

        storage = R2Storage()
        done = skipped = failed = 0
        for image in pending.iterator():
            if not image.file_key:
                skipped += 1
                continue
            try:
                # 1. Download from R2
                obj = storage.client.get_object(
                    Bucket=storage.bucket, Key=image.file_key)
                raw = obj['Body'].read()
                if not raw:
                    raise ValueError(f'Empty object for {image.file_key}')

                # 2–3. Persist through the standard files-backend path
                fake = InMemoryUploadedFile(
                    BytesIO(raw), None, image.title or 'image',
                    image.content_type or 'application/octet-stream',
                    len(raw), None,
                )
                upload = _save_uploaded_file(fake)

                legacy_key = image.file_key  # capture before clearing

                # 4. Link + clear legacy key
                image.file = upload
                image.file_key = ''
                image.content_type = upload.mime_type
                image.size = upload.size
                image.save(update_fields=['file', 'file_key', 'content_type', 'size', 'updated_at'])

                if delete_r2 and legacy_key:
                    try:
                        storage.delete_file(legacy_key)
                    except Exception as exc:  # noqa: BLE001
                        self.stderr.write(f'  ⚠ R2 delete failed for {legacy_key}: {exc}')

                done += 1
                if done % 25 == 0:
                    self.stdout.write(f'  … {done}/{total}')
            except Exception as exc:  # noqa: BLE001
                failed += 1
                self.stderr.write(f'  ✗ {image.pk} ({image.file_key}): {exc}')

        self.stdout.write(self.style.SUCCESS(
            f'Backfill complete: {done} migrated, {skipped} skipped, {failed} failed '
            f'(of {total}).'))
        remaining = Image.objects.filter(file__isnull=True).exclude(file_key='').count()
        if remaining:
            self.stdout.write(self.style.WARNING(f'Still pending: {remaining} (re-run to retry).'))
        else:
            self.stdout.write(self.style.SUCCESS('All images are now on the files backend.'))

        # Sanity check: every migrated image has a real file on disk
        missing = 0
        for img in Image.objects.select_related('file').filter(file__isnull=False):
            if not img.file.file_path or not __import__('os').path.exists(img.file.file_path):
                missing += 1
        if missing:
            self.stderr.write(self.style.ERROR(f'{missing} images reference a missing file on disk!'))
        else:
            self.stdout.write(self.style.SUCCESS('Disk verification passed for all files-backed images.'))
