from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone

from shop_management.models.data.inventory import StockMovement
from shop_management.models.data.stock_adjustment import StockAdjustment, StockAdjustmentItem
from shop_management.services.inventory import issue_stock_batch, receive_stock


def _number_for(adjustment):
    year = adjustment.adjustment_date.year
    document_code = "OPN" if adjustment.adjustment_kind == StockAdjustment.Kind.OPENING else "ADJ"
    prefix = f"{document_code}-{year}-"
    last = (
        StockAdjustment.objects.filter(adjustment_number__startswith=prefix)
        .order_by("-adjustment_number").values_list("adjustment_number", flat=True).first()
    )
    sequence = int(last.rsplit("-", 1)[-1]) + 1 if last else 1
    return f"{prefix}{sequence:06d}"


def _validate_item(adjustment, item):
    errors = {}
    if item.product.shop_id != adjustment.warehouse.shop_id:
        errors["product"] = "Product and adjustment warehouse must belong to the same branch."
    if item.batch.product_id != item.product_id:
        errors["batch"] = "Batch must belong to the selected product."
    if item.currency.shop_id != item.product.shop_id:
        errors["currency"] = "Currency must belong to the product branch."
    if item.direction == StockAdjustmentItem.Direction.IN and item.unit_cost is None:
        errors["unit_cost"] = "Unit cost is required for incoming adjustments."
    if errors:
        raise ValidationError(errors)


@transaction.atomic
def post_stock_adjustment(adjustment_id, *, user):
    adjustment = StockAdjustment.objects.select_for_update().select_related("warehouse").get(pk=adjustment_id)
    if adjustment.status != StockAdjustment.Status.DRAFT:
        raise ValidationError("Only draft adjustments can be posted.")
    items = list(adjustment.items.select_related("product", "batch", "currency"))
    if not items:
        raise ValidationError("At least one adjustment item is required.")
    if not adjustment.adjustment_number:
        adjustment.adjustment_number = _number_for(adjustment)
    for item in items:
        _validate_item(adjustment, item)
        common = dict(
            warehouse=adjustment.warehouse, product=item.product, batch=item.batch,
            quantity=item.quantity, reference_type="StockAdjustment",
            reference_id=adjustment.pk, remarks=item.remarks or adjustment.reason,
            created_by=user,
        )
        if item.direction == StockAdjustmentItem.Direction.IN:
            receive_stock(
                **common, unit_cost=item.unit_cost, currency=item.currency,
                movement_type=(
                    StockMovement.MovementType.OPENING
                    if adjustment.adjustment_kind == StockAdjustment.Kind.OPENING
                    else StockMovement.MovementType.ADJUSTMENT_IN
                ),
            )
        else:
            issue_stock_batch(
                **common, movement_type=StockMovement.MovementType.ADJUSTMENT_OUT,
            )
    adjustment.status = StockAdjustment.Status.POSTED
    adjustment.posted_at = timezone.now()
    adjustment.posted_by = user
    adjustment.save(update_fields=["adjustment_number", "status", "posted_at", "posted_by", "updated_at"])
    return adjustment


@transaction.atomic
def void_stock_adjustment(adjustment_id, *, user, reason):
    adjustment = StockAdjustment.objects.select_for_update().select_related("warehouse").get(pk=adjustment_id)
    if adjustment.status != StockAdjustment.Status.POSTED:
        raise ValidationError("Only posted adjustments can be cancelled.")
    if not str(reason or "").strip():
        raise ValidationError({"void_reason": "A cancellation reason is required."})
    movements = list(StockMovement.objects.filter(
        reference_type="StockAdjustment", reference_id=str(adjustment.pk),
        movement_type__in=[
            StockMovement.MovementType.OPENING,
            StockMovement.MovementType.ADJUSTMENT_IN,
            StockMovement.MovementType.ADJUSTMENT_OUT,
        ],
    ).select_related("warehouse", "product", "batch", "currency"))
    for movement in movements:
        common = dict(
            warehouse=movement.warehouse, product=movement.product, batch=movement.batch,
            reference_type="StockAdjustmentVoid", reference_id=adjustment.pk,
            remarks=str(reason).strip(), created_by=user,
        )
        if movement.quantity_in > 0:
            issue_stock_batch(
                **common, quantity=movement.quantity_in,
                movement_type=StockMovement.MovementType.VOID_REVERSAL,
            )
        else:
            receive_stock(
                **common, quantity=movement.quantity_out, unit_cost=movement.unit_cost,
                currency=movement.currency,
                movement_type=StockMovement.MovementType.VOID_REVERSAL,
            )
    adjustment.status = StockAdjustment.Status.VOID
    adjustment.voided_at = timezone.now()
    adjustment.voided_by = user
    adjustment.void_reason = str(reason).strip()
    adjustment.save(update_fields=["status", "voided_at", "voided_by", "void_reason", "updated_at"])
    return adjustment
