from decimal import Decimal

from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from django.db.models import Sum
from django.utils import timezone
from drf_spectacular.utils import extend_schema, OpenApiParameter

from apps.asset_management.models import Vendor
from .models import Requisition, RequisitionQuote, Approval, RFQRFP, PurchaseOrder, ProcurementContract, GoodsReceipt, Invoice, Payment
from .serializers import (
    RequisitionSerializer, RequisitionQuoteSerializer, ApprovalSerializer, RFQRFPSerializer,
    PurchaseOrderSerializer, ProcurementContractSerializer,
    GoodsReceiptSerializer, InvoiceSerializer, PaymentSerializer
)


class QueryFilterMixin:
    """Apply exact-match query-string filtering for declared fields.

    Each ViewSet lists the query params it honors in `query_filter_fields`
    (names match both the ?param and the model field). Present params filter
    the queryset; absent or blank ones are ignored. This backs the
    OpenApiParameter declarations on each list() — without it those params are
    documented but silently no-ops.
    """
    query_filter_fields: list[str] = []

    def get_queryset(self):
        qs = super().get_queryset()
        for field in self.query_filter_fields:
            value = self.request.query_params.get(field)
            if value not in (None, ""):
                qs = qs.filter(**{field: value})
        return qs


class RequisitionViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for Requisition management."""
    rbac_domain = "procurements"
    queryset = Requisition.objects.select_related("requested_by", "vendor", "budget_item")
    serializer_class = RequisitionSerializer
    query_filter_fields = ["status", "requested_by", "vendor"]

    def perform_create(self, serializer):
        serializer.save(requested_by=self.request.user)

    @extend_schema(
        parameters=[
            OpenApiParameter(name="status", type=str, description="Filter by status"),
            OpenApiParameter(name="requested_by", type=str, description="Filter by requester ID"),
            OpenApiParameter(name="vendor", type=str, description="Filter by vendor ID"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    @action(detail=True, methods=["post"])
    def submit(self, request, pk=None):
        """Submit a requisition for approval, building the amount-based chain.

        Reads total_amount → policy → ordered role chain. Creates one Approval
        per role (level 1 pending, the rest blocked) and moves the requisition
        to "pending". An empty chain (below the auto-approve floor) approves the
        requisition immediately with no Approval rows.

        Idempotent guard: refuses if the requisition already has approvals or is
        past the draft/pending stage.
        """
        from .approval_policy import chain_for

        req = self.get_object()
        if req.approvals.exists():
            return Response(
                {"detail": "Requisition already submitted."},
                status=409,
            )
        if req.status not in ("draft", "pending"):
            return Response(
                {"detail": f"Cannot submit a requisition in status '{req.status}'."},
                status=409,
            )

        chain = chain_for(req.total_amount)

        # Advisory budget context — never blocks, just surfaced to the caller.
        budget = req.budget_context()
        warnings = [budget["warning"]] if budget.get("warning") else []

        if not chain:
            # Below the floor — auto-approve, no human gate.
            req.status = "approved"
            req.save(update_fields=["status", "updated_at"])
            return Response({"status": "approved", "auto": True, "levels": 0, "warnings": warnings})

        approvals = []
        for idx, role in enumerate(chain, start=1):
            approvals.append(Approval.objects.create(
                requisition=req,
                level=idx,
                required_role=role,
                status="pending" if idx == 1 else "blocked",
            ))
        req.status = "pending"
        req.save(update_fields=["status", "updated_at"])

        # Notify everyone holding the level-1 role that an approval is waiting.
        for user in _users_in_role(chain[0]):
            _notify_procurement(
                user,
                title="Approval needed",
                message=f"Requisition “{req.title}” ({_money(req.total_amount)}) needs your approval.",
                link="/procurements/approvals",
                priority="high",
            )

        return Response(
            {"status": "pending", "auto": False, "levels": len(chain), "chain": chain, "warnings": warnings},
            status=201,
        )


class RequisitionQuoteViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for RequisitionQuote management."""
    rbac_domain = "procurements"
    queryset = RequisitionQuote.objects.select_related("requisition", "vendor", "awarded_by")
    serializer_class = RequisitionQuoteSerializer
    query_filter_fields = ["requisition", "vendor"]

    @extend_schema(
        parameters=[
            OpenApiParameter(name="requisition", type=str, description="Filter by requisition ID"),
            OpenApiParameter(name="vendor", type=str, description="Filter by vendor ID"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    @action(detail=True, methods=["post"])
    def award(self, request, pk=None):
        """Award this quote — selects it and clears any prior award on the
        requisition (single award enforced).

        Awarding a quote that isn't the cheapest requires `award_reason` in the
        body — a buyer can override the low bid, but must justify it. Records who
        awarded and when.
        """
        quote = self.get_object()
        reason = (request.data.get("award_reason") or "").strip()

        if not quote.is_lowest and not reason:
            return Response(
                {"award_reason": ["Awarding a non-lowest quote requires a justification."]},
                status=400,
            )

        # Clear any existing award on this requisition (the partial unique
        # constraint would otherwise reject a second selected row).
        RequisitionQuote.objects.filter(
            requisition_id=quote.requisition_id, is_selected=True
        ).exclude(id=quote.id).update(
            is_selected=False, award_reason="", awarded_by=None, awarded_at=None
        )

        quote.is_selected = True
        quote.award_reason = reason
        quote.awarded_by = request.user if request.user.is_authenticated else None
        quote.awarded_at = timezone.now()
        quote.save(update_fields=["is_selected", "award_reason", "awarded_by", "awarded_at"])

        return Response(self.get_serializer(quote).data)


def _users_in_role(role_slug):
    """All users assigned an RBAC role by slug. Empty if the role doesn't exist."""
    if not role_slug:
        return []
    from django.contrib.auth import get_user_model
    User = get_user_model()
    return list(
        User.objects.filter(user_roles__role__slug=role_slug).distinct()
    )


def _money(amount):
    """Format an IDR amount for notification copy."""
    if amount is None:
        return "Rp —"
    try:
        return f"Rp {int(amount):,}".replace(",", ".")
    except (TypeError, ValueError):
        return f"Rp {amount}"


def _notify_procurement(user, *, title, message, link="", priority="medium", related_user=None):
    """Fire an in-app procurement notification; never break the request on failure."""
    if not user:
        return
    try:
        from apps.notifications.handler import NotificationHandler
        NotificationHandler.send(
            user=user, title=title, message=message,
            notification_type="procurement", priority=priority, icon="shopping-cart",
            link=link, related_user=related_user, broadcast=True,
        )
    except Exception:
        pass


class ApprovalViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for Approval management."""
    rbac_domain = "procurements"
    queryset = Approval.objects.select_related("requisition", "requisition__vendor", "approver")
    serializer_class = ApprovalSerializer
    query_filter_fields = ["requisition", "status"]

    @staticmethod
    def _assert_can_decide(approval, actor):
        """Only a holder of the level's required_role may approve/reject it.

        Superusers bypass. A level with no required_role is open to any
        authenticated user (matches the chain builder's optional roles).
        """
        if actor is None:
            raise PermissionDenied("Authentication required to decide an approval.")
        if actor.is_superuser:
            return
        role = approval.required_role
        if not role:
            return
        if not actor.user_roles.filter(role__slug=role).exists():
            raise PermissionDenied(f"This approval requires the “{role}” role.")

    def perform_create(self, serializer):
        approval = serializer.save()
        actor = self.request.user if self.request.user.is_authenticated else None
        # New approval task → notify the assigned approver.
        if approval.approver_id and (not actor or approval.approver_id != actor.id):
            _notify_procurement(
                approval.approver,
                title="Approval needed",
                message=f"Requisition “{approval.requisition.title}” needs your approval.",
                link="/procurements",
                priority="high",
                related_user=actor,
            )

    def perform_update(self, serializer):
        old_status = serializer.instance.status
        actor = self.request.user if self.request.user.is_authenticated else None
        new_status = serializer.validated_data.get("status", old_status)

        # Deciding a level requires the level's RBAC role (superusers bypass).
        if new_status != old_status and new_status in ("approved", "rejected"):
            self._assert_can_decide(serializer.instance, actor)

        approval = serializer.save()
        if approval.status == old_status or approval.status not in ("approved", "rejected"):
            return

        # Stamp who decided + when (the chain doesn't pre-assign an approver).
        fields = []
        if actor and approval.approver_id is None:
            approval.approver = actor
            fields.append("approver")
        if approval.resolved_at is None:
            approval.resolved_at = timezone.now()
            fields.append("resolved_at")
        if fields:
            approval.save(update_fields=fields)

        self._advance_chain(approval, actor)

    def _advance_chain(self, approval, actor):
        """Move the requisition forward after a level's decision.

        Rejection at any level rejects the requisition and cancels the rest.
        Approval unblocks the next level; approval of the top level approves the
        requisition.
        """
        req = approval.requisition
        if not req:
            return
        siblings = list(req.approvals.order_by("level", "created_at"))

        if approval.status == "rejected":
            for s in siblings:
                if s.id != approval.id and s.status in ("blocked", "pending"):
                    s.status = "cancelled"
                    s.save(update_fields=["status"])
            if req.status in ("draft", "pending"):
                req.status = "rejected"
                req.save(update_fields=["status", "updated_at"])
            self._notify_requester(req, "rejected", approval.approver)
            return

        # Approved: find the next not-yet-decided level and unblock it.
        nxt = next(
            (s for s in siblings if s.level > approval.level and s.status == "blocked"),
            None,
        )
        if nxt:
            nxt.status = "pending"
            nxt.save(update_fields=["status"])
            for user in _users_in_role(nxt.required_role):
                _notify_procurement(
                    user,
                    title="Approval needed",
                    message=f"Requisition “{req.title}” reached your approval level.",
                    link="/procurements/approvals",
                    priority="high",
                )
            return

        # No higher level left → fully approved.
        if req.status in ("draft", "pending"):
            req.status = "approved"
            req.save(update_fields=["status", "updated_at"])
        self._notify_requester(req, "approved", approval.approver)

    def _notify_requester(self, req, decision, approver):
        if req.requested_by_id:
            _notify_procurement(
                req.requested_by,
                title=f"Requisition {decision}",
                message=f"Your requisition “{req.title}” was {decision}.",
                link="/procurements/requisitions",
                priority="high",
                related_user=approver,
            )

    @extend_schema(
        parameters=[
            OpenApiParameter(name="requisition", type=str, description="Filter by requisition ID"),
            OpenApiParameter(name="status", type=str, description="Filter by status"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)


class RFQRFPViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for RFQRFP management."""
    rbac_domain = "procurements"
    queryset = RFQRFP.objects.select_related("created_by", "vendor")
    serializer_class = RFQRFPSerializer
    query_filter_fields = ["status", "rfq_rfp_type"]

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)

    @extend_schema(
        parameters=[
            OpenApiParameter(name="status", type=str, description="Filter by status"),
            OpenApiParameter(name="rfq_rfp_type", type=str, description="Filter by type"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)


class PurchaseOrderViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for PurchaseOrder management."""
    rbac_domain = "procurements"
    queryset = PurchaseOrder.objects.select_related("vendor", "requisition")
    serializer_class = PurchaseOrderSerializer
    query_filter_fields = ["status", "vendor"]

    @extend_schema(
        parameters=[
            OpenApiParameter(name="status", type=str, description="Filter by status"),
            OpenApiParameter(name="vendor", type=str, description="Filter by vendor ID"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)


class ProcurementContractViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for ProcurementContract management."""
    rbac_domain = "procurements"
    queryset = ProcurementContract.objects.select_related("vendor")
    serializer_class = ProcurementContractSerializer
    query_filter_fields = ["status", "vendor"]

    @extend_schema(
        parameters=[
            OpenApiParameter(name="status", type=str, description="Filter by status"),
            OpenApiParameter(name="vendor", type=str, description="Filter by vendor ID"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)


class GoodsReceiptViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for GoodsReceipt management."""
    rbac_domain = "procurements"
    # generate_assets writes new Asset rows → gate it behind the create perm.
    rbac_action_map = {"generate_assets": "create"}
    queryset = GoodsReceipt.objects.select_related("purchase_order", "purchase_order__vendor", "received_by")
    serializer_class = GoodsReceiptSerializer
    query_filter_fields = ["purchase_order", "status"]

    def perform_create(self, serializer):
        gr = serializer.save()
        self._cascade_received(gr)

    def perform_update(self, serializer):
        gr = serializer.save()
        self._cascade_received(gr)

    @staticmethod
    def _cascade_received(gr):
        """Fully-received goods → advance the PO and its requisition.

        Mirrors the Approval→Requisition cascade so a goods receipt finishes
        the flow without separate manual status edits. Only acts on a complete
        receipt ('received'); 'partial' leaves the PO open.
        """
        if gr.status != "received":
            return
        po = gr.purchase_order
        if not po:
            return
        if po.status != "fulfilled":
            po.status = "fulfilled"
            po.save(update_fields=["status", "updated_at"])
        req = po.requisition
        if req and req.status != "fulfilled":
            req.status = "fulfilled"
            req.save(update_fields=["status", "updated_at"])

    @action(detail=True, methods=["post"])
    def generate_assets(self, request, pk=None):
        """Create inventory Assets from this goods receipt's line items.

        One Asset per unit (line `quantity`/`qty`), each pre-linked to this GR,
        its PO, and the PO vendor. Optional body: `category` (AssetCategory id),
        `location` (AssetLocation id). Skips lines already turned into assets so
        the action is idempotent per (GR, line) — re-running only fills gaps.

        Line items are heterogeneous (goods + services); the caller decides what
        to assetize by passing `line_indexes` (list of item positions). Omit it
        to assetize every line.
        """
        from apps.asset_management.models import Asset, AssetCategory, AssetLocation
        from apps.asset_management.serializers import AssetSerializer

        gr = self.get_object()
        po = gr.purchase_order
        items = gr.items or []

        wanted = request.data.get("line_indexes")
        if wanted is not None:
            wanted = set(wanted)

        category = None
        if request.data.get("category"):
            category = AssetCategory.objects.filter(pk=request.data["category"]).first()
        location = None
        if request.data.get("location"):
            location = AssetLocation.objects.filter(pk=request.data["location"]).first()

        # Count assets already generated from this GR to keep tags unique and
        # to skip re-creating on a second run.
        already = Asset.objects.filter(goods_receipt=gr).count()
        gr_seq = str(gr.id)[-6:]

        created = []
        seq = already
        for idx, it in enumerate(items):
            if wanted is not None and idx not in wanted:
                continue
            name = it.get("name") or "Item"
            qty = int(it.get("quantity") or it.get("qty") or 1)
            unit_price = it.get("unit_price") or it.get("price")
            # Per-line idempotency: how many already exist for this line name.
            existing_for_line = Asset.objects.filter(
                goods_receipt=gr, name=name
            ).count()
            for _ in range(max(0, qty - existing_for_line)):
                seq += 1
                asset = Asset.objects.create(
                    name=name,
                    asset_tag=f"GR-{gr_seq}-{seq:03d}",
                    category=category,
                    location=location,
                    vendor=po.vendor if po else None,
                    purchase_order=po,
                    goods_receipt=gr,
                    purchase_date=gr.received_date,
                    purchase_price=Decimal(str(unit_price)) if unit_price is not None else None,
                    status="available",
                )
                created.append(asset)

        data = AssetSerializer(created, many=True).data
        return Response({"created": len(created), "assets": data})

    @extend_schema(
        parameters=[
            OpenApiParameter(name="purchase_order", type=str, description="Filter by PO ID"),
            OpenApiParameter(name="status", type=str, description="Filter by status"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)


class InvoiceViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for Invoice management."""
    rbac_domain = "procurements"
    queryset = Invoice.objects.select_related("vendor", "purchase_order", "goods_receipt")
    serializer_class = InvoiceSerializer
    query_filter_fields = ["status", "vendor"]

    @extend_schema(
        parameters=[
            OpenApiParameter(name="status", type=str, description="Filter by status"),
            OpenApiParameter(name="vendor", type=str, description="Filter by vendor ID"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    @action(detail=True, methods=["post"])
    def match(self, request, pk=None):
        """Recompute the 3-way match for this invoice and return the detail.

        Match is also recomputed on every save(); this is the explicit
        "re-check now" button (e.g. after the linked PO/GRN was edited).
        """
        invoice = self.get_object()
        invoice.save(update_fields=["match_status"])
        return Response(invoice.match_detail())


class PaymentViewSet(QueryFilterMixin, viewsets.ModelViewSet):
    """ViewSet for Payment management."""
    rbac_domain = "procurements"
    queryset = Payment.objects.select_related("invoice", "invoice__vendor")
    serializer_class = PaymentSerializer
    query_filter_fields = ["invoice", "status"]

    @extend_schema(
        parameters=[
            OpenApiParameter(name="invoice", type=str, description="Filter by invoice ID"),
            OpenApiParameter(name="status", type=str, description="Filter by status"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

class ProcurementReportViewSet(viewsets.ViewSet):
    """Aggregated procurement statistics for the reports dashboard."""
    rbac_domain = "procurements"

    # Dashboard aggregation is expensive (12 sums/counts + 6-month window +
    # top-vendor + recent-order scans). Cache for 60s — slight staleness is
    # acceptable for a stats panel and saves the full scan on every poll.
    CACHE_KEY = "procurement_reports_dashboard"
    CACHE_TTL = 60

    def list(self, request):
        from django.core.cache import cache

        cached = cache.get(self.CACHE_KEY)
        if cached is not None:
            return Response(cached)

        payload = self._build(request)
        cache.set(self.CACHE_KEY, payload, self.CACHE_TTL)
        return Response(payload)

    def _build(self, request):
        from datetime import timedelta
        from django.utils import timezone
        from django.db.models import Count

        def total(qs, field):
            return float(qs.aggregate(s=Sum(field))["s"] or Decimal("0"))

        po_qs = PurchaseOrder.objects.all()
        inv_qs = Invoice.objects.all()
        pay_qs = Payment.objects.filter(status="completed")
        contract_qs = ProcurementContract.objects.all()
        req_qs = Requisition.objects.all()
        unpaid_qs = inv_qs.filter(status__in=["pending", "overdue"])
        open_po_qs = po_qs.filter(status__in=["draft", "sent", "acknowledged"])

        stats = {
            "total_spend": total(pay_qs, "amount"),
            "po_count": po_qs.count(),
            "po_open": open_po_qs.count(),
            "po_value": total(po_qs, "total"),
            "invoices_unpaid": unpaid_qs.count(),
            "invoices_unpaid_amount": total(unpaid_qs, "total"),
            "vendor_count": Vendor.objects.count(),
            "requisition_count": req_qs.count(),
            "approval_count": Approval.objects.count(),
            "gr_count": GoodsReceipt.objects.count(),
            "contract_count": contract_qs.count(),
            "contract_value": total(contract_qs, "value"),
        }

        # ── Monthly procurement spend (last 6 months, by PO created_at) ──────
        now = timezone.now()
        months = []
        cursor = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
        starts = []
        for _ in range(6):
            starts.append(cursor)
            # step to first day of previous month
            prev_last = cursor - timedelta(days=1)
            cursor = prev_last.replace(day=1)
        starts.reverse()
        for i, start in enumerate(starts):
            end = starts[i + 1] if i + 1 < len(starts) else (
                now + timedelta(days=1)
            )
            window = po_qs.filter(created_at__gte=start, created_at__lt=end)
            months.append({
                "month": start.strftime("%b"),
                "spend": total(window, "total"),
                "orders": window.count(),
            })

        # ── Top vendors by PO spend ──────────────────────────────────────────
        top_vendors = []
        vendor_rows = (
            po_qs.values("vendor__id", "vendor__name")
            .annotate(orders=Count("id"), spend=Sum("total"))
            .order_by("-spend")[:6]
        )
        for r in vendor_rows:
            top_vendors.append({
                "vendor_id": str(r["vendor__id"]) if r["vendor__id"] else None,
                "vendor_name": r["vendor__name"] or "—",
                "orders": r["orders"],
                "spend": float(r["spend"] or Decimal("0")),
            })

        # ── Recent purchase orders ───────────────────────────────────────────
        recent_orders = [
            {
                "id": str(po.id),
                "order_number": po.order_number,
                "vendor_name": po.vendor.name if po.vendor_id else None,
                "requisition_number": po.requisition.number if po.requisition_id else None,
                "status": po.status,
                "total": float(po.total or Decimal("0")),
                "created_at": po.created_at.isoformat(),
            }
            for po in po_qs.select_related("vendor", "requisition").order_by("-created_at")[:10]
        ]

        # ── PO status breakdown ──────────────────────────────────────────────
        status_breakdown = [
            {"status": r["status"], "count": r["c"]}
            for r in po_qs.values("status").annotate(c=Count("id")).order_by("-c")
        ]

        return {
            "stats": stats,
            "monthly": months,
            "top_vendors": top_vendors,
            "recent_orders": recent_orders,
            "status_breakdown": status_breakdown,
        }
