from decimal import Decimal, ROUND_HALF_UP

from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Sum
from django.utils import timezone

from shop_management.models.data.account_transaction import AccountTransaction, AccountTransactionType
from shop_management.models.data.customer_payment import CustomerPayment
from shop_management.models.data.inventory import StockMovement
from shop_management.models.data.sale import Sale
from shop_management.models.data.sale_return import SaleReturn, SaleReturnItem, SaleReturnItemBatchRestoration
from shop_management.services.inventory import issue_stock_batch, receive_stock
from shop_management.services.automatic_journals import create_system_journal, financial_ledger_code, void_system_journal


def _number(document):
    prefix = f"{document.shop.name[:3].upper()}-SRT-{timezone.localdate().year}-"
    last = SaleReturn.objects.filter(return_number__startswith=prefix).order_by("-return_number").values_list("return_number", flat=True).first()
    sequence = int(last.rsplit("-", 1)[-1]) + 1 if last else 1
    return f"{prefix}{sequence:06d}"


def _paid_amount(sale):
    legacy = (sale.received_amount or Decimal("0")) + sum((row.amount for row in sale.payments.all()), Decimal("0"))
    allocated = sale.customer_payment_allocations.filter(payment__status=CustomerPayment.Status.POSTED).aggregate(total=Sum("allocated_amount"))["total"] or Decimal("0")
    prior_refunds = sale.sale_returns.filter(document_status=SaleReturn.DocumentStatus.POSTED).aggregate(total=Sum("cash_refund_amount"))["total"] or Decimal("0")
    return legacy + allocated - prior_refunds


@transaction.atomic
def post_sale_return(return_id, *, user):
    document = SaleReturn.objects.select_for_update().select_related("sale__warehouse", "sale__currency", "shop", "refund_account__currency").get(pk=return_id)
    sale = document.sale
    if not document.uses_inventory_engine or document.document_status != SaleReturn.DocumentStatus.DRAFT:
        raise ValidationError("Only draft new-engine sale returns can be posted.")
    if sale.document_status != Sale.DocumentStatus.POSTED:
        raise ValidationError({"sale": "The original sale must be posted."})
    if document.shop_id != sale.shop_id or not sale.warehouse_id:
        raise ValidationError({"shop": "Return branch must match the original sale."})
    items = list(document.return_items.select_related("sale_item__product", "sale_item__warehouse"))
    if not items:
        raise ValidationError({"return_items": "At least one return item is required."})

    ratio = sale.calculated_total / sale.calculated_subtotal if sale.calculated_subtotal else Decimal("0")
    total_refund = Decimal("0")
    for row in items:
        item = row.sale_item
        if not item or item.sale_id != sale.id:
            raise ValidationError({"return_items": "Every item must belong to the original sale."})
        returned = SaleReturnItem.objects.filter(
            sale_item=item, sale_return__document_status=SaleReturn.DocumentStatus.POSTED
        ).exclude(sale_return=document).aggregate(total=Sum("return_quantity"))["total"] or 0
        if int(returned) + row.return_quantity > item.quantity:
            raise ValidationError({"return_items": f"Return quantity exceeds the remaining quantity for {item.product}."})

        remaining = Decimal(str(row.return_quantity))
        for allocation in item.batch_allocations.select_related("batch").order_by("id"):
            restored = SaleReturnItemBatchRestoration.objects.filter(
                batch=allocation.batch,
                return_item__sale_item=item,
                return_item__sale_return__document_status=SaleReturn.DocumentStatus.POSTED,
            ).aggregate(total=Sum("quantity"))["total"] or Decimal("0")
            available = allocation.quantity - restored
            quantity = min(available, remaining)
            if quantity <= 0:
                continue
            movement = StockMovement.objects.filter(
                movement_type=StockMovement.MovementType.SALE, reference_type="Sale",
                reference_id=str(sale.pk), product=item.product, batch=allocation.batch,
            ).first()
            receive_stock(
                warehouse=item.warehouse or sale.warehouse, product=item.product, batch=allocation.batch,
                quantity=quantity, unit_cost=allocation.unit_cost,
                currency=movement.currency if movement else sale.currency,
                movement_type=StockMovement.MovementType.SALE_RETURN,
                reference_type="SaleReturn", reference_id=document.pk,
                remarks=document.reason or "", created_by=user,
            )
            SaleReturnItemBatchRestoration.objects.create(
                return_item=row, batch=allocation.batch, quantity=quantity,
                unit_cost=allocation.unit_cost, created_by=user,
            )
            remaining -= quantity
            if remaining <= 0:
                break
        if remaining > 0:
            raise ValidationError({"return_items": f"Original batch allocation is insufficient for {item.product}."})
        row.refund_amount = (Decimal(str(row.return_quantity)) * item.sale_price * ratio).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        row.save(update_fields=["refund_amount", "updated_at"])
        total_refund += row.refund_amount

    if document.cash_refund_amount < 0 or document.cash_refund_amount > total_refund:
        raise ValidationError({"cash_refund_amount": "Cash refund must be between zero and the return total."})
    if document.cash_refund_amount > _paid_amount(sale):
        raise ValidationError({"cash_refund_amount": "Cash refund exceeds the amount paid by the customer."})
    if document.cash_refund_amount and not document.refund_account_id:
        raise ValidationError({"refund_account": "A refund account is required for a cash refund."})
    if document.refund_account_id:
        if document.refund_account.shop_id != document.shop_id or document.refund_account.currency_id != sale.currency_id:
            raise ValidationError({"refund_account": "Refund account must belong to the branch and use the sale currency."})
    if document.cash_refund_amount:
        AccountTransaction.objects.create(
            account=document.refund_account, account_currency=document.refund_account.currency,
            transaction_currency=sale.currency, transaction_type=AccountTransactionType.DEBIT,
            amount=document.cash_refund_amount, exchange_rate=sale.exchange_rate_used,
            converted_amount=AccountTransaction.compute_converted_amount(document.cash_refund_amount, sale.exchange_rate_used),
            transaction_date=timezone.localdate(), description=f"Sale return refund #{document.pk}",
        )
    document.total_refound_amount = total_refund
    document.return_number = document.return_number or _number(document)
    document.document_status = SaleReturn.DocumentStatus.POSTED
    document.posted_at = timezone.now()
    document.posted_by = user
    document.save(update_fields=["total_refound_amount", "return_number", "document_status", "posted_at", "posted_by", "updated_at"])
    restored_cogs = sum((row.quantity * row.unit_cost for row in SaleReturnItemBatchRestoration.objects.filter(return_item__sale_return=document)), Decimal("0"))
    receivable_credit = total_refund - document.cash_refund_amount
    journal_lines = [
        {"code": "4100", "side": "debit", "amount": total_refund, "customer": sale.customer},
        {"code": "1400", "side": "credit", "amount": receivable_credit, "customer": sale.customer},
        {"code": "1500", "side": "debit", "amount": restored_cogs},
        {"code": "5100", "side": "credit", "amount": restored_cogs},
    ]
    if document.cash_refund_amount:
        journal_lines.append({"code": financial_ledger_code(document.refund_account), "side": "credit", "amount": document.cash_refund_amount, "financial_account": document.refund_account, "customer": sale.customer})
    create_system_journal(shop=document.shop, entry_date=document.return_date.date(), source_type="SALE_RETURN", source_id=document.pk,
        description=f"Sale return {document.return_number}", currency=sale.currency, exchange_rate=sale.exchange_rate_used, user=user, lines=journal_lines)
    return document


@transaction.atomic
def void_sale_return(return_id, *, user, reason):
    document = SaleReturn.objects.select_for_update().select_related("sale__warehouse", "sale__currency", "refund_account__currency").get(pk=return_id)
    if not document.uses_inventory_engine or document.document_status != SaleReturn.DocumentStatus.POSTED:
        raise ValidationError("Only posted new-engine sale returns can be cancelled.")
    if not str(reason or "").strip():
        raise ValidationError({"void_reason": "A cancellation reason is required."})
    for restoration in SaleReturnItemBatchRestoration.objects.filter(return_item__sale_return=document).select_related("return_item__sale_item__product", "return_item__sale_item__warehouse", "batch"):
        issue_stock_batch(
            warehouse=restoration.return_item.sale_item.warehouse or document.sale.warehouse,
            product=restoration.return_item.sale_item.product,
            batch=restoration.batch, quantity=restoration.quantity,
            movement_type=StockMovement.MovementType.VOID_REVERSAL,
            reference_type="SaleReturnVoid", reference_id=document.pk,
            remarks=str(reason).strip(), created_by=user,
        )
    if document.cash_refund_amount:
        AccountTransaction.objects.create(
            account=document.refund_account, account_currency=document.refund_account.currency,
            transaction_currency=document.sale.currency, transaction_type=AccountTransactionType.CREDIT,
            amount=document.cash_refund_amount, exchange_rate=document.sale.exchange_rate_used,
            converted_amount=AccountTransaction.compute_converted_amount(document.cash_refund_amount, document.sale.exchange_rate_used),
            transaction_date=timezone.localdate(), description=f"Sale return refund reversal #{document.pk} | {str(reason).strip()}",
        )
    document.document_status = SaleReturn.DocumentStatus.VOID
    document.voided_at = timezone.now()
    document.voided_by = user
    document.void_reason = str(reason).strip()
    document.save(update_fields=["document_status", "voided_at", "voided_by", "void_reason", "updated_at"])
    void_system_journal(shop=document.shop, source_type="SALE_RETURN", source_id=document.pk, user=user, reason=str(reason).strip())
    return document
