from decimal import Decimal, ROUND_HALF_UP

from django.core.management.base import BaseCommand
from django.db import transaction

from shop_management.models.data.shop import Shop
from shop_management.models.data.stock import Stock
from shop_management.views.data.stock import normalize_currency_code


TWOPLACES = Decimal("0.01")
FOURPLACES = Decimal("0.0001")
SIX = Decimal("0.000001")
CENT = Decimal("0.01")


def _money(value):
    return Decimal(str(value or 0)).quantize(TWOPLACES, rounding=ROUND_HALF_UP)


def _stocks_for_shop(shop_id):
    return (
        Stock.objects.filter(
            shop_id=shop_id,
            is_deleted=False,
            quantity__gt=0,
            purchase_item__isnull=False,
            purchase_item__is_deleted=False,
            purchase_item__purchase__is_deleted=False,
        )
        .select_related(
            "purchase_item",
            "purchase_item__product",
            "purchase_item__currency",
            "purchase_item__purchase__currency",
        )
        .order_by("id")
    )


def _summary(shop_id):
    totals = {}
    qty_total = Decimal("0")
    for stock in _stocks_for_shop(shop_id):
        qty = Decimal(str(stock.quantity or 0))
        qty_total += qty
        code = normalize_currency_code(stock.currency.title if stock.currency else "")
        if not code:
            continue
        totals[code] = totals.get(code, Decimal("0")) + qty * Decimal(
            str(stock.average_buy_price or 0)
        )
    return qty_total, {
        code: amount.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
        for code, amount in sorted(totals.items())
    }


def _apply_cent_drift(planned, drift):
    """
    Adjust average_buy_price so sum(qty*avg) hits target.

    1) Prefer ±0.01 avg steps (2dp) using integer cent math.
    2) If leftover remains (e.g. all even qtys), use 4dp avg on smallest qty
       so leftover/qty can be stored exactly.
    """
    remaining = _money(drift)
    if remaining == 0:
        return planned, Decimal("0")

    planned = [list(item) for item in planned]
    sign = 1 if remaining > 0 else -1
    cents_left = int((abs(remaining) / CENT).to_integral_value(rounding=ROUND_HALF_UP))

    def row_sort_key(i):
        qty = int(planned[i][1])
        divides = 0 if (qty > 0 and cents_left % qty == 0) else 1
        return (divides, qty, planned[i][0].id)

    guard = 0
    while cents_left > 0 and guard < 10000:
        guard += 1
        order = sorted(range(len(planned)), key=row_sort_key)
        progressed = False
        for idx in order:
            stock, qty, old_avg, new_avg = planned[idx]
            qty_i = int(qty)
            if qty_i <= 0 or qty_i > cents_left:
                continue
            steps = cents_left // qty_i
            if steps <= 0:
                continue
            delta_avg = CENT * steps * sign
            planned[idx][3] = (Decimal(str(new_avg)) + delta_avg).quantize(TWOPLACES)
            cents_left -= steps * qty_i
            progressed = True
            break
        if not progressed:
            break

    leftover = _money(Decimal(cents_left) * CENT * sign)
    if leftover == 0:
        return [tuple(item) for item in planned], Decimal("0")

    # 4dp fallback: put leftover on smallest positive qty row.
    order = sorted(range(len(planned)), key=lambda i: (planned[i][1], planned[i][0].id))
    for idx in order:
        stock, qty, old_avg, new_avg = planned[idx]
        if qty <= 0:
            continue
        delta_avg = (leftover / qty).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
        if delta_avg == 0:
            continue
        planned[idx][3] = (Decimal(str(new_avg)) + delta_avg).quantize(FOURPLACES)
        return [tuple(item) for item in planned], Decimal("0")

    return [tuple(item) for item in planned], leftover


class Command(BaseCommand):
    help = (
        "Set a branch Total Stock Value to exact AFG/USD targets by scaling "
        "average_buy_price on positive-qty stock rows (qty unchanged)."
    )

    def add_arguments(self, parser):
        parser.add_argument("--shop", type=int, required=True, help="Branch shop id")
        parser.add_argument(
            "--afg",
            type=str,
            help="Target AFG total stock value (e.g. 9609183)",
        )
        parser.add_argument(
            "--usd",
            type=str,
            help="Target USD total stock value (e.g. 61464)",
        )
        parser.add_argument("--dry-run", action="store_true")

    def handle(self, *args, **options):
        shop_id = options["shop"]
        dry_run = options["dry_run"]
        targets = {}
        if options.get("afg") is not None:
            targets["AFG"] = _money(options["afg"])
        if options.get("usd") is not None:
            targets["USD"] = _money(options["usd"])
        if not targets:
            self.stderr.write(self.style.ERROR("Provide --afg and/or --usd target totals."))
            return

        shop = Shop.objects.filter(pk=shop_id).first()
        if not shop:
            self.stderr.write(self.style.ERROR(f"Shop {shop_id} not found."))
            return

        before_qty, before_values = _summary(shop_id)
        self.stdout.write(
            self.style.NOTICE(
                f"{shop.name} (id={shop_id}) BEFORE: qty={before_qty} "
                f"value=[{self._fmt(before_values)}]"
            )
        )

        stocks = list(_stocks_for_shop(shop_id))
        by_currency = {"AFG": [], "USD": []}
        for stock in stocks:
            code = normalize_currency_code(stock.currency.title if stock.currency else "")
            if code in by_currency:
                by_currency[code].append(stock)

        changed = 0
        for code, target in targets.items():
            rows = by_currency.get(code) or []
            current = before_values.get(code, Decimal("0.00"))
            if not rows:
                self.stdout.write(
                    self.style.WARNING(f"  {code}: no stock rows; cannot set target {target}")
                )
                continue
            if current < 0:
                self.stdout.write(
                    self.style.WARNING(
                        f"  {code}: current value is {current}; cannot scale to {target}"
                    )
                )
                continue

            # Already exact?
            if current == target:
                self.stdout.write(f"  {code}: already exact {target}")
                continue

            if current == 0:
                self.stdout.write(
                    self.style.WARNING(
                        f"  {code}: current value is 0; cannot scale to {target}"
                    )
                )
                continue

            factor = (target / current).quantize(SIX, rounding=ROUND_HALF_UP)
            # Tiny gap (cents only): do not re-scale/re-quantize to 2dp — that
            # destroys existing 4dp averages. Only apply cent/4dp drift.
            cent_only = abs(target - current) < Decimal("1.00")
            self.stdout.write(
                f"  {code}: {current} -> {target} "
                f"({'cent-fix' if cent_only else f'scale ×{factor}'}) "
                f"across {len(rows)} row(s)"
            )

            planned = []
            scaled_total = Decimal("0")
            for stock in rows:
                qty = Decimal(str(stock.quantity or 0))
                old_avg = Decimal(str(stock.average_buy_price or 0))
                if cent_only or factor == Decimal("1.000000"):
                    new_avg = old_avg
                else:
                    new_avg = (old_avg * factor).quantize(TWOPLACES, rounding=ROUND_HALF_UP)
                planned.append([stock, qty, old_avg, new_avg])
                scaled_total += qty * new_avg

            # Match summary rounding (sum then quantize to 2dp).
            scaled_total = scaled_total.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
            drift = (target - scaled_total).quantize(TWOPLACES, rounding=ROUND_HALF_UP)
            planned_tuples = [(p[0], p[1], p[2], p[3]) for p in planned]
            planned_tuples, leftover = _apply_cent_drift(planned_tuples, drift)

            # Verify planned total; if still off, force 4dp correction on smallest qty.
            planned_total = sum(
                (qty * new_avg for _, qty, _, new_avg in planned_tuples),
                Decimal("0"),
            ).quantize(TWOPLACES, rounding=ROUND_HALF_UP)
            final_gap = (target - planned_total).quantize(TWOPLACES, rounding=ROUND_HALF_UP)
            if final_gap != 0:
                planned_list = [list(p) for p in planned_tuples]
                planned_tuples, leftover = _apply_cent_drift(
                    [tuple(p) for p in planned_list], final_gap
                )
            if leftover != 0:
                self.stdout.write(
                    self.style.WARNING(
                        f"  {code}: could not absorb leftover {leftover}."
                    )
                )

            for stock, qty, old_avg, new_avg in planned_tuples:
                if new_avg == old_avg:
                    continue
                old_value = (qty * old_avg).quantize(TWOPLACES)
                new_value = (qty * new_avg).quantize(TWOPLACES)
                name = (
                    stock.purchase_item.product.name
                    if stock.purchase_item and stock.purchase_item.product
                    else "?"
                )
                self.stdout.write(
                    f"    Stock #{stock.id} {name}: avg {old_avg} -> {new_avg} "
                    f"| value {old_value} -> {new_value}"
                )
                changed += 1
                if dry_run:
                    continue
                with transaction.atomic():
                    locked = Stock.objects.select_for_update().get(pk=stock.id)
                    locked.average_buy_price = new_avg
                    locked.save(update_fields=["average_buy_price", "updated_at"])

            # Post-write micro-fix loop against real DB summary.
            if not dry_run:
                for _ in range(5):
                    _, now_values = _summary(shop_id)
                    now = now_values.get(code, Decimal("0.00"))
                    gap = (target - now).quantize(TWOPLACES, rounding=ROUND_HALF_UP)
                    if gap == 0:
                        break
                    # Adjust smallest qty row by gap/qty at 4dp.
                    row = min(rows, key=lambda s: (int(s.quantity or 0) or 10**9, s.id))
                    qty = Decimal(str(row.quantity or 0))
                    if qty <= 0:
                        break
                    with transaction.atomic():
                        locked = Stock.objects.select_for_update().get(pk=row.id)
                        old_avg = Decimal(str(locked.average_buy_price or 0))
                        delta = (gap / qty).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
                        if delta == 0:
                            break
                        new_avg = (old_avg + delta).quantize(FOURPLACES)
                        locked.average_buy_price = new_avg
                        locked.save(update_fields=["average_buy_price", "updated_at"])
                        changed += 1
                        self.stdout.write(
                            f"    Micro-fix stock #{locked.id}: avg {old_avg} -> {new_avg} "
                            f"(gap {gap})"
                        )
                    # refresh row object avg for next loop
                    row.average_buy_price = new_avg

        if dry_run:
            self.stdout.write(
                self.style.WARNING(
                    f"Dry-run only. Would update {changed} stock row(s). "
                    "Re-run without --dry-run to apply."
                )
            )
            return

        after_qty, after_values = _summary(shop_id)
        self.stdout.write(
            self.style.SUCCESS(
                f"{shop.name} AFTER: qty={after_qty} value=[{self._fmt(after_values)}] "
                f"(updated {changed} row(s))"
            )
        )
        for code, target in targets.items():
            actual = after_values.get(code, Decimal("0.00"))
            if actual != target:
                self.stdout.write(
                    self.style.WARNING(
                        f"  {code} is {actual}, target was {target}."
                    )
                )
            else:
                self.stdout.write(self.style.SUCCESS(f"  {code} exact match: {actual}"))

    @staticmethod
    def _fmt(values):
        return ", ".join(f"{amount} {code}" for code, amount in values.items()) or "0"
