"""Transactional procurement lifecycle interface."""
from django.db import transaction
from django.db.models import Sum
from django.utils import timezone
from rest_framework.exceptions import PermissionDenied, ValidationError

from apps.accounting import services as accounting_services
from apps.finance.models import BudgetItem
from .models import BudgetCommitment, FinancePosting, Invoice, Payment, ProcurementContract, ProcurementEvent, PurchaseOrder


def record_event(document, actor, action, from_status="", to_status="", reason="", metadata=None):
    import re
    document_type = re.sub(r"(?<!^)(?=[A-Z])", "_", document.__class__.__name__).lower()
    return ProcurementEvent.objects.create(
        organization=document.organization,
        document_type=document_type,
        document_id=str(document.pk), action=action, actor=actor,
        from_status=from_status, to_status=to_status, reason=reason,
        metadata=metadata or {},
    )


@transaction.atomic
def issue_purchase_order(purchase_order, actor):
    """Issue one approved, awarded PO and reserve its budget."""
    po = PurchaseOrder.objects.select_for_update().get(pk=purchase_order.pk)
    if po.status != "draft":
        raise ValidationError("Only a draft purchase order can be issued.")
    if po.created_by_id == actor.id:
        raise PermissionDenied("The purchase order creator cannot issue the same order.")
    requisition = po.requisition
    if not requisition or requisition.status != "approved":
        raise ValidationError("A purchase order requires an approved requisition.")
    award = requisition.quotes.filter(is_selected=True).first()
    if not award or award.vendor_id != po.vendor_id:
        raise ValidationError("The purchase order vendor must match the awarded vendor.")
    if not requisition.budget_item_id:
        raise ValidationError("An approved budget line is required before issuing a purchase order.")
    budget = BudgetItem.objects.select_for_update().get(pk=requisition.budget_item_id)
    committed = BudgetCommitment.objects.filter(
        budget_item=budget, status="active",
    ).aggregate(total=Sum("amount"))["total"] or 0
    if committed + po.total > budget.allocated_amount:
        raise ValidationError("Insufficient available budget for this purchase order.")
    BudgetCommitment.objects.create(
        purchase_order=po, budget_item=budget, amount=po.total, currency=po.currency,
    )
    po.status = "sent"
    po.issued_by = actor
    po.issued_at = timezone.now()
    po.save(update_fields=["status", "issued_by", "issued_at", "updated_at"])
    accounting_services.post_encumbrance(
        organization=po.organization, journal_no=f"ENC-{po.order_number}", date=timezone.now().date(), amount=po.total, budget_item=budget,
    )
    record_event(po, actor, "issued", "draft", "sent", metadata={"commitment": str(po.total)})
    return po


@transaction.atomic
def sign_contract(contract, actor):
    contract = ProcurementContract.objects.select_for_update().get(pk=contract.pk)
    if contract.status != "draft":
        raise ValidationError("Only a draft contract can be signed.")
    if contract.created_by_id == actor.id:
        raise PermissionDenied("The contract creator cannot sign the same contract.")
    requisition = contract.requisition
    if not requisition or requisition.status != "approved":
        raise ValidationError("A contract requires an approved requisition.")
    award = requisition.quotes.filter(is_selected=True).first()
    if not award or award.vendor_id != contract.vendor_id:
        raise ValidationError("The contract vendor must match the awarded vendor.")
    if not requisition.budget_item_id:
        raise ValidationError("An approved budget line is required before signing a contract.")
    budget = BudgetItem.objects.select_for_update().get(pk=requisition.budget_item_id)
    committed = BudgetCommitment.objects.filter(budget_item=budget, status="active").aggregate(total=Sum("amount"))["total"] or 0
    if committed + contract.value > budget.allocated_amount:
        raise ValidationError("Insufficient available budget for this contract.")
    BudgetCommitment.objects.create(contract=contract, budget_item=budget, amount=contract.value, currency=contract.currency)
    contract.status = "active"
    contract.signed_by = actor
    contract.signed_at = timezone.now()
    contract.save(update_fields=["status", "signed_by", "signed_at", "updated_at"])
    accounting_services.post_encumbrance(
        organization=contract.organization, journal_no=f"ENC-{contract.contract_number}", date=timezone.now().date(), amount=contract.value, budget_item=budget,
    )
    record_event(contract, actor, "signed", "draft", "active", metadata={"commitment": str(contract.value)})
    return contract


@transaction.atomic
def approve_invoice(invoice, actor):
    invoice = Invoice.objects.select_for_update().get(pk=invoice.pk)
    if invoice.status != "pending":
        raise ValidationError("Only a pending invoice can be approved.")
    if invoice.goods_receipt_id and invoice.goods_receipt.received_by_id == actor.id:
        raise PermissionDenied("The receiver cannot approve the related invoice.")
    detail = invoice.match_detail()
    if detail["status"] != "matched":
        raise ValidationError("Invoice approval requires a valid three-way match.")
    invoice.match_status = detail["status"]
    invoice.status = "approved"
    invoice.approved_by = actor
    invoice.approved_at = timezone.now()
    invoice.save(update_fields=["match_status", "status", "approved_by", "approved_at"])
    FinancePosting.objects.get_or_create(
        organization=invoice.organization, source_type="invoice", source_id=str(invoice.pk).replace("-", ""), posting_type="accrual",
        defaults={"amount": invoice.total, "currency": invoice.purchase_order.currency if invoice.purchase_order_id else "IDR", "payload": {"expense": str(invoice.amount), "tax": str(invoice.tax), "payable": str(invoice.total), "vendor_id": str(invoice.vendor_id)}, "created_by": actor},
    )
    requisition = invoice.purchase_order.requisition if invoice.purchase_order_id else None
    budget_item = requisition.budget_item if requisition else None
    category = budget_item.category if budget_item else None
    expense_account = category.expense_account if category else None
    if not expense_account:
        raise ValidationError("The requisition's budget category has no expense account configured for posting.")
    accounting_services.post_invoice_accrual(
        organization=invoice.organization, invoice=invoice, journal_no=f"ACR-{invoice.invoice_number}", date=timezone.now().date(),
        expense_account=expense_account, budget_item=budget_item,
    )
    record_event(invoice, actor, "approved", "pending", "approved")
    return invoice


@transaction.atomic
def release_payment(payment, actor, comment=""):
    payment = Payment.objects.select_for_update().select_related("invoice").get(pk=payment.pk)
    if payment.status != "pending":
        raise ValidationError("Only a prepared payment can be released.")
    if payment.prepared_by_id == actor.id:
        raise PermissionDenied("The payment preparer cannot release the same payment.")
    if not payment.fund_approved_by_id:
        raise ValidationError("Fund-owner approval is required before payment release.")
    if not (actor.is_superuser or getattr(actor, "organization_role", "staff") == "director" or actor.user_roles.filter(organization=payment.organization, role__slug__in=["fat-manager", "finance-manager"]).exists()):
        raise PermissionDenied("Only the F.A.T Manager can perform final payment release.")
    comment = (comment or "").strip()
    if not comment:
        raise ValidationError({"comment": "A release note is required."})
    invoice = Invoice.objects.select_for_update().get(pk=payment.invoice_id)
    detail = invoice.match_detail()
    if invoice.status != "approved" or detail["status"] != "matched":
        raise ValidationError("Payment release requires an approved, currently matched invoice.")
    paid = invoice.payments.filter(status="completed").exclude(pk=payment.pk).aggregate(total=Sum("amount"))["total"] or 0
    if payment.amount <= 0 or paid + payment.amount > invoice.total:
        raise ValidationError("Payment exceeds the outstanding invoice balance.")
    now = timezone.now()
    payment.status = "completed"
    payment.released_by = actor
    payment.paid_at = now
    payment.save(update_fields=["status", "released_by", "paid_at"])
    FinancePosting.objects.get_or_create(
        organization=payment.organization, source_type="payment", source_id=str(payment.pk).replace("-", ""), posting_type="payment",
        defaults={"amount": payment.amount, "currency": invoice.purchase_order.currency if invoice.purchase_order_id else "IDR", "payload": {"payable": str(payment.amount), "cash": str(payment.amount), "method": payment.method, "reference": payment.reference}, "created_by": actor},
    )
    requisition = invoice.purchase_order.requisition if invoice.purchase_order_id else None
    accounting_services.post_payment(
        organization=payment.organization, payment=payment, journal_no=f"PMT-{str(payment.pk)[:8]}", date=timezone.now().date(),
        budget_item=requisition.budget_item if requisition else None,
    )
    record_event(payment, actor, "released", "pending", "completed", reason=comment, metadata={"amount": str(payment.amount)})
    if paid + payment.amount == invoice.total:
        invoice.status = "paid"
        invoice.paid_at = now
        invoice.save(update_fields=["status", "paid_at"])
        commitment = getattr(invoice.purchase_order, "budget_commitment", None) if invoice.purchase_order_id else None
        if commitment and commitment.status == "active":
            commitment.status = "consumed"
            commitment.save(update_fields=["status"])
            accounting_services.reverse_encumbrance(
                organization=payment.organization, journal_no=f"ENC-REV-{invoice.purchase_order.order_number}", date=timezone.now().date(),
                amount=commitment.amount, budget_item=commitment.budget_item,
            )
    return payment


@transaction.atomic
def approve_payment_funds(payment, actor, comment=""):
    payment = Payment.objects.select_for_update().get(pk=payment.pk)
    if payment.status != "pending":
        raise ValidationError("Only a pending payment can receive fund approval.")
    if payment.prepared_by_id == actor.id:
        raise PermissionDenied("The payment preparer cannot approve its funding.")
    requisition = payment.invoice.purchase_order.requisition if payment.invoice.purchase_order_id else None
    project = requisition.project if requisition and requisition.project_id else None
    if project:
        if not (actor.is_superuser or getattr(actor, "organization_role", "staff") == "director" or project.team_members.filter(user=actor, role__in=["owner", "manager"]).exists()):
            raise PermissionDenied("Project fund approval requires a project owner or manager.")
    elif requisition and requisition.requesting_department_id:
        if not (actor.is_superuser or getattr(actor, "organization_role", "staff") == "director" or actor.department_memberships.filter(department_id=requisition.requesting_department_id, role="manager", is_active=True).exists()):
            raise PermissionDenied("Department fund approval requires the department manager.")
    else:
        raise ValidationError("The payment must be linked to a project or requesting department before fund approval.")
    payment.fund_approved_by = actor
    payment.fund_approved_at = timezone.now()
    payment.fund_approval_comment = (comment or "").strip()
    payment.save(update_fields=["fund_approved_by", "fund_approved_at", "fund_approval_comment"])
    record_event(payment, actor, "funds_approved", "pending", "pending", reason=payment.fund_approval_comment)
    return payment


@transaction.atomic
def cancel_purchase_order(purchase_order, actor, reason):
    po = PurchaseOrder.objects.select_for_update().get(pk=purchase_order.pk)
    reason = (reason or "").strip()
    if not reason:
        raise ValidationError({"reason": "A cancellation reason is required."})
    if po.status not in ("draft", "sent", "acknowledged"):
        raise ValidationError("Only an open purchase order can be cancelled.")
    if po.goods_receipts.filter(status__in=["partial", "received"]).exists() or po.invoices.exclude(status="cancelled").exists():
        raise ValidationError("Cancel related receipts or invoices before cancelling this purchase order.")
    old_status = po.status
    po.status = "cancelled"
    po.save(update_fields=["status", "updated_at"])
    commitment = getattr(po, "budget_commitment", None)
    if commitment and commitment.status == "active":
        commitment.status = "released"
        commitment.released_at = timezone.now()
        commitment.save(update_fields=["status", "released_at"])
        accounting_services.reverse_encumbrance(
            organization=po.organization, journal_no=f"ENC-REV-{po.order_number}", date=timezone.now().date(),
            amount=commitment.amount, budget_item=commitment.budget_item,
        )
    record_event(po, actor, "cancelled", old_status, "cancelled", reason=reason)
    return po


@transaction.atomic
def terminate_contract(contract, actor, reason):
    contract = ProcurementContract.objects.select_for_update().get(pk=contract.pk)
    reason = (reason or "").strip()
    if not reason:
        raise ValidationError({"reason": "A termination reason is required."})
    if contract.status != "active":
        raise ValidationError("Only an active contract can be terminated.")
    contract.status = "terminated"
    contract.save(update_fields=["status", "updated_at"])
    commitment = getattr(contract, "budget_commitment", None)
    if commitment and commitment.status == "active":
        commitment.status = "released"
        commitment.released_at = timezone.now()
        commitment.save(update_fields=["status", "released_at"])
        accounting_services.reverse_encumbrance(
            organization=contract.organization, journal_no=f"ENC-REV-{contract.contract_number}", date=timezone.now().date(),
            amount=commitment.amount, budget_item=commitment.budget_item,
        )
    record_event(contract, actor, "terminated", "active", "terminated", reason=reason)
    return contract
