from django.contrib import messages
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.views import View
from django.views.generic import CreateView, DetailView, ListView, UpdateView
from django.utils import timezone

from apps.core.audit.mixins import log_audit
from apps.core.lists import CsvExportMixin, SortableListMixin, paginate_qs
from apps.core.permissions.mixins import RimsAccessMixin
from apps.core.permissions.roles import UserRole
from apps.rims.finance.models import Budget
from apps.rims.grants import rims_perms
from apps.rims.projects.forms import (
    MilestoneForm,
    MilestoneReviewForm,
    MilestoneSubmitForm,
    ProjectForm,
    _PROJECT_SELECT_CLASS,
)
from apps.rims.projects.models import Deliverable, Milestone, MilestoneAudit, Project
from apps.rims.projects.services import (
    assert_award_accepts_milestone_submission,
    generate_reporting_milestones,
    project_budget_progress,
    project_milestone_compliance,
    refresh_overdue_milestone_states,
)
from django.core.exceptions import ValidationError


class ProjectStaffMixin(RimsAccessMixin):
    allowed_roles = (
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
        UserRole.GRANTS_MANAGER,
        UserRole.FINANCE_OFFICER,
        UserRole.PROGRAM_MANAGER,
        UserRole.PROGRAM_DIRECTOR,
        UserRole.FINANCE_DIRECTOR,
    )
    rims_permissions = (rims_perms.MANAGE_PROJECTS, rims_perms.MANAGE_CALLS, rims_perms.MANAGE_FINANCE)


class MilestoneCaptureMixin(RimsAccessMixin):
    allowed_roles = (
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
        UserRole.GRANTS_MANAGER,
        UserRole.PROGRAM_MANAGER,
    )
    rims_permissions = (rims_perms.MANAGE_PROJECTS, rims_perms.MANAGE_CALLS)


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


class ProjectFormAwardBudgetMixin:
    """HTMX: when grant award changes, reload finance budget choices for that award."""

    def get_form(self, form_class=None):
        form = super().get_form(form_class)
        form.fields["award"].widget.attrs.update(
            {
                "hx-get": reverse("rims_projects:finance_budget_options"),
                "hx-trigger": "change, load",
                "hx-target": "#finance-budget-select",
                "hx-swap": "innerHTML",
            }
        )
        return form


class FinanceBudgetOptionsView(ProjectStaffMixin, View):
    """Return `<select>` HTML for budgets linked to the given award (HTMX fragment)."""

    def get(self, request, *args, **kwargs):
        award_raw = request.GET.get("award")
        budgets = Budget.objects.none()
        if award_raw not in (None, ""):
            try:
                aid = int(award_raw)
            except (TypeError, ValueError):
                aid = None
            if aid is not None:
                budgets = (
                    Budget.objects.filter(award_id=aid)
                    .select_related("funding_source")
                    .order_by("-created_at")
                )
        return TemplateResponse(
            request,
            "projects/partials/finance_budget_select.html",
            {"budgets": budgets, "select_class": _PROJECT_SELECT_CLASS},
        )


class ProjectListView(ProjectStaffMixin, SortableListMixin, CsvExportMixin, ListView):
    model = Project
    template_name = "projects/project_list.html"
    context_object_name = "projects"
    paginate_by = 25

    sortable_fields = {
        "title": "title",
        "status": "status",
        "start": "start_date",
        "end": "end_date",
    }
    default_sort = "-start_date"

    csv_columns = (
        ("Title", "title"),
        ("Status", "get_status_display"),
        ("Lead", lambda p: p.lead.email if p.lead_id else ""),
        ("Start date", lambda p: p.start_date.isoformat() if p.start_date else ""),
        ("End date", lambda p: p.end_date.isoformat() if p.end_date else ""),
    )
    csv_filename = "projects"

    def get_queryset(self):
        qs = Project.objects.select_related("lead", "award__application__applicant")
        q = (self.request.GET.get("q") or "").strip()
        if q:
            qs = qs.filter(
                Q(title__icontains=q)
                | Q(lead__email__icontains=q)
                | Q(lead__first_name__icontains=q)
                | Q(lead__last_name__icontains=q)
            )
        return self.apply_sort(qs)

    def get_template_names(self):
        # Live-search swaps only #project-rows; without this an HTMX request
        # would return the full page and nest it inside the table body.
        if getattr(self.request, "htmx", False):
            return ["projects/partials/project_list_results.html"]
        return super().get_template_names()

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        base = Project.objects.all()
        ctx["project_stats"] = {
            "active": base.filter(status=Project.Status.ACTIVE).count(),
            "at_risk": base.filter(status=Project.Status.AT_RISK).count(),
            "closed": base.filter(status=Project.Status.CLOSED).count(),
            "total": base.count(),
        }
        return ctx


class ProjectCreateView(ProjectFormAwardBudgetMixin, ProjectStaffMixin, CreateView):
    model = Project
    form_class = ProjectForm
    template_name = "projects/project_form.html"

    def form_valid(self, form):
        self.object = form.save(commit=False)
        if self.object.lead_id is None and getattr(self.object.award, "application", None):
            self.object.lead = self.object.award.application.applicant
        self.object.save()
        created = generate_reporting_milestones(self.object)
        if created:
            messages.info(self.request, f"{created} reporting milestone(s) auto-generated from reporting schedule.")
        messages.success(self.request, "Project created.")
        return redirect("rims_projects:project_detail", pk=self.object.pk)


class ProjectDetailView(ProjectStaffMixin, DetailView):
    model = Project
    template_name = "projects/project_detail.html"
    context_object_name = "project"

    def get_queryset(self):
        return Project.objects.select_related(
            "award",
            "finance_budget",
            "finance_budget__funding_source",
            "finance_budget__funding_source__partner",
        ).prefetch_related(
            "milestones",
            "milestones__deliverables",
            "milestones__audit_events",
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        refresh_overdue_milestone_states(self.object)
        self.object.refresh_from_db()
        ctx["compliance"] = project_milestone_compliance(self.object)
        ctx["milestones"] = paginate_qs(
            self.request, self.object.milestones.all(), per_page=25, param="milestone_page"
        )
        ctx["budget_progress"] = project_budget_progress(self.object)
        ctx["milestone_submit_form"] = MilestoneSubmitForm()
        ctx["milestone_review_form"] = MilestoneReviewForm()
        role = getattr(self.request.user, "role", "")
        ctx["can_capture_milestones"] = self.request.user.is_superuser or role in MilestoneCaptureMixin.allowed_roles
        ctx["can_review_milestones"] = self.request.user.is_superuser or role in MilestoneReviewMixin.allowed_roles
        # PRD §5.1 FRFA-AA019 + §4.3.4 — real-time expenditure widget on the
        # project detail page. Only renders when a finance budget is linked;
        # the snapshot pulls live disbursement totals from the finance app.
        if self.object.finance_budget_id is not None:
            try:
                from apps.rims.finance.services import finance_health_snapshot

                ctx["expenditure_snapshot"] = finance_health_snapshot(self.object.finance_budget)
            except Exception:  # pragma: no cover - widget must not break detail
                ctx["expenditure_snapshot"] = None
        else:
            ctx["expenditure_snapshot"] = None
        from apps.rims.grants.ai_helpers import project_risk_snapshot

        ctx["ai_project_risk"] = project_risk_snapshot(self.object)
        return ctx


class ProjectUpdateView(ProjectFormAwardBudgetMixin, ProjectStaffMixin, UpdateView):
    model = Project
    form_class = ProjectForm
    template_name = "projects/project_edit.html"

    def form_valid(self, form):
        self.object = form.save()
        messages.success(self.request, "Project updated.")
        return redirect("rims_projects:project_detail", pk=self.object.pk)


class MilestoneCreateView(MilestoneCaptureMixin, CreateView):
    model = Milestone
    form_class = MilestoneForm
    template_name = "projects/milestone_form.html"

    def dispatch(self, request, *args, **kwargs):
        self.project = get_object_or_404(Project, pk=kwargs["project_pk"])
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["project"] = self.project
        ctx["mode"] = "create"
        return ctx

    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.project = self.project
        self.object.save()
        MilestoneAudit.objects.create(
            milestone=self.object,
            event="created",
            actor=self.request.user,
            payload={"due_date": str(self.object.due_date)},
        )
        log_audit(
            actor=self.request.user,
            action="create",
            target_app="rims_projects",
            target_model="Milestone",
            object_id=self.object.pk,
            object_repr=str(self.object.name),
            changes={"event": "created"},
        )
        messages.success(self.request, "Milestone created.")
        return redirect("rims_projects:project_detail", pk=self.project.pk)


class MilestoneUpdateView(MilestoneCaptureMixin, UpdateView):
    model = Milestone
    form_class = MilestoneForm
    template_name = "projects/milestone_form.html"
    pk_url_kwarg = "milestone_pk"

    def dispatch(self, request, *args, **kwargs):
        self.project = get_object_or_404(Project, pk=kwargs["project_pk"])
        return super().dispatch(request, *args, **kwargs)

    def get_queryset(self):
        return Milestone.objects.filter(project=self.project)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["project"] = self.project
        ctx["mode"] = "edit"
        return ctx

    def form_valid(self, form):
        self.object = form.save()
        MilestoneAudit.objects.create(
            milestone=self.object,
            event="updated",
            actor=self.request.user,
            payload={"completion_percent": self.object.completion_percent},
        )
        log_audit(
            actor=self.request.user,
            action="update",
            target_app="rims_projects",
            target_model="Milestone",
            object_id=self.object.pk,
            object_repr=str(self.object.name),
            changes={"event": "updated"},
        )
        messages.success(self.request, "Milestone updated.")
        return redirect("rims_projects:project_detail", pk=self.project.pk)


class MilestoneSubmitView(MilestoneCaptureMixin, View):
    def post(self, request, project_pk, milestone_pk):
        project = get_object_or_404(Project, pk=project_pk)
        milestone = get_object_or_404(Milestone, pk=milestone_pk, project=project)
        try:
            assert_award_accepts_milestone_submission(project)
        except ValidationError as exc:
            messages.error(request, " ".join(getattr(exc, "messages", [str(exc)])))
            return redirect("rims_projects:project_detail", pk=project.pk)
        form = MilestoneSubmitForm(request.POST, request.FILES)
        if not form.is_valid():
            messages.error(request, "Provide a valid summary and completion percentage.")
            return redirect("rims_projects:project_detail", pk=project.pk)
        milestone.report_summary = form.cleaned_data["report_summary"]
        milestone.completion_percent = form.cleaned_data["completion_percent"]
        milestone.status = Milestone.Status.SUBMITTED
        milestone.report_submitted_at = timezone.now()
        milestone.submitted_by = request.user
        milestone.save(
            update_fields=[
                "report_summary",
                "completion_percent",
                "status",
                "report_submitted_at",
                "submitted_by",
            ]
        )
        deliverable_title = (form.cleaned_data.get("deliverable_title") or "").strip()
        deliverable_notes = (form.cleaned_data.get("deliverable_notes") or "").strip()
        deliverable_file = form.cleaned_data.get("deliverable_file")
        if deliverable_title or deliverable_notes or deliverable_file:
            Deliverable.objects.create(
                milestone=milestone,
                title=deliverable_title or "Milestone evidence",
                notes=deliverable_notes,
                file=deliverable_file,
            )
        MilestoneAudit.objects.create(
            milestone=milestone,
            event="submitted",
            actor=request.user,
            payload={"completion_percent": milestone.completion_percent},
        )
        log_audit(
            actor=request.user,
            action="update",
            target_app="rims_projects",
            target_model="Milestone",
            object_id=milestone.pk,
            object_repr=str(milestone.name),
            changes={"event": "submitted"},
        )
        messages.success(request, "Milestone report submitted for review.")
        return redirect("rims_projects:project_detail", pk=project.pk)


class MilestoneReviewView(MilestoneReviewMixin, View):
    def post(self, request, project_pk, milestone_pk):
        project = get_object_or_404(Project, pk=project_pk)
        milestone = get_object_or_404(Milestone, pk=milestone_pk, project=project)
        form = MilestoneReviewForm(request.POST)
        if not form.is_valid():
            messages.error(request, "Review decision is required.")
            return redirect("rims_projects:project_detail", pk=project.pk)
        old_status = milestone.status
        milestone.status = form.cleaned_data["decision"]
        milestone.review_comment = (form.cleaned_data.get("comment") or "").strip()
        milestone.reviewed_by = request.user
        milestone.reviewed_at = timezone.now()
        milestone.save(update_fields=["status", "review_comment", "reviewed_by", "reviewed_at"])
        MilestoneAudit.objects.create(
            milestone=milestone,
            event="reviewed",
            actor=request.user,
            payload={"old_status": old_status, "new_status": milestone.status},
        )
        log_audit(
            actor=request.user,
            action="update",
            target_app="rims_projects",
            target_model="Milestone",
            object_id=milestone.pk,
            object_repr=str(milestone.name),
            changes={"old_status": old_status, "new_status": milestone.status},
        )
        messages.success(request, "Milestone review decision saved.")
        response = redirect("rims_projects:project_detail", pk=project.pk)
        if milestone.status == Milestone.Status.ACCEPTED:
            response["HX-Trigger"] = "milestoneAccepted"
        return response


class BudgetProgressPartialView(ProjectStaffMixin, View):
    """HTMX partial: refreshable budget progress for a project."""

    def get(self, request, pk):
        project = get_object_or_404(
            Project.objects.prefetch_related("milestones", "budget_lines"),
            pk=pk,
        )
        return TemplateResponse(
            request,
            "projects/partials/budget_progress.html",
            {"project": project, "budget_progress": project_budget_progress(project)},
        )


class MilestoneComplianceView(ProjectStaffMixin, SortableListMixin, CsvExportMixin, ListView):
    model = Project
    template_name = "projects/milestone_compliance.html"
    context_object_name = "projects"
    paginate_by = 30

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

    csv_columns = (
        ("Project", "title"),
        ("Status", "get_status_display"),
        ("Award", lambda p: str(p.award) if p.award_id else ""),
        ("Updated", lambda p: p.updated_at.isoformat() if p.updated_at else ""),
    )
    csv_filename = "milestone_compliance"

    def get_queryset(self):
        qs = (
            Project.objects.select_related("award__application__applicant", "award__application__call")
            .prefetch_related("milestones")
        )
        return self.apply_sort(qs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        rows = []
        for project in ctx["projects"]:
            refresh_overdue_milestone_states(project)
            rows.append({"project": project, "compliance": project_milestone_compliance(project)})
        ctx["rows"] = rows
        return ctx
