"""
FEFO (First Expired First Out) allocation service for sales.

Allocates inventory from batches ordered by expiration date (nearest first).
All batch mutations use select_for_update to prevent race conditions.
"""
from decimal import Decimal

from django.db import transaction
from django.db.models import F

from api.models.data.inventory_batch import InventoryBatch
from api.models.data.sale_batch_allocation import SaleBatchAllocation
from api.services.batch_queries import sellable_batch_q


class InsufficientInventoryError(Exception):
    pass


def _d(value):
    return Decimal(str(value or 0))


def _sellable_batches(stock):
    return (
        InventoryBatch.objects.select_for_update()
        .filter(
            product=stock.product,
            condition=stock.condition,
        )
        .filter(sellable_batch_q())
        .order_by(F('expire_date').asc(nulls_last=True), 'created_at')
    )


def _sync_legacy_stock(batch, delta):
    """Add delta (positive or negative) to the matching Stock.piece_amount."""
    from api.models.data.stock import Stock
    Stock.objects.filter(
        product=batch.product,
        condition=batch.condition,
    ).update(piece_amount=F('piece_amount') + delta)


@transaction.atomic
def allocate_inventory_for_sale_item(sale_item, quantity_needed):
    """
    Allocate pieces from FEFO-ordered batches for a sale item.

    Returns list of SaleBatchAllocation objects created.
    Raises InsufficientInventoryError if stock is insufficient (no partial state left).
    """
    quantity_needed = _d(quantity_needed)
    if quantity_needed <= 0:
        return []

    stock = sale_item.stock
    if not stock:
        raise InsufficientInventoryError("Sale item has no associated stock.")

    batches = list(_sellable_batches(stock))

    # Legacy / seed paths may set Stock.piece_amount without creating InventoryBatch.
    # Sale UI reads Stock, but FEFO sells from batches — heal the gap before failing.
    if not batches and _d(getattr(stock, 'piece_amount', 0)) > 0:
        from api.services.batch_inventory import sync_manual_stock_batch

        sync_manual_stock_batch(
            stock,
            pieces=stock.piece_amount,
            expire_date=getattr(stock, 'expire_date', None),
        )
        batches = list(_sellable_batches(stock))

    remaining = quantity_needed
    pending = []  # (batch, take_amount)

    for batch in batches:
        if remaining <= 0:
            break
        take = min(_d(batch.remaining_pieces), remaining)
        if take <= 0:
            continue
        pending.append((batch, take))
        remaining -= take

    if remaining > 0:
        stock_on_hand = _d(getattr(stock, 'piece_amount', 0))
        batch_available = quantity_needed - remaining
        raise InsufficientInventoryError(
            f"Insufficient inventory for {stock.product.name}. "
            f"Needed: {quantity_needed}, available in sellable batches: {batch_available}"
            f" (stock record shows {stock_on_hand} pieces)."
        )

    # All checks passed — commit mutations
    allocations = []
    for batch, take in pending:
        batch.remaining_pieces = _d(batch.remaining_pieces) - take
        if batch.remaining_pieces <= 0:
            batch.status = InventoryBatch.STATUS_EMPTY
        batch.save(update_fields=['remaining_pieces', 'status'])
        _sync_legacy_stock(batch, -take)

        allocation = SaleBatchAllocation.objects.create(
            sale=sale_item.sales,
            sale_item=sale_item,
            inventory_batch=batch,
            quantity=take,
        )
        allocations.append(allocation)

    return allocations


@transaction.atomic
def revert_sale_item_allocations(sale_item):
    """Return all allocated pieces back to their batches and delete allocation records."""
    from django.utils import timezone

    allocations = list(
        SaleBatchAllocation.objects.filter(sale_item=sale_item)
        .select_related('inventory_batch')
    )

    for allocation in allocations:
        batch = allocation.inventory_batch
        batch.remaining_pieces = _d(batch.remaining_pieces) + _d(allocation.quantity)
        if batch.remaining_pieces > 0:
            if batch.expire_date and batch.expire_date < timezone.localdate():
                batch.status = InventoryBatch.STATUS_EXPIRED
            else:
                batch.status = InventoryBatch.STATUS_ACTIVE
        batch.save(update_fields=['remaining_pieces', 'status'])
        _sync_legacy_stock(batch, _d(allocation.quantity))

    SaleBatchAllocation.objects.filter(sale_item=sale_item).delete()


@transaction.atomic
def restore_pieces_to_sale_batches(sale_item, pieces_to_restore):
    """
    Return pieces to the batches this sale item consumed (for partial returns).
    Does NOT delete SaleBatchAllocation rows — sale history stays intact.
    Returns actual pieces restored.
    """
    from django.utils import timezone

    pieces_to_restore = _d(pieces_to_restore)
    if pieces_to_restore <= 0:
        return _d(0)

    allocations = (
        SaleBatchAllocation.objects.filter(sale_item=sale_item)
        .select_related('inventory_batch')
        .order_by('-id')
    )

    remaining = pieces_to_restore
    restored = _d(0)

    for allocation in allocations:
        if remaining <= 0:
            break
        give_back = min(_d(allocation.quantity), remaining)
        if give_back <= 0:
            continue

        batch = allocation.inventory_batch
        batch.remaining_pieces = _d(batch.remaining_pieces) + give_back
        if batch.remaining_pieces > 0:
            if batch.expire_date and batch.expire_date < timezone.localdate():
                batch.status = InventoryBatch.STATUS_EXPIRED
            else:
                batch.status = InventoryBatch.STATUS_ACTIVE
        batch.save(update_fields=['remaining_pieces', 'status'])
        _sync_legacy_stock(batch, give_back)

        remaining -= give_back
        restored += give_back

    # Fallback for legacy sales with no allocation records
    if restored <= 0 and sale_item.stock_id:
        from api.models.data.stock import Stock
        Stock.objects.filter(pk=sale_item.stock_id).update(
            piece_amount=F('piece_amount') + pieces_to_restore
        )
        restored = pieces_to_restore

    return restored


@transaction.atomic
def update_sale_item_allocations(sale_item, old_quantity, new_quantity):
    """Adjust allocations when a sale item quantity changes."""
    old_quantity = _d(old_quantity)
    new_quantity = _d(new_quantity)
    diff = new_quantity - old_quantity

    if diff > 0:
        allocate_inventory_for_sale_item(sale_item, diff)
    elif diff < 0:
        to_release = abs(diff)
        for allocation in SaleBatchAllocation.objects.filter(sale_item=sale_item).order_by('-id'):
            if to_release <= 0:
                break
            can_release = min(_d(allocation.quantity), to_release)
            batch = allocation.inventory_batch
            batch.remaining_pieces = _d(batch.remaining_pieces) + can_release
            if batch.remaining_pieces > 0:
                batch.status = InventoryBatch.STATUS_ACTIVE
            batch.save(update_fields=['remaining_pieces', 'status'])
            _sync_legacy_stock(batch, can_release)

            if allocation.quantity == can_release:
                allocation.delete()
            else:
                allocation.quantity = _d(allocation.quantity) - can_release
                allocation.save(update_fields=['quantity'])
            to_release -= can_release


def get_available_batches_for_product(product_id, warehouse_id=None, condition='good'):
    """Get sellable batches for a product ordered by FEFO."""
    qs = InventoryBatch.objects.filter(
        product_id=product_id,
        condition=condition,
    ).filter(sellable_batch_q())

    if warehouse_id:
        qs = qs.filter(warehouse_id=warehouse_id)
    return qs.order_by(F('expire_date').asc(nulls_last=True), 'created_at')


def consume_pieces_from_sale_batches(sale_item, pieces_to_consume):
    """
    Re-consume pieces from FEFO batches when a return is reduced or deleted.
    Inverse of restore_pieces_to_sale_batches.
    """
    pieces_to_consume = _d(pieces_to_consume)
    if pieces_to_consume <= 0:
        return
    allocate_inventory_for_sale_item(sale_item, pieces_to_consume)
