from decimal import Decimal, ROUND_HALF_UP

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.sale import Sale, SaleItemBatchAllocation
from shop_management.models.data.customer_payment import CustomerPayment
from shop_management.services.inventory import issue_stock_batch, issue_stock_fefo, receive_stock
from shop_management.services.automatic_journals import create_system_journal, void_system_journal
from shop_management.services.customer_credits import apply_customer_credit_to_sale
from shop_management.services.customer_payments import void_customer_payment
from shop_management.services.sale_returns import void_sale_return


def _sale_number(sale):
    prefix = f"{sale.shop.name[:3].upper()}-SAL-{sale.invoice_date.year}-"
    last = Sale.objects.filter(shop_id=sale.shop_id, sale_number__startswith=prefix).order_by("-sale_number").values_list("sale_number", flat=True).first()
    sequence = int(last.rsplit("-", 1)[-1]) + 1 if last else 1
    return f"{prefix}{sequence:06d}"


def _calculate_totals(sale, items):
    subtotal = sum((Decimal(str(item.quantity)) * item.sale_price for item in items), Decimal("0"))
    value = Decimal(str(sale.discount_value or 0))
    if value < 0:
        raise ValidationError({"discount_value": "Discount cannot be negative."})
    if sale.discount_type == Sale.DiscountType.PERCENTAGE:
        if value > 100:
            raise ValidationError({"discount_value": "Percentage discount cannot exceed 100."})
        discount = subtotal * value / Decimal("100")
    else:
        discount = value
    if discount > subtotal:
        raise ValidationError({"discount_value": "Discount cannot exceed subtotal."})
    return subtotal.quantize(Decimal("0.01")), discount.quantize(Decimal("0.01")), (subtotal - discount).quantize(Decimal("0.01"))


@transaction.atomic
def post_sale(sale_id, *, user):
    sale = Sale.objects.select_for_update().select_related("shop", "warehouse", "currency", "customer").get(pk=sale_id)
    if not sale.uses_inventory_engine or sale.document_status != Sale.DocumentStatus.DRAFT:
        raise ValidationError("Only draft new-engine sales can be posted.")
    if StockMovement.objects.filter(
        reference_type="Sale",
        reference_id=str(sale.pk),
        movement_type=StockMovement.MovementType.SALE,
    ).exists():
        raise ValidationError(
            "Stock was already issued for this draft sale. Posting was stopped "
            "to prevent a duplicate or partial issue; run the inventory ledger "
            "audit and repair the document first."
        )
    errors = {}
    if not sale.warehouse_id or sale.warehouse.shop_id != sale.shop_id:
        errors["warehouse"] = "Warehouse must belong to the sale branch."
    if not sale.currency_id or sale.currency.shop_id != sale.shop_id:
        errors["currency"] = "Currency must belong to the sale branch."
    if sale.exchange_rate_used <= 0:
        errors["exchange_rate_used"] = "Exchange rate must be greater than zero."
    if sale.sales_type == Sale.SalesType.CREDIT and not sale.customer_id:
        errors["customer"] = "Customer is required for a credit sale."
    items = list(sale.sale_items.select_related("product", "unit", "warehouse"))
    if not items:
        errors["items"] = "At least one sale item is required."
    for index, item in enumerate(items):
        item_warehouse = item.warehouse or sale.warehouse
        if not item.product_id or item.product.shop_id != sale.shop_id:
            errors[f"items[{index}]"] = "Product must belong to the sale branch."
        elif not item.product.default_unit_id:
            errors[f"items[{index}]"] = (
                f'Product "{item.product.name}" has no default unit. '
                "Edit the product and select a unit before selling it."
            )
        elif item.unit_id != item.product.default_unit_id:
            errors[f"items[{index}]"] = "Sale unit must match the product's default unit."
        elif item.quantity <= 0 or item.sale_price < 0:
            errors[f"items[{index}]"] = "Quantity must be positive and price nonnegative."
        elif not item_warehouse or item_warehouse.shop_id != sale.shop_id or not item_warehouse.is_active:
            errors[f"items[{index}].warehouse"] = "Warehouse must be active and belong to the sale branch."
    if errors:
        raise ValidationError(errors)
    subtotal, discount, total = _calculate_totals(sale, items)
    total_cogs = Decimal("0")
    for item in items:
        item_warehouse = item.warehouse or sale.warehouse
        requested = Decimal(str(item.quantity))
        manual = list(item.batch_allocations.select_related("batch"))
        if manual:
            if sum((row.quantity for row in manual), Decimal("0")) != requested:
                raise ValidationError({"items": f"Manual batch quantities must equal the sale quantity for {item.product}."})
            allocations = []
            for row in manual:
                balance, movement = issue_stock_batch(
                    warehouse=item_warehouse, product=item.product, batch=row.batch,
                    quantity=row.quantity, movement_type=StockMovement.MovementType.SALE,
                    reference_type="Sale", reference_id=sale.pk, remarks=sale.notes or "", created_by=user,
                )
                row.unit_cost = movement.unit_cost
                row.save(update_fields=["unit_cost", "updated_at"])
                allocations.append(row)
        else:
            issued, movements = issue_stock_fefo(
                warehouse=item_warehouse, product=item.product, quantity=requested,
                movement_type=StockMovement.MovementType.SALE, reference_type="Sale",
                reference_id=sale.pk, remarks=sale.notes or "", created_by=user,
            )
            allocations = []
            for allocation in issued:
                allocations.append(SaleItemBatchAllocation.objects.create(
                    sale_item=item, batch=allocation["batch"], quantity=allocation["quantity"],
                    unit_cost=allocation["unit_cost"], created_by=user,
                ))
        item_cogs = sum((row.quantity * row.unit_cost for row in allocations), Decimal("0"))
        item.cogs_total = item_cogs.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        item.cogs_unit_cost = (item_cogs / requested).quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP)
        item.buy_price = item.cogs_unit_cost
        item.save(update_fields=["cogs_total", "cogs_unit_cost", "buy_price", "updated_at"])
        total_cogs += item_cogs
    sale.sale_number = sale.sale_number or _sale_number(sale)
    sale.calculated_subtotal, sale.discount_amount, sale.calculated_total = subtotal, discount, total
    sale.amount = discount
    sale.document_status = Sale.DocumentStatus.POSTED
    sale.posted_at = timezone.now()
    sale.posted_by = user
    sale.save(update_fields=["sale_number", "calculated_subtotal", "discount_amount", "calculated_total", "amount", "document_status", "posted_at", "posted_by", "updated_at"])
    apply_customer_credit_to_sale(sale.pk, user=user)
    create_system_journal(shop=sale.shop, entry_date=sale.invoice_date, source_type="SALE", source_id=sale.pk,
        description=f"Sale {sale.sale_number}", currency=sale.currency, exchange_rate=sale.exchange_rate_used, user=user,
        lines=[{"code": "1400", "side": "debit", "amount": total, "customer": sale.customer}, {"code": "4100", "side": "credit", "amount": total, "customer": sale.customer}, {"code": "5100", "side": "debit", "amount": total_cogs}, {"code": "1500", "side": "credit", "amount": total_cogs}])
    return sale


@transaction.atomic
def void_sale(sale_id, *, user, reason, preserve_payments=False):
    sale = Sale.objects.select_for_update().select_related("warehouse").get(pk=sale_id)
    if not sale.uses_inventory_engine or sale.document_status != Sale.DocumentStatus.POSTED:
        raise ValidationError("Only posted new-engine sales can be cancelled.")
    if not str(reason or "").strip():
        raise ValidationError({"void_reason": "A cancellation reason is required."})
    reason = str(reason).strip()

    # A posted return already restored part of the sold stock. Reverse those
    # returns first, then restore the complete original sale exactly once.
    for sale_return in sale.sale_returns.select_for_update().filter(
        uses_inventory_engine=True,
        document_status="POSTED",
    ).order_by("id"):
        void_sale_return(sale_return.pk, user=user, reason=reason)

    # Payments created as part of completing this invoice are reversed with it.
    # Independently recorded receipts stay posted, but their allocation to the
    # cancelled invoice is released so the money becomes customer credit.
    for allocation in sale.customer_payment_allocations.select_for_update().select_related("payment"):
        payment = allocation.payment
        automatic_reference = payment.reference in (
            f"CASH-{sale.sale_number}",
            f"SALE-{sale.sale_number}",
        )
        if automatic_reference and not preserve_payments and payment.status == CustomerPayment.Status.POSTED:
            void_customer_payment(payment.pk, user=user, reason=reason)
        else:
            allocation.delete()
    for row in SaleItemBatchAllocation.objects.filter(sale_item__sale=sale).select_related("sale_item__product", "sale_item__warehouse", "batch"):
        movement = StockMovement.objects.filter(
            movement_type=StockMovement.MovementType.SALE, reference_type="Sale",
            reference_id=str(sale.pk), batch=row.batch, product=row.sale_item.product,
        ).first()
        currency = movement.currency if movement else sale.currency
        receive_stock(
            warehouse=row.sale_item.warehouse or sale.warehouse, product=row.sale_item.product, batch=row.batch,
            quantity=row.quantity, unit_cost=row.unit_cost, currency=currency,
            movement_type=StockMovement.MovementType.VOID_REVERSAL,
            reference_type="SaleVoid", reference_id=sale.pk,
            remarks=reason, created_by=user,
        )
    sale.document_status = Sale.DocumentStatus.VOID
    sale.voided_at = timezone.now()
    sale.voided_by = user
    sale.void_reason = reason
    sale.save(update_fields=["document_status", "voided_at", "voided_by", "void_reason", "updated_at"])
    void_system_journal(shop=sale.shop, source_type="SALE", source_id=sale.pk, user=user, reason=reason)
    return sale
