"""Funding record (grant planning) UI — PRD §5.1.1 parent of calls.

PRD coverage map (FRFA-GPS001–027):

- ``FundingRecordCreateView`` — GPS001 (initialise grant record), GPS003 (draft
  state), GPS005 (donor & funding source), GPS010–013 (budget structure via
  ``GrantBudgetLineFormSet``), GPS017 (programme / finance / grant-manager
  staff assignment), GPS019 (notify assigned staff via downstream signals).
- ``FundingRecordUpdateView`` — GPS024 (revise after rejection while still in
  draft), GPS027 (controlled edit window — only ``DRAFT`` and
  ``DRAFT_REVISION_REQUIRED`` may edit).
- ``FundingSubmitForApprovalView`` — GPS018 (assignment gate enforced in
  ``submit_funding_for_approval``), GPS020 (mandatory completeness check),
  GPS021 (route to approver) and GPS026 (versioning on transitions handled in
  ``apps.rims.grants.funding``).
- ``FundingApproveView`` — GPS002 (issue Grant ID via
  ``approve_funding_record``), GPS023 (mark approved + notify), GPS025 (audit
  log on approval).
- ``FundingRejectView`` — GPS024 (return to revision with comment).
- All views log via ``audit_rims`` to satisfy GPS025 + NFRFA011 (system audit).

See ``apps/rims/grants/PRD_MAPPING.md`` for the full ID map across this module.
"""

from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.views import View
from django.views.generic import CreateView, DetailView, ListView, UpdateView

from django.db.models import Count, Q

from apps.core.lists import CsvExportMixin, SortableListMixin
from apps.core.permissions.mixins import RimsAccessMixin
from apps.core.permissions.roles import UserRole
from apps.rims.grants import rims_perms
from apps.rims.grants.audit_helper import action_create, action_update, audit_rims
from apps.rims.grants.forms import FundingRecordForm, GrantBudgetLineFormSet
from apps.rims.grants.funding import (
    approve_funding_record,
    reject_funding_record,
    submit_funding_for_approval,
    uncommitted_balance,
)
from apps.rims.grants.models import FundingRecord, FundingRecordVersion


_FUNDING_RECORD_WIZARD_STEPS = [
    {"n": 1, "title": "Identity & amount"},
    {"n": 2, "title": "Objectives & reporting"},
    {"n": 3, "title": "Roles"},
    {"n": 4, "title": "Budget lines"},
]


class FundingManagerMixin(RimsAccessMixin):
    allowed_roles = (UserRole.ADMIN, UserRole.GRANTS_MANAGER)
    rims_permissions = (rims_perms.MANAGE_CALLS,)


class FundingApproveMixin(RimsAccessMixin):
    allowed_roles = (
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
        UserRole.PROGRAM_DIRECTOR,
        UserRole.FINANCE_DIRECTOR,
    )
    rims_permissions = (rims_perms.APPROVE_GRANTS,)


class FundingReadMixin(RimsAccessMixin):
    allowed_roles = (
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
        UserRole.GRANTS_MANAGER,
        UserRole.PROGRAM_DIRECTOR,
        UserRole.FINANCE_DIRECTOR,
    )
    rims_permissions = (rims_perms.MANAGE_CALLS, rims_perms.APPROVE_GRANTS)


class FundingRecordListView(FundingManagerMixin, SortableListMixin, CsvExportMixin, ListView):
    model = FundingRecord
    template_name = "grants/funding_list.html"
    context_object_name = "records"
    paginate_by = 25

    sortable_fields = {
        "title": "title",
        "status": "status",
        "amount": "amount",
        "created": "created_at",
    }
    default_sort = "-created_at"

    csv_columns = (
        ("Public ID", "public_grant_id"),
        ("Title", "title"),
        ("Status", "get_status_display"),
        ("Amount", "amount"),
        ("Currency", "currency"),
        ("Partner", "partner.name"),
        ("Created", lambda r: r.created_at.isoformat() if r.created_at else ""),
    )
    csv_filename = "funding_records"

    FACET_STATUSES = (
        ("draft", "Draft"),
        ("pending_approval", "Pending approval"),
        ("draft_revision_required", "Revision required"),
        ("approved", "Approved"),
        ("closed", "Closed"),
    )

    def get_queryset(self):
        qs = FundingRecord.objects.select_related("partner", "grant_manager")
        self._status_filter = (self.request.GET.get("status") or "").strip()
        if self._status_filter:
            qs = qs.filter(status=self._status_filter)
        self._search = (self.request.GET.get("q") or "").strip()
        if self._search:
            qs = qs.filter(
                Q(public_grant_id__icontains=self._search) | Q(title__icontains=self._search)
            )
        return self.apply_sort(qs)

    def get_template_names(self):
        if getattr(self.request, "htmx", False):
            return ["grants/partials/funding_list_results.html"]
        return super().get_template_names()

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        base_qs = FundingRecord.objects.all()
        counts = {row["status"]: row["n"] for row in base_qs.values("status").annotate(n=Count("id"))}
        active = getattr(self, "_status_filter", "") or ""
        facets = [{
            "label": "All",
            "href": self.request.path,
            "count": base_qs.count(),
            "active": not active,
        }]
        for value, label in self.FACET_STATUSES:
            facets.append({
                "label": label,
                "href": f"{self.request.path}?status={value}",
                "count": counts.get(value, 0),
                "active": active == value,
            })
        ctx["facets"] = facets
        ctx["search_value"] = getattr(self, "_search", "")
        return ctx


class FundingRecordVersionListView(FundingReadMixin, ListView):
    """PRD GPS026 — show version history of a funding record so approvers
    reviewing a resubmission can see what changed across versions."""

    model = FundingRecordVersion
    template_name = "grants/funding_version_list.html"
    context_object_name = "versions"

    def get_queryset(self):
        return FundingRecordVersion.objects.filter(
            funding_record_id=self.kwargs["pk"]
        ).select_related("created_by").order_by("-version_number")

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["funding_record"] = get_object_or_404(FundingRecord, pk=self.kwargs["pk"])
        versions = list(ctx["versions"])
        # Compute changed top-level keys vs the immediately prior version (chronologically).
        rows = []
        ordered = sorted(versions, key=lambda v: v.version_number)
        prior_snapshot = {}
        for v in ordered:
            current = v.snapshot or {}
            keys = set(prior_snapshot) | set(current)
            changed = sorted(k for k in keys if prior_snapshot.get(k) != current.get(k))
            rows.append({"version": v, "changed_keys": changed})
            prior_snapshot = current
        rows.reverse()
        ctx["version_rows"] = rows
        return ctx


class FundingApprovalQueueView(FundingApproveMixin, SortableListMixin, ListView):
    """PRD GPS021–022 — pending funding records awaiting director approval."""

    model = FundingRecord
    template_name = "grants/funding_approval_queue.html"
    context_object_name = "records"
    paginate_by = 25

    sortable_fields = {
        "updated": "updated_at",
        "amount": "amount",
        "title": "title",
    }
    default_sort = "-updated_at"

    def get_queryset(self):
        qs = (
            FundingRecord.objects.filter(status=FundingRecord.Status.PENDING_APPROVAL)
            .select_related(
                "partner", "grant_manager", "program_lead", "finance_officer", "created_by"
            )
        )
        return self.apply_sort(qs)

    def get_context_data(self, **kwargs):
        from decimal import Decimal

        ctx = super().get_context_data(**kwargs)
        pending = FundingRecord.objects.filter(
            status=FundingRecord.Status.PENDING_APPROVAL
        )
        total = sum((r.amount or Decimal("0") for r in pending), Decimal("0"))
        ctx["queue_stats"] = {
            "pending_count": pending.count(),
            "pending_value": total,
            "approved_total": FundingRecord.objects.filter(
                status=FundingRecord.Status.APPROVED
            ).count(),
        }
        return ctx


class FundingRecordCreateView(FundingManagerMixin, CreateView):
    model = FundingRecord
    form_class = FundingRecordForm
    template_name = "grants/funding_form.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        inst = getattr(self, "object", None)
        ctx["budget_formset"] = kwargs.get(
            "budget_formset",
            GrantBudgetLineFormSet(
                self.request.POST or None,
                self.request.FILES or None,
                instance=inst,
            ),
        )
        ctx["form_title"] = "New funding record"
        ctx["wizard_steps"] = _FUNDING_RECORD_WIZARD_STEPS
        ctx["cancel_url"] = reverse("rims_grants:funding_list")
        return ctx

    def form_invalid(self, form):
        return self.render_to_response(
            self.get_context_data(form=form, budget_formset=GrantBudgetLineFormSet(self.request.POST))
        )

    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.created_by = self.request.user
        self.object.save()
        form.save_m2m()
        fs = GrantBudgetLineFormSet(self.request.POST, self.request.FILES, instance=self.object)
        if not fs.is_valid():
            self.object.delete()
            return self.form_invalid(form)
        fs.save()
        try:
            self.object.full_clean()
        except ValidationError as exc:
            self.object.delete()
            for msg in exc.messages:
                form.add_error(None, msg)
            return self.form_invalid(form)
        audit_rims(
            actor=self.request.user,
            action=action_create(),
            target_model="FundingRecord",
            object_id=self.object.pk,
            object_repr=str(self.object),
            changes={"title": self.object.title, "amount": str(self.object.amount)},
            request=self.request,
        )
        messages.success(self.request, "Funding record saved as draft.")
        return redirect("rims_grants:funding_detail", pk=self.object.pk)


class FundingRecordDetailView(FundingReadMixin, DetailView):
    model = FundingRecord
    template_name = "grants/funding_detail.html"
    context_object_name = "funding"

    def get_queryset(self):
        return FundingRecord.objects.select_related(
            "partner", "grant_manager", "finance_officer", "program_lead", "created_by"
        ).prefetch_related("budget_lines", "calls")

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        f = self.object
        ctx["uncommitted"] = uncommitted_balance(f)
        ctx["linked_calls"] = f.calls.all().order_by("-created_at")[:20]
        ctx["reporting_calendar"] = list(f.reporting_calendar.all())
        from apps.rims.grants.ai_helpers import funding_rule_advisor

        ctx["ai_funding_advisor"] = funding_rule_advisor(f)

        # PRD §5.1 FRFA-CO012/013 — total residuals returned at close-out across
        # all closed child awards. Lets auditors see the funds released back to
        # the parent funding pool.
        from decimal import Decimal as _D

        from apps.rims.grants.models import Award

        total_residual = _D("0")
        residual_award_count = 0
        for award in Award.objects.filter(
            application__call__funding_record=f,
            status=Award.Status.CLOSED,
            closeout_record__isnull=False,
        ).select_related("closeout_record"):
            snap = award.closeout_record.financial_snapshot or {}
            raw = snap.get("residual_returned")
            if raw:
                try:
                    total_residual += _D(str(raw))
                    residual_award_count += 1
                except Exception:  # noqa: BLE001 — best-effort aggregation.
                    continue
        ctx["total_residual_returned"] = total_residual
        ctx["residual_award_count"] = residual_award_count
        return ctx


class FundingRecordUpdateView(FundingManagerMixin, UpdateView):
    model = FundingRecord
    form_class = FundingRecordForm
    template_name = "grants/funding_form.html"
    pk_url_kwarg = "pk"

    def get_queryset(self):
        return FundingRecord.objects.exclude(status=FundingRecord.Status.CLOSED)

    def get_success_url(self):
        return reverse_lazy("rims_grants:funding_detail", kwargs={"pk": self.object.pk})

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["budget_formset"] = kwargs.get(
            "budget_formset",
            GrantBudgetLineFormSet(
                self.request.POST or None,
                self.request.FILES or None,
                instance=self.object,
            ),
        )
        ctx["form_title"] = "Edit funding record"
        ctx["wizard_steps"] = _FUNDING_RECORD_WIZARD_STEPS
        ctx["cancel_url"] = reverse(
            "rims_grants:funding_detail", kwargs={"pk": self.object.pk}
        )
        return ctx

    def form_invalid(self, form):
        return self.render_to_response(
            self.get_context_data(
                form=form,
                budget_formset=GrantBudgetLineFormSet(self.request.POST, instance=self.object),
            )
        )

    @transaction.atomic
    def form_valid(self, form):
        if self.object.status not in (
            FundingRecord.Status.DRAFT,
            FundingRecord.Status.DRAFT_REVISION_REQUIRED,
        ):
            messages.error(self.request, "Only draft funding records can be edited.")
            return redirect("rims_grants:funding_detail", pk=self.object.pk)
        self.object = form.save()
        fs = GrantBudgetLineFormSet(self.request.POST, self.request.FILES, instance=self.object)
        if not fs.is_valid():
            transaction.set_rollback(True)
            return self.form_invalid(form)
        fs.save()
        try:
            self.object.full_clean()
        except ValidationError as exc:
            transaction.set_rollback(True)
            for msg in exc.messages:
                form.add_error(None, msg)
            return self.form_invalid(form)
        audit_rims(
            actor=self.request.user,
            action=action_update(),
            target_model="FundingRecord",
            object_id=self.object.pk,
            object_repr=str(self.object),
            changes={"updated": True},
            request=self.request,
        )
        messages.success(self.request, "Funding record updated.")
        return redirect(self.get_success_url())


class FundingSubmitForApprovalView(FundingManagerMixin, View):
    """POST-only — transition draft → pending."""

    def post(self, request, pk):
        funding = get_object_or_404(
            FundingRecord.objects.exclude(status=FundingRecord.Status.CLOSED),
            pk=pk,
        )
        try:
            submit_funding_for_approval(funding)
        except ValidationError as exc:
            messages.error(request, " ".join(getattr(exc, "messages", [str(exc)])))
            return redirect("rims_grants:funding_detail", pk=funding.pk)
        audit_rims(
            actor=request.user,
            action=action_update(),
            target_model="FundingRecord",
            object_id=funding.pk,
            object_repr=str(funding),
            changes={"status": funding.status},
            request=request,
        )
        messages.success(request, "Submitted for approval.")
        return redirect("rims_grants:funding_detail", pk=funding.pk)


class FundingApproveView(FundingApproveMixin, View):
    def post(self, request, pk):
        funding = get_object_or_404(FundingRecord, pk=pk)
        try:
            approve_funding_record(funding)
        except ValidationError as exc:
            messages.error(request, " ".join(getattr(exc, "messages", [str(exc)])))
            return redirect("rims_grants:funding_detail", pk=funding.pk)
        audit_rims(
            actor=request.user,
            action=action_update(),
            target_model="FundingRecord",
            object_id=funding.pk,
            object_repr=str(funding),
            changes={"status": "approved", "public_grant_id": funding.public_grant_id},
            request=request,
        )
        messages.success(request, "Funding record approved.")
        return redirect("rims_grants:funding_detail", pk=funding.pk)


class FundingRejectView(FundingApproveMixin, View):
    def post(self, request, pk):
        funding = get_object_or_404(FundingRecord, pk=pk)
        comment = (request.POST.get("comment") or "").strip()
        try:
            reject_funding_record(funding, comment)
        except ValidationError as exc:
            messages.error(request, " ".join(getattr(exc, "messages", [str(exc)])))
            return redirect("rims_grants:funding_detail", pk=funding.pk)
        audit_rims(
            actor=request.user,
            action=action_update(),
            target_model="FundingRecord",
            object_id=funding.pk,
            object_repr=str(funding),
            changes={"status": "draft_revision_required"},
            request=request,
        )
        messages.success(request, "Funding record returned for revision.")
        return redirect("rims_grants:funding_detail", pk=funding.pk)
