"""Copy existing MEDIA_ROOT files to R2, preserving each row's stored key.

Walks every FileField/ImageField on every model, reads the local file, and writes
it to the configured R2 bucket under the exact same name. Because the key does not
change, no database rows are touched — once the objects exist in R2, flipping
STORAGES['default'] to R2MediaStorage makes the same `.name` resolve there.

Dry-run by default: prints what it would upload and exits. Pass --commit to write.

Local files are left in place. Deleting MEDIA_ROOT is a separate, manual decision
to be made only after R2 reads are verified in the target environment.
"""

from django.apps import apps
from django.core.files.storage import FileSystemStorage
from django.core.management.base import BaseCommand, CommandError
from django.db import models


class Command(BaseCommand):
    help = "Copy existing local MEDIA files to the configured R2 bucket."

    def add_arguments(self, parser):
        parser.add_argument(
            "--commit",
            action="store_true",
            help="Actually upload. Without this the command only reports.",
        )
        parser.add_argument(
            "--overwrite",
            action="store_true",
            help="Re-upload objects that already exist in the bucket.",
        )
        parser.add_argument(
            "--app",
            dest="app_label",
            help="Limit to a single app label (e.g. hr).",
        )

    def handle(self, *args, **options):
        from django.conf import settings

        if not getattr(settings, "R2_BUCKET_NAME", ""):
            raise CommandError(
                "R2 is not configured. Set R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / "
                "R2_SECRET_ACCESS_KEY / R2_BUCKET_NAME before running."
            )

        from apps.core.storage_backends import R2MediaStorage

        remote = R2MediaStorage()
        # Read from disk explicitly rather than field.storage: once STORAGES has
        # been flipped to R2, field.storage IS the remote and the source would be
        # the very bucket being written to.
        local = FileSystemStorage(location=settings.MEDIA_ROOT)

        commit = options["commit"]
        overwrite = options["overwrite"]
        app_label = options.get("app_label")

        copied = skipped = missing = failed = 0
        total_bytes = 0

        for model in apps.get_models():
            if app_label and model._meta.app_label != app_label:
                continue
            file_fields = [
                f for f in model._meta.get_fields()
                if isinstance(f, models.FileField)
            ]
            if not file_fields:
                continue

            names = [f.name for f in file_fields]
            qs = model._default_manager.all().only("pk", *names).iterator()
            for obj in qs:
                for field in file_fields:
                    name = getattr(obj, field.name).name
                    if not name:
                        continue
                    if not local.exists(name):
                        missing += 1
                        self.stderr.write(f"  missing on disk: {name}")
                        continue
                    size = local.size(name)
                    # Report from local state only, so a dry run needs no network
                    # and no valid credentials. The already-present check costs a
                    # HEAD per object and belongs on the committing path.
                    if not commit:
                        copied += 1
                        total_bytes += size
                        self.stdout.write(f"  would upload {name} ({size} bytes)")
                        continue

                    if not overwrite and remote.exists(name):
                        skipped += 1
                        continue

                    try:
                        with local.open(name, "rb") as fh:
                            # _save bypasses get_available_name, which would
                            # suffix the key on collision and break the
                            # name-preserving contract this command relies on.
                            remote._save(name, fh)
                    except Exception as exc:
                        failed += 1
                        self.stderr.write(self.style.ERROR(f"  FAILED {name}: {exc}"))
                        continue
                    copied += 1
                    total_bytes += size
                    self.stdout.write(f"  uploaded {name} ({size} bytes)")

        verb = "would copy" if not commit else "copied"
        self.stdout.write(
            self.style.SUCCESS(
                f"{verb} {copied} file(s), {total_bytes} bytes; "
                f"{skipped} already present, {missing} missing on disk, {failed} failed."
            )
        )
        if not commit:
            self.stdout.write("Dry run — re-run with --commit to upload.")
        if failed:
            raise CommandError(f"{failed} upload(s) failed.")
