# parsly/management/commands/import_parser_models.py
import json
import sys
from typing import Any, Dict, List, Tuple

from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction


class Command(BaseCommand):
    help = (
        "Import fixture-like JSON (list of {model, pk, fields}) and upsert rows.\n"
        "Example:\n"
        "  python manage.py import_parser_models /path/to/data.json\n"
        "  cat data.json | python manage.py import_parser_models -\n"
        "\n"
        "Optional: --clear-by-parser 1  (delete existing rows for parser_id=1 before insert)"
    )

    def add_arguments(self, parser):
        parser.add_argument(
            "json_path",
            help="Path to JSON file or '-' to read from STDIN",
        )
        parser.add_argument(
            "--clear-by-parser",
            type=int,
            default=None,
            help="If provided, deletes existing rows (per model in the JSON) where parser_id=<value> BEFORE import.",
        )
        parser.add_argument(
            "--dry-run",
            action="store_true",
            help="Parse and validate only; do not write to DB.",
        )

    def _load_json(self, path: str) -> List[Dict[str, Any]]:
        """Load JSON list from file or stdin."""
        try:
            if path.strip() == "-":
                data = sys.stdin.read()
            else:
                with open(path, "r", encoding="utf-8") as f:
                    data = f.read()
            payload = json.loads(data)
            if not isinstance(payload, list):
                raise CommandError("Top-level JSON must be a list.")
            return payload
        except FileNotFoundError:
            raise CommandError(f"File not found: {path}")
        except json.JSONDecodeError as e:
            raise CommandError(f"Invalid JSON: {e}")

    def _parse_model_label(self, label: str) -> Tuple[str, str]:
        """
        Convert 'app_label.model_name' into (app_label, model_name).
        Django's apps.get_model is case-insensitive for model_name.
        """
        try:
            app_label, model_name = label.split(".", 1)
            return app_label, model_name
        except ValueError:
            raise CommandError(f"Invalid model label '{label}'. Expected 'app_label.model_name'.")

    def handle(self, *args, **options):
        json_path = options["json_path"]
        clear_by_parser = options["clear_by_parser"]
        dry_run = options["dry_run"]

        rows = self._load_json(json_path)

        # Group rows by model so we can clear per-model if requested
        rows_by_model: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}
        for item in rows:
            if not isinstance(item, dict):
                raise CommandError("Each item in the list must be a JSON object.")
            for key in ("model", "fields"):
                if key not in item:
                    raise CommandError(f"Missing '{key}' in JSON object: {item}")
            app_label, model_name = self._parse_model_label(item["model"])
            rows_by_model.setdefault((app_label, model_name), []).append(item)

        created = 0
        updated = 0
        skipped = 0

        # Single atomic transaction to keep data consistent
        with transaction.atomic():
            for (app_label, model_name), items in rows_by_model.items():
                model = apps.get_model(app_label, model_name)
                if model is None:
                    raise CommandError(f"Model not found: {app_label}.{model_name}")

                # Optional cleanup by parser_id
                if clear_by_parser is not None:
                    # Try to delete only rows matching parser_id if the field exists
                    if hasattr(model, "_meta") and any(f.name == "parser" or f.attname == "parser_id" for f in model._meta.fields):
                        # Filter by parser_id - works if FK field is 'parser' or raw field 'parser_id'
                        delete_qs = getattr(model.objects, "all")().filter(**{"parser_id": clear_by_parser})
                        count = delete_qs.count()
                        if not dry_run:
                            delete_qs.delete()
                        self.stdout.write(self.style.WARNING(
                            f"[{app_label}.{model_name}] Deleted {count} rows with parser_id={clear_by_parser}"
                        ))
                    else:
                        self.stdout.write(self.style.WARNING(
                            f"[{app_label}.{model_name}] Skipped clear-by-parser: field 'parser'/'parser_id' not present."
                        ))

                # Upsert
                for item in items:
                    pk = item.get("pk", None)
                    fields = item["fields"] or {}

                    # Safety: ensure dict
                    if not isinstance(fields, dict):
                        raise CommandError(f"'fields' must be an object. Got: {fields}")

                    # update_or_create requires pk if we want to keep exact IDs
                    lookup = {}
                    if pk is not None:
                        lookup["pk"] = pk

                    # Defaults are all provided fields
                    defaults = dict(fields)

                    if dry_run:
                        # Validate instance by constructing it (won't hit DB)
                        _ = model(**({**lookup, **defaults}))
                        skipped += 1
                        continue

                    obj, was_created = model.objects.update_or_create(defaults=defaults, **lookup)
                    if was_created:
                        created += 1
                    else:
                        updated += 1

        msg = f"Done. created={created}, updated={updated}"
        if dry_run:
            msg = f"[DRY-RUN] validated={skipped}"
        self.stdout.write(self.style.SUCCESS(msg))
