from decimal import Decimal, InvalidOperation, ROUND_HALF_UP

from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Case, IntegerField, Q, Value, When
from django.utils import timezone

from shop_management.models.data.inventory import StockBalance, StockMovement


QUANTITY_PLACES = Decimal("0.0001")
COST_PLACES = Decimal("0.000001")


def _decimal(value, field_name, *, allow_zero=False):
    try:
        result = Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        raise ValidationError({field_name: "A valid decimal value is required."})
    if result < 0 or (result == 0 and not allow_zero):
        comparison = "nonnegative" if allow_zero else "greater than zero"
        raise ValidationError({field_name: f"Value must be {comparison}."})
    return result


def calculate_weighted_average(current_qty, current_average, incoming_qty, incoming_cost):
    current_qty = _decimal(current_qty, "current_qty", allow_zero=True)
    current_average = _decimal(current_average, "current_average", allow_zero=True)
    incoming_qty = _decimal(incoming_qty, "incoming_qty")
    incoming_cost = _decimal(incoming_cost, "incoming_cost", allow_zero=True)
    total_qty = current_qty + incoming_qty
    result = ((current_qty * current_average) + (incoming_qty * incoming_cost)) / total_qty
    return result.quantize(COST_PLACES, rounding=ROUND_HALF_UP)


def _validate_dimensions(warehouse, product, batch, currency):
    errors = {}
    if batch.product_id != product.id:
        errors["batch"] = "Batch does not belong to this product."
    if warehouse.shop_id != product.shop_id:
        errors["warehouse"] = "Warehouse does not belong to the product branch."
    if currency.shop_id != product.shop_id:
        errors["currency"] = "Currency does not belong to the product branch."
    if product.requires_expiry and not batch.expiry_date:
        errors["batch"] = "This product requires a batch expiry date."
    if errors:
        raise ValidationError(errors)


@transaction.atomic
def receive_stock(*, warehouse, product, batch, quantity, unit_cost, currency,
                  movement_type=StockMovement.MovementType.PURCHASE,
                  reference_type="", reference_id="", remarks="", created_by=None,
                  movement_date=None):
    quantity = _decimal(quantity, "quantity").quantize(QUANTITY_PLACES)
    unit_cost = _decimal(unit_cost, "unit_cost", allow_zero=True).quantize(COST_PLACES)
    _validate_dimensions(warehouse, product, batch, currency)
    balance, _ = StockBalance.objects.select_for_update().get_or_create(
        warehouse=warehouse,
        product=product,
        batch=batch,
        defaults={"cost_currency": currency, "created_by": created_by},
    )
    if balance.quantity > 0 and balance.cost_currency_id != currency.id:
        raise ValidationError({"currency": "Existing balance uses a different cost currency."})
    balance.average_cost = calculate_weighted_average(
        balance.quantity, balance.average_cost, quantity, unit_cost
    )
    balance.quantity = (balance.quantity + quantity).quantize(QUANTITY_PLACES)
    balance.cost_currency = currency
    balance.save(update_fields=["quantity", "average_cost", "cost_currency", "updated_at"])
    movement = StockMovement.objects.create(
        warehouse=warehouse, product=product, batch=batch,
        movement_type=movement_type, quantity_in=quantity, quantity_out=0,
        unit_cost=unit_cost, currency=currency,
        movement_date=movement_date or timezone.now(),
        reference_type=reference_type, reference_id=str(reference_id or ""),
        remarks=remarks, created_by=created_by,
    )
    return balance, movement


def _fefo_balances(warehouse, product, today):
    return (
        StockBalance.objects.select_for_update()
        .filter(warehouse=warehouse, product=product, quantity__gt=0)
        .filter(Q(batch__expiry_date__isnull=True) | Q(batch__expiry_date__gte=today))
        .filter(batch__is_blocked=False)
        .select_related("batch", "cost_currency")
        .annotate(
            expiry_priority=Case(
                When(batch__expiry_date__isnull=False, then=Value(0)),
                default=Value(1), output_field=IntegerField(),
            )
        )
        .order_by("expiry_priority", "batch__expiry_date", "id")
    )


@transaction.atomic
def allocate_fefo(warehouse, product, quantity, *, today=None):
    requested = _decimal(quantity, "quantity").quantize(QUANTITY_PLACES)
    remaining = requested
    allocations = []
    for balance in _fefo_balances(warehouse, product, today or timezone.localdate()):
        allocated = min(balance.quantity, remaining)
        allocations.append({"balance": balance, "batch": balance.batch, "quantity": allocated,
                            "unit_cost": balance.average_cost, "currency": balance.cost_currency})
        remaining -= allocated
        if remaining <= 0:
            break
    if remaining > 0:
        available = requested - remaining
        raise ValidationError(
            {"quantity": f"Insufficient valid stock. Requested {requested}, available {available}."}
        )
    return allocations


@transaction.atomic
def issue_stock_fefo(*, warehouse, product, quantity,
                     movement_type=StockMovement.MovementType.SALE,
                     reference_type="", reference_id="", remarks="", created_by=None,
                     movement_date=None):
    allocations = allocate_fefo(warehouse, product, quantity)
    movements = []
    for allocation in allocations:
        balance = allocation["balance"]
        issued = allocation["quantity"]
        if balance.quantity < issued:
            raise ValidationError({"quantity": "Stock changed during allocation; retry."})
        balance.quantity = (balance.quantity - issued).quantize(QUANTITY_PLACES)
        balance.save(update_fields=["quantity", "updated_at"])
        movements.append(StockMovement.objects.create(
            warehouse=warehouse, product=product, batch=balance.batch,
            movement_type=movement_type, quantity_in=0, quantity_out=issued,
            unit_cost=allocation["unit_cost"], currency=allocation["currency"],
            movement_date=movement_date or timezone.now(), reference_type=reference_type,
            reference_id=str(reference_id or ""), remarks=remarks, created_by=created_by,
        ))
    return allocations, movements


@transaction.atomic
def issue_stock_batch(*, warehouse, product, batch, quantity,
                      movement_type=StockMovement.MovementType.ADJUSTMENT_OUT,
                      reference_type="", reference_id="", remarks="", created_by=None,
                      movement_date=None):
    quantity = _decimal(quantity, "quantity").quantize(QUANTITY_PLACES)
    balance = StockBalance.objects.select_for_update().filter(
        warehouse=warehouse, product=product, batch=batch
    ).select_related("cost_currency").first()
    if not balance or balance.quantity < quantity:
        available = balance.quantity if balance else Decimal("0")
        raise ValidationError(
            {"quantity": f"Insufficient batch stock. Requested {quantity}, available {available}."}
        )
    balance.quantity = (balance.quantity - quantity).quantize(QUANTITY_PLACES)
    balance.save(update_fields=["quantity", "updated_at"])
    movement = StockMovement.objects.create(
        warehouse=warehouse, product=product, batch=batch,
        movement_type=movement_type, quantity_in=0, quantity_out=quantity,
        unit_cost=balance.average_cost, currency=balance.cost_currency,
        movement_date=movement_date or timezone.now(), reference_type=reference_type,
        reference_id=str(reference_id or ""), remarks=remarks, created_by=created_by,
    )
    return balance, movement
