from django.db import models
from django.db.models import Sum
from django.conf import settings
from django.utils import timezone
from decimal import Decimal
import secrets
import time
from apps.core.uploads import tenant_upload_path


def uuid7() -> str:
    """Generate a UUID v7 (time-ordered UUID)."""
    nanoseconds = time.time_ns()
    uuid_int = (nanoseconds << 16) | secrets.randbits(48)
    return f"{uuid_int:032x}"


_ROMAN_MONTHS = {
    1: "I", 2: "II", 3: "III", 4: "IV", 5: "V", 6: "VI",
    7: "VII", 8: "VIII", 9: "IX", 10: "X", 11: "XI", 12: "XII",
}


class ProcurementPolicy(models.Model):
    organization = models.OneToOneField("companies.Organization", on_delete=models.CASCADE, related_name="procurement_policy")
    direct_purchase_limit = models.DecimalField(max_digits=18, decimal_places=2, default=1000000)
    formal_sourcing_limit = models.DecimalField(max_digits=18, decimal_places=2, default=50000000)
    minimum_quotes = models.PositiveSmallIntegerField(default=3)
    price_tolerance_percent = models.DecimalField(max_digits=6, decimal_places=3, default=0)
    quantity_tolerance_percent = models.DecimalField(max_digits=6, decimal_places=3, default=0)
    tax_tolerance_amount = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    updated_at = models.DateTimeField(auto_now=True)
    class Meta: db_table = "procurement_policy"
    @classmethod
    def current(cls, organization):
        obj, _ = cls.objects.get_or_create(organization=organization)
        return obj


class ProcurementEvent(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="procurement_events")
    document_type = models.CharField(max_length=40, db_index=True)
    document_id = models.CharField(max_length=40, db_index=True)
    action = models.CharField(max_length=40, db_index=True)
    from_status = models.CharField(max_length=30, blank=True)
    to_status = models.CharField(max_length=30, blank=True)
    actor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="procurement_events")
    reason = models.TextField(blank=True)
    metadata = models.JSONField(default=dict, blank=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    class Meta:
        db_table = "procurement_events"
        ordering = ["created_at"]


class FinancePosting(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="procurement_finance_postings_scoped")
    source_type = models.CharField(max_length=30)
    source_id = models.CharField(max_length=40)
    posting_type = models.CharField(max_length=20, choices=[("accrual", "Accrual"), ("payment", "Payment"), ("reversal", "Reversal")])
    amount = models.DecimalField(max_digits=18, decimal_places=2)
    currency = models.CharField(max_length=10, default="IDR")
    payload = models.JSONField(default=dict)
    status = models.CharField(max_length=20, choices=[("pending", "Pending"), ("posted", "Posted"), ("failed", "Failed")], default="pending")
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="procurement_finance_postings")
    created_at = models.DateTimeField(auto_now_add=True)
    class Meta:
        db_table = "procurement_finance_postings"
        ordering = ["created_at"]
        constraints = [models.UniqueConstraint(fields=["organization", "source_type", "source_id", "posting_type"], name="unique_procurement_finance_posting")]


class Requisition(models.Model):
    """Purchase requisitions."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="requisitions")
    number = models.CharField(max_length=40, blank=True)
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=[
        ("draft", "Draft"),
        ("pending", "Pending"),
        ("approved", "Approved"),
        ("rejected", "Rejected"),
        ("fulfilled", "Fulfilled"),
    ], default="draft")
    procurement_method = models.CharField(max_length=30, choices=[("direct_purchase", "Direct Purchase"), ("three_quotes", "Three Quotations"), ("formal_rfq", "Formal RFQ/RFP"), ("single_source", "Single Source"), ("emergency", "Emergency")], blank=True)
    exception_justification = models.TextField(blank=True)
    requested_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="requisitions")
    requesting_department = models.ForeignKey("companies.Department", on_delete=models.PROTECT, null=True, blank=True, related_name="purchase_requisitions")
    project = models.ForeignKey("projects.Project", on_delete=models.PROTECT, null=True, blank=True, related_name="purchase_requisitions")
    vendor = models.ForeignKey(
        "asset_management.Vendor",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="requisitions",
    )
    items = models.JSONField(default=list)
    total_amount = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
    # Budget line this requisition draws from (traceability + advisory spend
    # context). Optional — unlinked requisitions submit with a soft warning.
    budget_item = models.ForeignKey(
        "finance.BudgetItem",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="requisitions",
    )
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "requisitions"
        ordering = ["-created_at"]
        indexes = [models.Index(fields=["status"])]
        constraints = [models.UniqueConstraint(fields=["organization", "number"], name="unique_requisition_number_per_org")]

    def save(self, *args, **kwargs):
        if not self.number:
            now = timezone.now()
            month = _ROMAN_MONTHS[now.month]
            year = now.year
            # Derive from the highest existing sequence, not count() — gaps from
            # deleted rows would otherwise reuse a taken number and collide.
            # Scoped per-org so each org's sequence starts fresh and can't
            # collide with (or skip ahead based on) another org's rows.
            suffix = f"/{month}/{year}"
            existing = Requisition.objects.filter(organization=self.organization, number__endswith=suffix).values_list("number", flat=True)
            max_seq = 0
            for num in existing:
                try:
                    max_seq = max(max_seq, int(num.split("-")[1].split("/")[0]))
                except (IndexError, ValueError):
                    continue
            self.number = f"P-{max_seq + 1:04d}{suffix}"
        super().save(*args, **kwargs)

    def budget_context(self) -> dict:
        """Advisory budget info for this requisition's linked budget item.

        BudgetItem has no allocation figure yet, so there's nothing to enforce —
        we surface committed spend (sum of this + other live requisitions on the
        same item) and a warning when no budget line is linked. Returns None
        figures rather than raising, so callers can render it unconditionally.
        """
        if not self.budget_item_id:
            return {
                "budget_item": None,
                "budget_item_name": None,
                "committed": None,
                "warning": "No budget line linked — spend is untracked.",
            }
        # "Committed" = amounts already in flight (pending/approved/fulfilled)
        # on this budget item, including this requisition.
        committed = (
            Requisition.objects.filter(
                budget_item_id=self.budget_item_id,
                status__in=["pending", "approved", "fulfilled"],
            )
            .exclude(total_amount__isnull=True)
            .aggregate(total=Sum("total_amount"))["total"] or Decimal("0")
        )
        return {
            "budget_item": str(self.budget_item_id),
            "budget_item_name": self.budget_item.name,
            "committed": committed,
            "warning": None,
        }


class RequisitionQuote(models.Model):
    """Vendor quotes for a requisition (multi-vendor price comparison)."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    requisition = models.ForeignKey(Requisition, on_delete=models.PROTECT, related_name="quotes")
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.PROTECT, related_name="requisition_quotes")
    quoted_amount = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
    delivery_estimate = models.CharField(max_length=100, blank=True)
    is_selected = models.BooleanField(default=False)
    # Award audit. Awarding a non-cheapest quote requires a justification
    # (enforced in the award action), captured here alongside who/when.
    award_reason = models.TextField(blank=True)
    awarded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="awarded_quotes",
    )
    awarded_at = models.DateTimeField(null=True, blank=True)
    notes = models.TextField(blank=True)
    attachment = models.FileField(upload_to=tenant_upload_path("requisition-quotes"), null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "requisition_quotes"
        ordering = ["-is_selected", "quoted_amount"]
        constraints = [
            models.UniqueConstraint(fields=["requisition", "vendor"], name="uniq_requisition_vendor_quote"),
            # At most one awarded quote per requisition.
            models.UniqueConstraint(
                fields=["requisition"],
                condition=models.Q(is_selected=True),
                name="uniq_awarded_quote_per_requisition",
            ),
        ]

    @property
    def is_lowest(self) -> bool:
        """True if this is the cheapest priced quote on its requisition."""
        if self.quoted_amount is None:
            return False
        cheapest = (
            RequisitionQuote.objects
            .filter(requisition_id=self.requisition_id, quoted_amount__isnull=False)
            .order_by("quoted_amount")
            .values_list("quoted_amount", flat=True)
            .first()
        )
        return cheapest is not None and self.quoted_amount == cheapest


class Approval(models.Model):
    """Requisition approvals.

    Part of an ordered, amount-driven approval chain. A requisition's
    total_amount picks a policy (see approval_policy.py) that generates one
    Approval per level. Level 1 starts "pending"; higher levels start "blocked"
    and unblock only when the level below approves. The requisition reaches
    "approved" once the top level approves; any rejection rejects it outright.
    """
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    requisition = models.ForeignKey(Requisition, on_delete=models.PROTECT, related_name="approvals")
    # 1-based position in the chain. Lower levels approve first.
    level = models.PositiveSmallIntegerField(default=1)
    # RBAC role slug allowed to act on this step (e.g. "procurement",
    # "finance-manager", "super-admin"). Anyone holding it can claim/decide.
    required_role = models.CharField(max_length=80, blank=True)
    # Resolved once someone claims/decides the step. Null while unclaimed.
    approver = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="requisition_approvals",
    )
    status = models.CharField(max_length=20, choices=[
        ("blocked", "Blocked"),       # waiting on a lower level
        ("pending", "Pending"),       # active, awaiting this level's decision
        ("approved", "Approved"),
        ("rejected", "Rejected"),
        ("cancelled", "Cancelled"),   # chain rejected/superseded above
    ], default="pending")
    comment = models.TextField(blank=True)
    resolved_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "requisition_approvals"
        ordering = ["requisition", "level", "created_at"]
        indexes = [
            # Decision logic walks a requisition's chain in level order.
            models.Index(fields=["requisition", "level"]),
        ]


class RFQRFP(models.Model):
    """Request for Quote / Proposal."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="rfq_rfps_scoped")
    title = models.CharField(max_length=255)
    rfq_rfp_type = models.CharField(max_length=10, choices=[
        ("rfq", "Request for Quote"),
        ("rfp", "Request for Proposal"),
    ])
    description = models.TextField()
    requirements = models.TextField()
    deadline = models.DateTimeField()
    status = models.CharField(max_length=20, choices=[
        ("draft", "Draft"),
        ("published", "Published"),
        ("closed", "Closed"),
        ("awarded", "Awarded"),
    ], default="draft")
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="rfq_rfps")
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.SET_NULL, null=True, blank=True, related_name="rfq_rfps")
    attachments = models.JSONField(default=list)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "rfq_rfps"
        ordering = ["-created_at"]


class PurchaseOrder(models.Model):
    """Purchase orders."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="purchase_orders_scoped")
    order_number = models.CharField(max_length=50)
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.PROTECT, related_name="purchase_orders")
    requisition = models.ForeignKey(Requisition, on_delete=models.PROTECT, null=True, blank=True, related_name="purchase_orders")
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="created_purchase_orders")
    issued_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="issued_purchase_orders")
    issued_at = models.DateTimeField(null=True, blank=True)
    currency = models.CharField(max_length=10, default="IDR")
    items = models.JSONField(default=list)
    subtotal = models.DecimalField(max_digits=12, decimal_places=2)
    tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    total = models.DecimalField(max_digits=12, decimal_places=2)
    status = models.CharField(max_length=20, choices=[
        ("draft", "Draft"),
        ("sent", "Sent"),
        ("acknowledged", "Acknowledged"),
        ("fulfilled", "Fulfilled"),
        ("cancelled", "Cancelled"),
    ], default="draft")
    expected_delivery = models.DateField(null=True, blank=True)
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "purchase_orders"
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["status"]),
            models.Index(fields=["created_at"]),
        ]
        constraints = [models.UniqueConstraint(fields=["organization", "order_number"], name="unique_order_number_per_org")]


class BudgetCommitment(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    purchase_order = models.OneToOneField(PurchaseOrder, on_delete=models.PROTECT, null=True, blank=True, related_name="budget_commitment")
    contract = models.OneToOneField("ProcurementContract", on_delete=models.PROTECT, null=True, blank=True, related_name="budget_commitment")
    budget_item = models.ForeignKey("finance.BudgetItem", on_delete=models.PROTECT, related_name="procurement_commitments")
    amount = models.DecimalField(max_digits=18, decimal_places=2)
    currency = models.CharField(max_length=10, default="IDR")
    status = models.CharField(max_length=20, choices=[("active", "Active"), ("consumed", "Consumed"), ("released", "Released")], default="active")
    created_at = models.DateTimeField(auto_now_add=True)
    released_at = models.DateTimeField(null=True, blank=True)
    class Meta:
        db_table = "procurement_budget_commitments"
        constraints = [models.CheckConstraint(condition=models.Q(purchase_order__isnull=False, contract__isnull=True) | models.Q(purchase_order__isnull=True, contract__isnull=False), name="commitment_exactly_one_source")]


class ProcurementContract(models.Model):
    """Contracts for procurement."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="procurement_contracts_scoped")
    title = models.CharField(max_length=255)
    contract_number = models.CharField(max_length=100)
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.PROTECT, related_name="procurement_contracts")
    requisition = models.ForeignKey(Requisition, on_delete=models.PROTECT, null=True, blank=True, related_name="procurement_contracts")
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="created_procurement_contracts")
    signed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="signed_procurement_contracts")
    signed_at = models.DateTimeField(null=True, blank=True)
    currency = models.CharField(max_length=10, default="IDR")
    value = models.DecimalField(max_digits=12, decimal_places=2)
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    status = models.CharField(max_length=20, choices=[
        ("draft", "Draft"),
        ("active", "Active"),
        ("expired", "Expired"),
        ("terminated", "Terminated"),
    ], default="draft")
    document = models.FileField(upload_to=tenant_upload_path("procurement-contracts"), null=True, blank=True)
    terms = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "procurement_contracts"
        ordering = ["-created_at"]
        constraints = [models.UniqueConstraint(fields=["organization", "contract_number"], name="unique_contract_number_per_org")]


class GoodsReceipt(models.Model):
    """Goods receipt records."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="goods_receipts_scoped")
    purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.PROTECT, related_name="goods_receipts")
    receipt_number = models.CharField(max_length=50)
    received_date = models.DateField()
    items = models.JSONField(default=list)
    status = models.CharField(max_length=20, choices=[
        ("pending", "Pending"),
        ("received", "Received"),
        ("partial", "Partially Received"),
        ("rejected", "Rejected"),
    ], default="pending")
    notes = models.TextField(blank=True)
    received_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "goods_receipts"
        ordering = ["-received_date"]
        constraints = [models.UniqueConstraint(fields=["organization", "receipt_number"], name="unique_receipt_number_per_org")]


class Invoice(models.Model):
    """Purchase invoices."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="procurement_invoices_scoped")
    invoice_number = models.CharField(max_length=50)
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.PROTECT, related_name="invoices")
    purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.PROTECT, null=True, blank=True, related_name="invoices")
    goods_receipt = models.ForeignKey(GoodsReceipt, on_delete=models.PROTECT, null=True, blank=True, related_name="invoices")
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="created_procurement_invoices")
    approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="approved_procurement_invoices")
    approved_at = models.DateTimeField(null=True, blank=True)
    items = models.JSONField(default=list, blank=True)
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    total = models.DecimalField(max_digits=12, decimal_places=2)
    due_date = models.DateField()
    status = models.CharField(max_length=20, choices=[
        ("pending", "Pending"),
        ("approved", "Approved"),
        ("paid", "Paid"),
        ("overdue", "Overdue"),
        ("cancelled", "Cancelled"),
    ], default="pending")
    paid_at = models.DateTimeField(null=True, blank=True)
    # 3-way match result (PO total == GRN received value == Invoice total).
    # Recomputed on save; gates payment when not "matched".
    match_status = models.CharField(max_length=20, choices=[
        ("unmatched", "Unmatched"),      # missing PO or GRN link — cannot compare
        ("matched", "Matched"),          # all three totals equal (exact)
        ("mismatched", "Mismatched"),    # at least one total differs
    ], default="unmatched")
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "invoices"
        ordering = ["-created_at"]
        constraints = [models.UniqueConstraint(fields=["organization", "invoice_number"], name="unique_procurement_invoice_number_per_org")]

    @staticmethod
    def _grn_value(goods_receipt) -> "Decimal | None":
        """Received value = sum(line qty × unit_price) over the GRN's items."""
        if goods_receipt is None:
            return None
        total = Decimal("0")
        for it in (goods_receipt.items or []):
            qty = Decimal(str(it.get("quantity") or it.get("qty") or 0))
            price = Decimal(str(it.get("unit_price") or it.get("price") or 0))
            total += qty * price
        return total

    def match_detail(self) -> dict:
        """Compare PO total, GRN received value, and Invoice total (exact).

        Returns the three numbers plus a per-leg pass flag and overall status,
        so the API/UI can show *why* a match failed, not just that it did.
        """
        if self.purchase_order_id and self.items and self.purchase_order.items:
            policy = ProcurementPolicy.current(self.organization)
            key = lambda item: str(item.get("sku") or item.get("id") or item.get("name") or "").strip().casefold()
            def number(item, *names):
                for name in names:
                    if item.get(name) not in (None, ""):
                        return Decimal(str(item[name]))
                return Decimal("0")
            ordered = {key(item): item for item in self.purchase_order.items}
            received, prior_invoiced = {}, {}
            # InvoiceViewSet's queryset prefetches these under matching_goods_receipts/
            # matching_invoices (Prefetch + to_attr) to avoid a query per row; fall back
            # to a live filtered query when match_detail() is called off that queryset.
            goods_receipts = getattr(
                self.purchase_order, "matching_goods_receipts", None
            )
            if goods_receipts is None:
                goods_receipts = self.purchase_order.goods_receipts.filter(status__in=["partial", "received"])
            for receipt in goods_receipts:
                for item in receipt.items or []:
                    received[key(item)] = received.get(key(item), Decimal("0")) + number(item, "quantity", "qty")
            other_invoices = getattr(self.purchase_order, "matching_invoices", None)
            if other_invoices is None:
                other_invoices = self.purchase_order.invoices.exclude(pk=self.pk).exclude(status="cancelled")
            else:
                other_invoices = [inv for inv in other_invoices if inv.pk != self.pk]
            for invoice in other_invoices:
                for item in invoice.items or []:
                    prior_invoiced[key(item)] = prior_invoiced.get(key(item), Decimal("0")) + number(item, "quantity", "qty")
            lines, computed = [], Decimal("0")
            for item in self.items:
                k, order_item = key(item), ordered.get(key(item))
                invoice_qty, invoice_price = number(item, "quantity", "qty"), number(item, "unit_price", "price")
                computed += invoice_qty * invoice_price
                if not order_item:
                    lines.append({"key": k, "quantity_ok": False, "price_ok": False, "reason": "Line is not on the purchase order."}); continue
                ordered_qty, ordered_price = number(order_item, "quantity", "qty"), number(order_item, "unit_price", "price")
                available = max(Decimal("0"), received.get(k, Decimal("0")) - prior_invoiced.get(k, Decimal("0")))
                quantity_ok = invoice_qty > 0 and invoice_qty <= available + ordered_qty * policy.quantity_tolerance_percent / Decimal("100")
                price_ok = abs(invoice_price - ordered_price) <= ordered_price * policy.price_tolerance_percent / Decimal("100")
                lines.append({"key": k, "ordered_quantity": ordered_qty, "received_available": available, "invoice_quantity": invoice_qty, "ordered_unit_price": ordered_price, "invoice_unit_price": invoice_price, "quantity_ok": quantity_ok, "price_ok": price_ok})
            amount_ok = computed == self.amount
            tax_ok = abs((self.total - self.amount) - self.tax) <= policy.tax_tolerance_amount
            matched = bool(lines and all(line["quantity_ok"] and line["price_ok"] for line in lines) and amount_ok and tax_ok)
            return {"status": "matched" if matched else "mismatched", "lines": lines, "amount_ok": amount_ok, "tax_ok": tax_ok, "po_total": self.purchase_order.total, "grn_value": self._grn_value(self.goods_receipt), "invoice_total": self.total, "po_vs_invoice": None, "grn_vs_invoice": matched, "po_vs_grn": None}

        po_total = self.purchase_order.total if self.purchase_order_id else None
        grn_value = self._grn_value(self.goods_receipt) if self.goods_receipt_id else None
        inv_total = self.total

        has_links = po_total is not None and grn_value is not None
        po_inv = has_links and po_total == inv_total
        grn_inv = has_links and grn_value == inv_total
        po_grn = has_links and po_total == grn_value
        matched = bool(has_links and po_inv and grn_inv and po_grn)

        if not has_links:
            status = "unmatched"
        elif matched:
            status = "matched"
        else:
            status = "mismatched"

        return {
            "status": status,
            "lines": [],
            "po_total": po_total,
            "grn_value": grn_value,
            "invoice_total": inv_total,
            "po_vs_invoice": po_inv,
            "grn_vs_invoice": grn_inv,
            "po_vs_grn": po_grn,
        }

    def save(self, *args, **kwargs):
        self.match_status = self.match_detail()["status"]
        super().save(*args, **kwargs)


class Payment(models.Model):
    """Payments for invoices."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="procurement_payments_scoped")
    invoice = models.ForeignKey(Invoice, on_delete=models.PROTECT, related_name="payments")
    prepared_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="prepared_procurement_payments")
    released_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="released_procurement_payments")
    fund_approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="fund_approved_procurement_payments")
    fund_approved_at = models.DateTimeField(null=True, blank=True)
    fund_approval_comment = models.TextField(blank=True)
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    method = models.CharField(max_length=20, choices=[
        ("bank_transfer", "Bank Transfer"),
        ("cash", "Cash"),
        ("check", "Check"),
        ("card", "Card"),
    ])
    reference = models.CharField(max_length=100, blank=True)
    status = models.CharField(max_length=20, choices=[
        ("pending", "Pending"),
        ("completed", "Completed"),
        ("failed", "Failed"),
    ], default="pending")
    paid_at = models.DateTimeField(null=True, blank=True)
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "payments"
        ordering = ["-created_at"]
        indexes = [
            # Cash statements filter completed payments on every request.
            models.Index(fields=["status"]),
        ]
