from datetime import timedelta
from decimal import Decimal

from django.db.models import Sum
from django.utils import timezone

from account.permissions import accessible_shops
from shop_management.models.data.customer import Customer
from shop_management.models.data.inventory import InventoryBatch, StockBalance
from shop_management.models.data.notification import OperationalAlert
from shop_management.models.data.organization import CompanySetting
from shop_management.models.data.product import Product
from shop_management.models.data.supplier import Supplier


def _money_text(amounts):
    return ", ".join(f"{amount:,.2f} {currency}" for currency, amount in sorted(amounts.items()))


def _upsert(user, shop, alert_type, target, *, severity, title, message, path, extra=None):
    fingerprint = f"{user.pk}:{alert_type}:{shop.pk}:{target._meta.label}:{target.pk}"
    alert = OperationalAlert.objects.filter(fingerprint=fingerprint).first()
    values = dict(
        recipient=user, shop=shop, alert_type=alert_type, severity=severity,
        title=title, message=message, target_model=target._meta.label,
        target_object_id=str(target.pk), target_path=path, extra=extra or {},
        is_active=True, resolved_at=None, last_detected_at=timezone.now(),
    )
    if alert:
        was_resolved = not alert.is_active
        for field, value in values.items():
            setattr(alert, field, value)
        if was_resolved:
            alert.is_read = False
        alert.save()
        return alert
    return OperationalAlert.objects.create(fingerprint=fingerprint, **values)


def sync_operational_alerts(user, request=None):
    """Refresh active alerts for one user without creating duplicates."""
    shops = list(accessible_shops(user, request))
    shop_ids = [shop.pk for shop in shops]
    now = timezone.now()
    seen = set()
    today = timezone.localdate()

    for shop in shops:
        warning_days = CompanySetting.objects.filter(shop=shop).values_list(
            "expiry_warning_days", flat=True
        ).first() or 60
        warning_date = today + timedelta(days=warning_days)
        for product in Product.objects.filter(shop=shop, is_active=True, minimum_stock_level__gt=0):
            quantity = StockBalance.objects.filter(
                warehouse__shop=shop, product=product
            ).aggregate(total=Sum("quantity"))["total"] or Decimal("0")
            if quantity <= product.minimum_stock_level:
                seen.add(_upsert(
                    user, shop, OperationalAlert.Type.LOW_STOCK, product,
                    severity=OperationalAlert.Severity.WARNING,
                    title=f"Low stock: {product.name}",
                    message=f"{quantity} available; minimum level is {product.minimum_stock_level}.",
                    path=f"/shop/stock/?product={product.pk}",
                    extra={"quantity": str(quantity), "minimum": str(product.minimum_stock_level)},
                ).fingerprint)

        batches = InventoryBatch.objects.filter(
            product__shop=shop, expiry_date__isnull=False,
            stock_balances__quantity__gt=0,
        ).distinct()
        for batch in batches:
            quantity = batch.stock_balances.filter(warehouse__shop=shop).aggregate(total=Sum("quantity"))["total"] or Decimal("0")
            if quantity <= 0:
                continue
            if batch.expiry_date < today:
                kind, severity, title = OperationalAlert.Type.EXPIRED_STOCK, OperationalAlert.Severity.CRITICAL, f"Expired: {batch.product.name}"
            elif batch.expiry_date <= warning_date:
                kind, severity, title = OperationalAlert.Type.NEAR_EXPIRY, OperationalAlert.Severity.WARNING, f"Near expiry: {batch.product.name}"
            else:
                continue
            seen.add(_upsert(
                user, shop, kind, batch, severity=severity, title=title,
                message=f"Batch {batch.batch_number} has {quantity} units and expires {batch.expiry_date}.",
                path=f"/shop/stock/?batch={batch.pk}",
                extra={"batch": batch.batch_number, "quantity": str(quantity), "expiry_date": batch.expiry_date.isoformat()},
            ).fingerprint)

        for customer in Customer.objects.filter(shop=shop, is_general_customer=False):
            amounts = customer.get_total_owed_by_currency()
            if amounts:
                seen.add(_upsert(
                    user, shop, OperationalAlert.Type.CUSTOMER_DUE, customer,
                    severity=OperationalAlert.Severity.INFO, title=f"Customer due: {customer.name}",
                    message=f"Outstanding balance: {_money_text(amounts)}.",
                    path=f"/shop/customers/?customer={customer.pk}", extra={"amounts": amounts},
                ).fingerprint)

        for supplier in Supplier.objects.filter(shop=shop, is_deleted=False):
            amounts = supplier.get_total_owed_by_currency()
            if amounts:
                seen.add(_upsert(
                    user, shop, OperationalAlert.Type.SUPPLIER_DUE, supplier,
                    severity=OperationalAlert.Severity.INFO, title=f"Supplier due: {supplier.name}",
                    message=f"Outstanding balance: {_money_text(amounts)}.",
                    path=f"/shop/suppliers/?supplier={supplier.pk}", extra={"amounts": amounts},
                ).fingerprint)

    stale = OperationalAlert.objects.filter(recipient=user, shop_id__in=shop_ids, is_active=True)
    if seen:
        stale = stale.exclude(fingerprint__in=seen)
    stale.update(is_active=False, resolved_at=now)

    return OperationalAlert.objects.filter(recipient=user, shop_id__in=shop_ids, is_active=True)
