from django.db import models
from django.conf import settings
from django.utils import timezone
from decimal import Decimal
import secrets
import time


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 Requisition(models.Model):
    """Purchase requisitions."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    number = models.CharField(max_length=40, unique=True, 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")
    requested_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="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"]

    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.
            suffix = f"/{month}/{year}"
            existing = Requisition.objects.filter(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.
        live = (
            Requisition.objects.filter(
                budget_item_id=self.budget_item_id,
                status__in=["pending", "approved", "fulfilled"],
            )
            .exclude(total_amount__isnull=True)
        )
        committed = sum((r.total_amount for r in live), 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.CASCADE, related_name="quotes")
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.CASCADE, 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="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.CASCADE, 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)
    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.CASCADE, 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)
    order_number = models.CharField(max_length=50, unique=True)
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.CASCADE, related_name="purchase_orders")
    requisition = models.ForeignKey(Requisition, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_orders")
    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"]


class ProcurementContract(models.Model):
    """Contracts for procurement."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    title = models.CharField(max_length=255)
    contract_number = models.CharField(max_length=100, unique=True)
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.CASCADE, related_name="procurement_contracts")
    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=[
        ("active", "Active"),
        ("expired", "Expired"),
        ("terminated", "Terminated"),
    ], default="active")
    document = models.FileField(upload_to="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"]


class GoodsReceipt(models.Model):
    """Goods receipt records."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name="goods_receipts")
    receipt_number = models.CharField(max_length=50, unique=True)
    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"]


class Invoice(models.Model):
    """Purchase invoices."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    invoice_number = models.CharField(max_length=50, unique=True)
    vendor = models.ForeignKey("asset_management.Vendor", on_delete=models.CASCADE, related_name="invoices")
    purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.SET_NULL, null=True, blank=True, related_name="invoices")
    goods_receipt = models.ForeignKey(GoodsReceipt, on_delete=models.SET_NULL, null=True, blank=True, related_name="invoices")
    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"]

    @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.
        """
        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,
            "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)
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name="payments")
    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"]),
        ]