from collections import defaultdict
from decimal import Decimal

from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.db.models import Q, Sum

from shop_management.models.data.inventory import StockBalance, StockMovement
from shop_management.models.data.purchase import Purchase
from shop_management.models.data.shop import Shop
from shop_management.services.inventory import receive_stock


ZERO = Decimal("0")


class Command(BaseCommand):
    help = (
        "Audit posted purchase receipts and reconcile StockBalance quantities "
        "against the immutable StockMovement ledger."
    )

    def add_arguments(self, parser):
        parser.add_argument("--shop", type=int, help="Shop id")
        parser.add_argument("--shop-name", help="Exact or partial shop name")
        parser.add_argument("--purchase", type=int, help="Limit purchase checks to one id")
        parser.add_argument(
            "--product-name",
            action="append",
            dest="product_names",
            help="Exact product name; repeat for multiple products",
        )
        parser.add_argument(
            "--all",
            action="store_true",
            help="Audit every product in the selected shop",
        )
        parser.add_argument(
            "--repair-balances",
            action="store_true",
            help="Set balance quantities to ledger totals (never creates missing purchase movements)",
        )
        parser.add_argument(
            "--repair-orphan-void",
            action="store_true",
            help=(
                "For a still-POSTED --purchase, compensate orphan PurchaseVoid "
                "movements with auditable ADJUSTMENT_IN movements"
            ),
        )

    def handle(self, *args, **options):
        shop = self._shop(options)
        names = [name.strip() for name in options.get("product_names") or [] if name.strip()]
        if not options["all"] and not names and not options.get("purchase"):
            raise CommandError("Use --product-name, --purchase, or --all.")

        product_filter = Q(shop=shop)
        if names:
            name_filter = Q()
            for name in names:
                name_filter |= Q(name__iexact=name)
            product_filter &= name_filter

        from shop_management.models.data.product import Product

        products = list(Product.objects.filter(product_filter).order_by("name", "id"))
        if names:
            found = {product.name.casefold() for product in products}
            missing = [name for name in names if name.casefold() not in found]
            if missing:
                self.stdout.write(self.style.WARNING("Products not found: " + ", ".join(missing)))
            if not products:
                raise CommandError("None of the requested products exists in this shop.")

        product_ids = [product.id for product in products]
        self.stdout.write(self.style.MIGRATE_HEADING(f"Inventory ledger audit: {shop.name} (id={shop.id})"))
        self._audit_purchases(shop, product_ids, options.get("purchase"))
        self._audit_balances(shop, product_ids, options["repair_balances"])
        if options["repair_orphan_void"]:
            self._repair_orphan_void(shop, options.get("purchase"), product_ids)

    def _shop(self, options):
        if options.get("shop"):
            shop = Shop.objects.filter(pk=options["shop"]).first()
        elif options.get("shop_name"):
            name = options["shop_name"].strip()
            shop = Shop.objects.filter(name__iexact=name).first()
            if not shop:
                matches = list(Shop.objects.filter(name__icontains=name)[:2])
                shop = matches[0] if len(matches) == 1 else None
                if len(matches) > 1:
                    raise CommandError("Shop name is ambiguous; use --shop=<id>.")
        else:
            raise CommandError("Use --shop=<id> or --shop-name=\"...\".")
        if not shop:
            raise CommandError("Shop not found.")
        return shop

    def _audit_purchases(self, shop, product_ids, purchase_id):
        purchases = Purchase.objects.filter(
            shop=shop,
            document_status=Purchase.DocumentStatus.POSTED,
            is_deleted=False,
        ).prefetch_related("purchase_items__product")
        if purchase_id:
            purchases = purchases.filter(pk=purchase_id)

        self.stdout.write("")
        self.stdout.write(self.style.MIGRATE_HEADING("POSTED PURCHASE RECEIPTS"))
        checked = problems = 0
        for purchase in purchases.order_by("id"):
            items = [
                item for item in purchase.purchase_items.all()
                if not item.is_deleted and (not product_ids or item.product_id in product_ids)
            ]
            if not items:
                continue
            expected_by_product = defaultdict(Decimal)
            product_by_id = {}
            for item in items:
                expected_by_product[item.product_id] += Decimal(str(item.quantity or 0))
                product_by_id[item.product_id] = item.product
            for product_id, expected in expected_by_product.items():
                checked += 1
                received = StockMovement.objects.filter(
                    warehouse_id=purchase.warehouse_id,
                    product_id=product_id,
                    movement_type=StockMovement.MovementType.PURCHASE,
                    reference_type="Purchase",
                    reference_id=str(purchase.pk),
                ).aggregate(total=Sum("quantity_in"))["total"] or ZERO
                state = "OK"
                if not purchase.uses_inventory_engine:
                    state = "LEGACY (not received by the warehouse ledger)"
                elif not purchase.warehouse_id:
                    state = "ERROR missing warehouse"
                elif received != expected:
                    state = f"ERROR expected receipt {expected}, ledger receipt {received}"
                if state != "OK":
                    problems += 1
                self.stdout.write(
                    f"purchase#{purchase.pk} product#{product_id} {product_by_id[product_id].name!r}: "
                    f"item={expected} receipt={received} [{state}]"
                )
        self.stdout.write(f"Checked {checked} purchase lines; problems={problems}.")

    def _audit_balances(self, shop, product_ids, repair):
        movement_filter = Q(warehouse__shop=shop)
        balance_filter = Q(warehouse__shop=shop)
        if product_ids:
            movement_filter &= Q(product_id__in=product_ids)
            balance_filter &= Q(product_id__in=product_ids)

        ledger_rows = StockMovement.objects.filter(movement_filter).values(
            "warehouse_id", "product_id", "batch_id"
        ).annotate(total_in=Sum("quantity_in"), total_out=Sum("quantity_out"))
        ledger = {
            (row["warehouse_id"], row["product_id"], row["batch_id"]):
            (row["total_in"] or ZERO) - (row["total_out"] or ZERO)
            for row in ledger_rows
        }
        balances = {
            (row.warehouse_id, row.product_id, row.batch_id): row
            for row in StockBalance.objects.filter(balance_filter).select_related(
                "warehouse", "product", "batch", "cost_currency"
            )
        }

        self.stdout.write("")
        self.stdout.write(self.style.MIGRATE_HEADING("BALANCE VS MOVEMENT LEDGER"))
        problems = repaired = 0
        keys = sorted(set(ledger) | set(balances))
        for key in keys:
            expected = ledger.get(key, ZERO)
            balance = balances.get(key)
            actual = balance.quantity if balance else ZERO
            if actual == expected:
                continue
            problems += 1
            label = (
                f"warehouse#{key[0]} product#{key[1]} batch#{key[2]}"
                if not balance
                else f"{balance.warehouse.name} / {balance.product.name} / {balance.batch.batch_number}"
            )
            self.stdout.write(self.style.ERROR(f"{label}: balance={actual}, ledger={expected}"))
            if not repair:
                continue
            if expected < 0:
                self.stdout.write(self.style.WARNING("  SKIP: ledger total is negative; investigate movements."))
                continue
            if not balance:
                movement = StockMovement.objects.filter(
                    warehouse_id=key[0], product_id=key[1], batch_id=key[2]
                ).order_by("-movement_date", "-id").first()
                if not movement:
                    continue
                currencies = set(StockMovement.objects.filter(
                    warehouse_id=key[0], product_id=key[1], batch_id=key[2]
                ).values_list("currency_id", flat=True))
                if len(currencies) != 1:
                    self.stdout.write(self.style.WARNING("  SKIP: ledger has multiple currencies."))
                    continue
                with transaction.atomic():
                    StockBalance.objects.create(
                        warehouse_id=key[0], product_id=key[1], batch_id=key[2],
                        quantity=expected, average_cost=movement.unit_cost,
                        cost_currency_id=movement.currency_id,
                    )
            else:
                with transaction.atomic():
                    StockBalance.objects.select_for_update().filter(pk=balance.pk).update(
                        quantity=expected
                    )
            repaired += 1
            self.stdout.write(self.style.SUCCESS(f"  repaired quantity -> {expected}"))

        if not problems:
            self.stdout.write(self.style.SUCCESS("All balance quantities match the movement ledger."))
        else:
            self.stdout.write(f"Balance problems={problems}; repaired={repaired}.")

        self.stdout.write("")
        self.stdout.write(self.style.NOTICE("MOVEMENT HISTORY"))
        movements = StockMovement.objects.filter(movement_filter).select_related(
            "warehouse", "product", "batch"
        ).order_by("product__name", "movement_date", "id")
        totals = defaultdict(lambda: [ZERO, ZERO])
        for movement in movements:
            totals[movement.product_id][0] += movement.quantity_in
            totals[movement.product_id][1] += movement.quantity_out
            self.stdout.write(
                f"{movement.movement_date:%Y-%m-%d %H:%M} | {movement.product.name} | "
                f"{movement.movement_type} | +{movement.quantity_in} -{movement.quantity_out} | "
                f"{movement.reference_type}#{movement.reference_id or '-'} | "
                f"{movement.warehouse.name} | batch={movement.batch.batch_number}"
            )
        for product_id, (qty_in, qty_out) in sorted(totals.items()):
            self.stdout.write(f"product#{product_id} TOTAL: in={qty_in}, out={qty_out}, net={qty_in - qty_out}")

    def _repair_orphan_void(self, shop, purchase_id, product_ids):
        if not purchase_id:
            raise CommandError("--repair-orphan-void requires --purchase=<id>.")
        purchase = Purchase.objects.filter(pk=purchase_id, shop=shop).first()
        if not purchase:
            raise CommandError("Purchase not found in the selected shop.")
        if purchase.document_status != Purchase.DocumentStatus.POSTED:
            raise CommandError("Orphan void repair is allowed only while the purchase is POSTED.")

        reversals = StockMovement.objects.filter(
            reference_type="PurchaseVoid",
            reference_id=str(purchase.pk),
            movement_type=StockMovement.MovementType.VOID_REVERSAL,
        )
        if product_ids:
            reversals = reversals.filter(product_id__in=product_ids)
        reversal_rows = list(
            reversals.values("warehouse_id", "product_id", "batch_id", "currency_id")
            .annotate(quantity=Sum("quantity_out"))
            .order_by("product_id", "batch_id")
        )
        if not reversal_rows:
            raise CommandError("No matching orphan PurchaseVoid movements were found.")

        repaired = 0
        with transaction.atomic():
            for row in reversal_rows:
                repaired_qty = StockMovement.objects.filter(
                    warehouse_id=row["warehouse_id"],
                    product_id=row["product_id"],
                    batch_id=row["batch_id"],
                    reference_type="PurchaseVoidRepair",
                    reference_id=str(purchase.pk),
                    movement_type=StockMovement.MovementType.ADJUSTMENT_IN,
                ).aggregate(total=Sum("quantity_in"))["total"] or ZERO
                missing = (row["quantity"] or ZERO) - repaired_qty
                if missing <= 0:
                    self.stdout.write(
                        f"product#{row['product_id']} batch#{row['batch_id']}: already repaired"
                    )
                    continue
                reversal = reversals.filter(
                    warehouse_id=row["warehouse_id"],
                    product_id=row["product_id"],
                    batch_id=row["batch_id"],
                    currency_id=row["currency_id"],
                ).select_related("warehouse", "product", "batch", "currency").first()
                receive_stock(
                    warehouse=reversal.warehouse,
                    product=reversal.product,
                    batch=reversal.batch,
                    quantity=missing,
                    unit_cost=reversal.unit_cost,
                    currency=reversal.currency,
                    movement_type=StockMovement.MovementType.ADJUSTMENT_IN,
                    reference_type="PurchaseVoidRepair",
                    reference_id=purchase.pk,
                    remarks="Compensate orphan partial cancellation reversal",
                    created_by=purchase.posted_by,
                )
                repaired += 1
                self.stdout.write(
                    self.style.SUCCESS(
                        f"restored {missing} of {reversal.product.name} batch {reversal.batch.batch_number}"
                    )
                )
        self.stdout.write(self.style.SUCCESS(f"Orphan void repair complete; rows repaired={repaired}."))
