from __future__ import annotations

import json

from django.contrib import messages
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views import View
from django.views.generic import CreateView, DetailView, FormView, ListView, UpdateView

from apps.core.permissions.mixins import RoleRequiredMixin
from apps.core.permissions.roles import UserRole
from apps.mel.reports.forms import (
    ReportDistributionWebhookForm,
    ReportGenerateForm,
    ReportNarrativeForm,
    ReportTemplateForm,
)
from apps.mel.reports.models import (
    ComplianceAlert,
    ComplianceSeverity,
    Report,
    ReportArtifact,
    ReportDistributionWebhook,
    ReportSection,
    ReportSectionKind,
    ReportStatus,
    ReportTemplate,
    ReportWebhookDelivery,
    ScheduledReport,
    ScheduleFrequency,
)
from apps.mel.reports.services import (
    distribute_report,
    generate_report,
    publish_report,
    render_all,
    transition_report,
    unpublish_report,
)

OFFICER_ROLES = (UserRole.MEL_OFFICER, UserRole.ADMIN, UserRole.SYSTEM_ADMIN)
VIEW_ROLES = OFFICER_ROLES + (
    UserRole.PROGRAM_MANAGER,
    UserRole.PROGRAM_DIRECTOR,
    UserRole.GRANTS_MANAGER,
)


# ---------------------------------------------------------------------------
# Report templates
# ---------------------------------------------------------------------------


class TemplateListView(RoleRequiredMixin, ListView):
    allowed_roles = VIEW_ROLES
    model = ReportTemplate
    context_object_name = "templates"
    paginate_by = 10
    template_name = "reports/template_list.html"


_REPORT_TEMPLATE_WIZARD_STEPS = [
    {"n": 1, "title": "Identity"},
    {"n": 2, "title": "Scope & sections"},
    {"n": 3, "title": "Schedule & distribution"},
]


class _TemplateFormContextMixin:
    """Pass the saved sections list as JSON so the Alpine section-picker
    can hydrate. ``form.sections.value()`` returns the raw JSONField value;
    we re-encode it as JSON for safe interpolation in ``x-data``.
    """

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        sections_raw = ctx["form"].instance.sections if ctx["form"].instance.pk else []
        if not isinstance(sections_raw, list):
            sections_raw = []
        ctx["sections_initial"] = json.dumps(sections_raw)
        ctx["wizard_steps"] = _REPORT_TEMPLATE_WIZARD_STEPS
        ctx["cancel_url"] = reverse("mel_reports:template_list")
        return ctx


class TemplateCreateView(_TemplateFormContextMixin, RoleRequiredMixin, CreateView):
    allowed_roles = OFFICER_ROLES
    model = ReportTemplate
    form_class = ReportTemplateForm
    template_name = "reports/template_form.html"
    success_url = reverse_lazy("mel_reports:template_list")


class TemplateUpdateView(_TemplateFormContextMixin, RoleRequiredMixin, UpdateView):
    allowed_roles = OFFICER_ROLES
    model = ReportTemplate
    form_class = ReportTemplateForm
    slug_field = "slug"
    slug_url_kwarg = "slug"
    template_name = "reports/template_form.html"
    success_url = reverse_lazy("mel_reports:template_list")


# ---------------------------------------------------------------------------
# Reports
# ---------------------------------------------------------------------------


class ReportListView(RoleRequiredMixin, ListView):
    allowed_roles = VIEW_ROLES
    model = Report
    context_object_name = "reports"
    paginate_by = 10
    template_name = "reports/report_list.html"

    def get_queryset(self):
        qs = Report.objects.select_related("template").order_by("-generated_at")
        status = self.request.GET.get("status", "").strip()
        if status:
            qs = qs.filter(status=status)
        return qs

    def get_context_data(self, **kwargs):
        from django.db.models import Count, Q

        ctx = super().get_context_data(**kwargs)
        ctx["status_stats"] = Report.objects.aggregate(
            draft=Count("pk", filter=Q(status=ReportStatus.DRAFT)),
            in_review=Count("pk", filter=Q(status=ReportStatus.IN_REVIEW)),
            approved=Count("pk", filter=Q(status=ReportStatus.APPROVED)),
            published=Count("pk", filter=Q(status=ReportStatus.PUBLISHED)),
        )
        ctx["selected_status"] = self.request.GET.get("status", "")
        return ctx


class ReportDetailView(RoleRequiredMixin, DetailView):
    allowed_roles = VIEW_ROLES
    model = Report
    context_object_name = "report"
    template_name = "reports/report_detail.html"

    def get_context_data(self, **kwargs):
        from apps.mel.reports.models import REPORT_STATUS_TRANSITIONS

        ctx = super().get_context_data(**kwargs)
        report = ctx["report"]
        # C-D8 — expose the valid step-wise transitions so reviewers can walk
        # draft → in_review → approved without the one-click publish. PUBLISHED
        # is excluded here because publishing also distributes (its own button).
        status_labels = dict(ReportStatus.choices)
        allowed = REPORT_STATUS_TRANSITIONS.get(report.status, set())
        ctx["allowed_transitions"] = [
            {"value": s, "label": status_labels.get(s, s)}
            for s in (ReportStatus.IN_REVIEW, ReportStatus.APPROVED, ReportStatus.DRAFT, ReportStatus.ARCHIVED)
            if s in allowed
        ]
        # C-D7 — surface the report-review feedback channel (seeded on publish)
        # so stakeholders have a visible path to respond.
        ctx["feedback_channel"] = _report_feedback_channel(report)
        # Whether the current user may drive transitions (officer tier only).
        ctx["can_transition"] = getattr(self.request.user, "role", "") in OFFICER_ROLES
        return ctx


def _report_feedback_channel(report):
    """Return the published report's review FeedbackChannel, or None.

    The channel is seeded by the feedback ``mel_report_published`` receiver
    (slug ``report-<pk>``). Read-only lookup — reports never mutate it.
    """
    if report.status != ReportStatus.PUBLISHED:
        return None
    try:
        from apps.mel.feedback.models import FeedbackChannel

        return FeedbackChannel.objects.filter(
            slug=f"report-{report.pk}", is_active=True,
        ).first()
    except Exception:  # pragma: no cover - feedback app optional
        return None


class ReportDashboardView(RoleRequiredMixin, DetailView):
    allowed_roles = VIEW_ROLES
    model = Report
    context_object_name = "report"
    template_name = "reports/report_dashboard.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["data"] = ctx["report"].consolidated_data or {}
        return ctx


class ReportGenerateView(RoleRequiredMixin, FormView):
    allowed_roles = OFFICER_ROLES
    form_class = ReportGenerateForm
    template_name = "reports/report_generate.html"

    def get_context_data(self, **kwargs):
        from apps.mel.indicators.services import distinct_period_labels

        ctx = super().get_context_data(**kwargs)
        ctx["period_options"] = distinct_period_labels()
        return ctx

    def form_valid(self, form):
        report = generate_report(
            template=form.cleaned_data["template"],
            period_label=form.cleaned_data["period_label"],
            period_start=form.cleaned_data["period_start"],
            period_end=form.cleaned_data["period_end"],
            user=self.request.user,
            narrative=form.cleaned_data.get("narrative", ""),
        )
        render_all(report)
        messages.success(self.request, f"Report generated: {report}")
        return HttpResponseRedirect(
            reverse("mel_reports:report_detail", kwargs={"pk": report.pk})
        )


_LOCKED_REPORT_STATUSES = (ReportStatus.PUBLISHED, ReportStatus.ARCHIVED)


class ReportNarrativeView(RoleRequiredMixin, UpdateView):
    allowed_roles = OFFICER_ROLES
    model = Report
    form_class = ReportNarrativeForm
    template_name = "reports/report_narrative_form.html"

    def dispatch(self, request, *args, **kwargs):
        # C-D2 — the pre-save guard raises ValidationError (→ HTTP 500) when a
        # PUBLISHED/ARCHIVED report is edited. Block the edit up front with a
        # friendly message and redirect instead of attempting the save.
        self.object = self.get_object()
        if self.object.status in _LOCKED_REPORT_STATUSES:
            messages.error(
                request,
                "This report is locked while "
                f"{self.object.get_status_display().lower()}. "
                "Unpublish it first to edit the narrative.",
            )
            return HttpResponseRedirect(
                reverse("mel_reports:report_detail", kwargs={"pk": self.object.pk})
            )
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return reverse("mel_reports:report_detail", kwargs={"pk": self.object.pk})


class ReportPublishView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        from apps.mel.reports.services import report_publish_blockers

        report = get_object_or_404(Report, pk=pk)
        # M&E SRS (E1) — do not finalize when mandatory information is missing.
        blockers = report_publish_blockers(report)
        if blockers:
            # M&E SRS Table 65 (A2) — when evidence is what's missing, notify
            # the responsible officers so they can upload it.
            from apps.mel.indicators.services import (
                evidence_gaps,
                notify_evidence_required,
            )

            scope = report.template.logframe if report.template_id else None
            gaps = evidence_gaps(scope, report.period_label)
            if gaps:
                notify_evidence_required(gaps)
            messages.error(
                request, "Cannot publish — " + " ".join(blockers)
            )
            return HttpResponseRedirect(
                reverse("mel_reports:report_detail", kwargs={"pk": report.pk})
            )
        publish_report(report)
        queued = distribute_report(report)
        if queued:
            messages.success(
                request,
                f"Report published and distributed to {queued} recipient{'' if queued == 1 else 's'}.",
            )
        else:
            messages.success(
                request,
                "Report published (no recipients configured — add default recipients on the template to distribute).",
            )
        return HttpResponseRedirect(
            reverse("mel_reports:report_detail", kwargs={"pk": report.pk})
        )


class ReportUnpublishView(RoleRequiredMixin, View):
    """POST endpoint that bounces a PUBLISHED report back to APPROVED with a logged reason (G14)."""

    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        report = get_object_or_404(Report, pk=pk)
        reason = (request.POST.get("reason") or "").strip()
        try:
            unpublish_report(report, user=request.user, reason=reason)
        except Exception as exc:
            messages.error(request, str(exc))
        else:
            messages.success(
                request,
                "Report unpublished — edits unlocked. Republish when corrections are ready.",
            )
        return HttpResponseRedirect(
            reverse("mel_reports:report_detail", kwargs={"pk": report.pk})
        )


class ReportTransitionView(RoleRequiredMixin, View):
    """POST endpoint to move a Report through its status matrix."""

    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        report = get_object_or_404(Report, pk=pk)
        new_status = request.POST.get("status", "").strip()
        valid = {c for c, _ in ReportStatus.choices}
        if new_status not in valid:
            messages.error(request, f"Unknown status '{new_status}'.")
        else:
            try:
                transition_report(report, new_status)
                messages.success(request, f"Report transitioned to {new_status}.")
            except Exception as exc:
                messages.error(request, str(exc))
        return HttpResponseRedirect(
            reverse("mel_reports:report_detail", kwargs={"pk": report.pk})
        )


class ReportArtifactDownloadView(RoleRequiredMixin, View):
    allowed_roles = VIEW_ROLES

    def get(self, request, pk: int, fmt: str):
        try:
            artifact = ReportArtifact.objects.get(report_id=pk, format=fmt)
        except ReportArtifact.DoesNotExist as exc:
            raise Http404 from exc
        if not artifact.file:
            raise Http404
        content_types = {
            "pdf": "application/pdf",
            "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            "word": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            "csv": "text/csv",
            "html": "text/html",
        }
        extensions = {"word": "docx"}
        extension = extensions.get(fmt, fmt)
        response = HttpResponse(
            artifact.file.read(),
            content_type=content_types.get(fmt, "application/octet-stream"),
        )
        response["Content-Disposition"] = (
            f'attachment; filename="{artifact.report.template.slug}-{artifact.report.period_label}.{extension}"'
        )
        return response


class PublicReportView(View):
    """Anonymous read-only view of a published report via share_token.

    Token is a 64-char URL-safe random value generated on first publish.
    Only PUBLISHED reports are accessible; revoke by clearing ``share_token``
    via the officer-only ReportShareTokenView with action=revoke.
    """

    def get(self, request, token: str):
        report = Report.objects.filter(
            share_token=token, status=ReportStatus.PUBLISHED,
        ).select_related("template").first()
        if not report or not token:
            raise Http404("Report not found or no longer shared.")
        artifacts = list(report.artifacts.all())
        from django.shortcuts import render as _render

        return _render(
            request,
            "reports/public_report.html",
            {
                "report": report,
                "data": report.consolidated_data or {},
                "artifacts": artifacts,
                "share_token": token,
            },
        )


class ReportShareTokenView(RoleRequiredMixin, View):
    """POST-only endpoint to generate / rotate / revoke the share token on a Report."""

    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        report = get_object_or_404(Report, pk=pk)
        if report.status != ReportStatus.PUBLISHED:
            messages.error(request, "Only published reports can be shared.")
            return HttpResponseRedirect(reverse("mel_reports:report_detail", args=[pk]))
        action = (request.POST.get("action") or "rotate").lower()
        if action == "revoke":
            report.share_token = ""
            report.save(update_fields=["share_token"])
            messages.success(request, "Share link revoked.")
        else:
            report.share_token = Report.generate_share_token()
            report.save(update_fields=["share_token"])
            messages.success(request, "Share link generated.")
        return HttpResponseRedirect(reverse("mel_reports:report_detail", args=[pk]))


# ---------------------------------------------------------------------------
# SME-Hub × M&EL reports (FRSME-MEI014–016, MEI020, MEI021)
# ---------------------------------------------------------------------------


from django.views.generic import TemplateView  # noqa: E402

from apps.core.audit.models import AuditLog  # noqa: E402

_SMEHUB_REPORT_ROLES = OFFICER_ROLES + (UserRole.SME_ADMIN,)


_XLSX_CONTENT_TYPE = (
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)


class SMEHubDonorReportView(RoleRequiredMixin, TemplateView):
    """Donor-facing rollup of funding-secured by sector / cohort / AIH /
    period (FRSME-MEI014–016). Uses the existing ``AuditableMixin``-style
    AuditLog write to record report-generation events (FRSME-MEI022).

    Phase 8 — accepts ``?format=xlsx`` and ``?format=pdf`` query params
    to download artefacts (FRSME-MEI020/021 export closure).
    """

    allowed_roles = _SMEHUB_REPORT_ROLES
    template_name = "reports/smehub_donor_report.html"

    def _resolve_period(self):
        from datetime import date as _date

        period_start = self.request.GET.get("period_start") or None
        period_end = self.request.GET.get("period_end") or None
        ps = _date.fromisoformat(period_start) if period_start else None
        pe = _date.fromisoformat(period_end) if period_end else None
        return period_start, period_end, ps, pe

    def _audit(self, period_start, period_end, fmt):
        AuditLog.objects.create(
            actor=self.request.user if self.request.user.is_authenticated else None,
            action=AuditLog.Action.ACCESS,
            target_app="mel_reports",
            target_model="SMEHubDonorReport",
            object_id="smehub_donor",
            object_repr=f"smehub_donor period={period_start or '∅'}→{period_end or '∅'} fmt={fmt}",
            changes={"period_start": period_start, "period_end": period_end, "format": fmt},
        )

    def get(self, request, *args, **kwargs):
        from django.http import HttpResponse

        from apps.mel.reports.builders import build_smehub_donor_report
        from apps.mel.reports.smehub_renderers import (
            render_smehub_donor_pdf,
            render_smehub_donor_xlsx,
        )

        fmt = (request.GET.get("format") or "html").lower()
        period_start, period_end, ps, pe = self._resolve_period()
        report = build_smehub_donor_report(period_start=ps, period_end=pe)

        if fmt == "xlsx":
            self._audit(period_start, period_end, "xlsx")
            return HttpResponse(
                render_smehub_donor_xlsx(report),
                content_type=_XLSX_CONTENT_TYPE,
                headers={"Content-Disposition": 'attachment; filename="smehub-donor-report.xlsx"'},
            )
        if fmt == "pdf":
            self._audit(period_start, period_end, "pdf")
            return HttpResponse(
                render_smehub_donor_pdf(report),
                content_type="application/pdf",
                headers={"Content-Disposition": 'attachment; filename="smehub-donor-report.pdf"'},
            )

        self._audit(period_start, period_end, "html")
        ctx = self.get_context_data(
            report=report, period_start=period_start or "", period_end=period_end or "",
        )
        return self.render_to_response(ctx)

    def get_context_data(self, **kwargs):
        # Used only on the HTML path; values are passed in via ``get`` above.
        return super().get_context_data(**kwargs)


class SMEHubLearningReportView(RoleRequiredMixin, TemplateView):
    """Learning report: sector performance + AIH completion + partnership-to-deal
    conversion + dropout reasons (FRSME-MEI020, MEI021).

    Phase 8 — accepts ``?format=xlsx`` and ``?format=pdf`` for artefact download.
    """

    allowed_roles = _SMEHUB_REPORT_ROLES
    template_name = "reports/smehub_learning_report.html"

    def _audit(self, fmt, params):
        AuditLog.objects.create(
            actor=self.request.user if self.request.user.is_authenticated else None,
            action=AuditLog.Action.ACCESS,
            target_app="mel_reports",
            target_model="SMEHubLearningReport",
            object_id="smehub_learning",
            object_repr=f"smehub_learning fmt={fmt}",
            changes={"format": fmt, **params},
        )

    def _resolve_params(self, request):
        """Parse the MEI020 filter controls from the query string."""
        from datetime import date as _date

        programme = (request.GET.get("programme") or "").strip()
        geographic_scope = (request.GET.get("geographic_scope") or "").strip()
        cohort_raw = (request.GET.get("cohort") or "").strip()
        cohort_id = int(cohort_raw) if cohort_raw.isdigit() else None
        ps_raw = request.GET.get("period_start") or ""
        pe_raw = request.GET.get("period_end") or ""
        try:
            ps = _date.fromisoformat(ps_raw) if ps_raw else None
        except ValueError:
            ps = None
        try:
            pe = _date.fromisoformat(pe_raw) if pe_raw else None
        except ValueError:
            pe = None
        return {
            "programme": programme,
            "cohort_id": cohort_id,
            "period_start": ps,
            "period_end": pe,
            "geographic_scope": geographic_scope,
        }

    def get(self, request, *args, **kwargs):
        from django.http import HttpResponse

        from apps.mel.reports.builders import build_smehub_learning_report
        from apps.mel.reports.smehub_renderers import (
            render_smehub_learning_pdf,
            render_smehub_learning_xlsx,
        )

        fmt = (request.GET.get("format") or "html").lower()
        params = self._resolve_params(request)
        audit_params = {
            "programme": params["programme"],
            "cohort_id": params["cohort_id"],
            "geographic_scope": params["geographic_scope"],
            "period_start": params["period_start"].isoformat() if params["period_start"] else None,
            "period_end": params["period_end"].isoformat() if params["period_end"] else None,
        }
        report = build_smehub_learning_report(**params)

        if fmt == "xlsx":
            self._audit("xlsx", audit_params)
            return HttpResponse(
                render_smehub_learning_xlsx(report),
                content_type=_XLSX_CONTENT_TYPE,
                headers={"Content-Disposition": 'attachment; filename="smehub-learning-report.xlsx"'},
            )
        if fmt == "pdf":
            self._audit("pdf", audit_params)
            return HttpResponse(
                render_smehub_learning_pdf(report),
                content_type="application/pdf",
                headers={"Content-Disposition": 'attachment; filename="smehub-learning-report.pdf"'},
            )

        self._audit("html", audit_params)
        ctx = self.get_context_data(report=report, filters=params, cohorts=self._cohort_choices())
        return self.render_to_response(ctx)

    def _cohort_choices(self):
        try:
            from apps.smehub.incubation.models import Cohort

            return list(
                Cohort.objects.select_related("programme").order_by("-start_date")[:200]
            )
        except Exception:  # pragma: no cover - smehub optional
            return []

    def get_context_data(self, **kwargs):
        return super().get_context_data(**kwargs)


# ---------------------------------------------------------------------------
# Phase 4.4 — Report builder & schedule manager
# ---------------------------------------------------------------------------


class ReportBuilderView(RoleRequiredMixin, View):
    """Drag-drop section configurator for a ReportTemplate."""

    allowed_roles = OFFICER_ROLES
    template_name = "reports/report_builder.html"

    def get(self, request, slug: str):
        template = get_object_or_404(ReportTemplate, slug=slug)
        sections = list(template.section_rows.all().order_by("ordinal"))
        if sections:
            sections_payload = [
                {
                    "kind": s.kind,
                    "title": s.title,
                    "htmx_lazy": s.htmx_lazy,
                    "config": s.config,
                }
                for s in sections
            ]
        else:
            # No builder rows yet — hydrate from the wizard-authored
            # ``template.sections`` list so the builder is a true alternate
            # editor of the same section set (MEI015). Unknown keys are dropped.
            valid_kinds = set(ReportSectionKind.values)
            sections_payload = [
                {"kind": key, "title": "", "htmx_lazy": False, "config": {}}
                for key in (template.sections or [])
                if key in valid_kinds
            ]
        sections_initial = json.dumps(sections_payload)
        from apps.mel.indicators.models import Indicator

        indicator_options = list(
            Indicator.objects.filter(is_active=True)
            .order_by("code")
            .values("code", "name")[:200]
        )
        from apps.mel.tracking.models import TrackingRecord

        return render(
            request,
            self.template_name,
            {
                "template": template,
                "sections": sections,
                "sections_initial": sections_initial,
                "kind_choices": ReportSectionKind.choices,
                "indicator_options": json.dumps(indicator_options),
                "module_options": json.dumps([
                    {"value": v, "label": l} for v, l in TrackingRecord.Module.choices
                ]),
                "severity_options": json.dumps(["low", "medium", "high", "critical"]),
            },
        )

    def post(self, request, slug: str):
        template = get_object_or_404(ReportTemplate, slug=slug)
        try:
            sections = json.loads(request.POST.get("sections_json") or "[]")
        except json.JSONDecodeError:
            sections = []
        template.section_rows.all().delete()
        valid_kinds = set(ReportSectionKind.values)
        ordered_keys: list[str] = []
        for ordinal, section in enumerate(sections):
            kind = section.get("kind") or ReportSectionKind.NARRATIVE
            ReportSection.objects.create(
                template=template,
                ordinal=ordinal,
                kind=kind,
                title=section.get("title") or "",
                config=section.get("config") or {},
                htmx_lazy=bool(section.get("htmx_lazy")),
            )
            # MEI015 — bridge the drag-drop arrangement into the JSON list that
            # report generation actually reads. ``narrative`` is prose surfaced
            # from ``Report.narrative`` (executive summary), not a data section,
            # so it is excluded. Order is preserved; duplicates collapse.
            if (
                kind in valid_kinds
                and kind != ReportSectionKind.NARRATIVE
                and kind not in ordered_keys
            ):
                ordered_keys.append(kind)
        template.sections = ordered_keys
        template.save(update_fields=["sections", "updated_at"])
        messages.success(request, "Section layout saved.")
        # Explicit "Save & exit" button posts intent=done — route to template list.
        if request.POST.get("intent") == "done":
            return HttpResponseRedirect(reverse("mel_reports:template_list"))
        return HttpResponseRedirect(reverse("mel_reports:report_builder", args=[template.slug]))


class ReportSectionFragmentView(RoleRequiredMixin, View):
    """HTMX endpoint — render one section for an existing report."""

    allowed_roles = VIEW_ROLES

    def get(self, request, pk: int, section_id: int):
        report = get_object_or_404(Report, pk=pk)
        section = get_object_or_404(ReportSection, pk=section_id, template=report.template)
        from apps.mel.reports.services import render_section

        ctx = {
            "section": section,
            "report": report,
            "fragment": render_section(section, report=report),
        }
        return render(request, "reports/partials/report_section.html", ctx)


class ScheduledReportListView(RoleRequiredMixin, ListView):
    allowed_roles = OFFICER_ROLES
    model = ScheduledReport
    context_object_name = "schedules"
    paginate_by = 10
    template_name = "reports/scheduled_reports.html"

    def get_queryset(self):
        return (
            ScheduledReport.objects.select_related("template")
            .prefetch_related("recipients")
            .order_by("-is_active", "next_run_at")
        )

    def get_context_data(self, **kwargs):
        from apps.mel.reports.services import mel_recipient_users

        ctx = super().get_context_data(**kwargs)
        ctx["templates"] = ReportTemplate.objects.filter(is_active=True).order_by("name")
        ctx["frequencies"] = ScheduleFrequency.choices
        # C-D5 — scope the recipient picker to MEL-relevant / staff users
        # (not the first 200 by email, which hid mel_officer and most staff).
        # The <select data-s2> makes it searchable so the full list is reachable.
        ctx["recipient_options"] = mel_recipient_users()
        return ctx


class ScheduledReportCreateView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES

    def get(self, request):
        # C-D9 — the create form lives inline on the schedules list; a bare GET
        # to schedules/new/ (deep link / autocomplete) used to raise 405. Send
        # the user to the list page where the form is rendered.
        return HttpResponseRedirect(reverse("mel_reports:scheduled_reports"))

    def post(self, request):
        template_id = request.POST.get("template")
        frequency = request.POST.get("frequency") or ScheduleFrequency.QUARTERLY
        formats = [f for f in (request.POST.getlist("formats") or []) if f]
        recipient_ids = [int(x) for x in request.POST.getlist("recipients") if x.isdigit()]
        extra_emails = [
            e.strip() for e in (request.POST.get("extra_emails") or "").splitlines() if e.strip()
        ]
        if not template_id:
            messages.error(request, "Pick a template.")
            return HttpResponseRedirect(reverse("mel_reports:scheduled_reports"))
        sched = ScheduledReport.objects.create(
            template_id=template_id,
            frequency=frequency,
            formats=formats or ["pdf", "html"],
            extra_emails=extra_emails,
            next_run_at=timezone.now(),
            created_by=request.user,
        )
        if recipient_ids:
            sched.recipients.add(*recipient_ids)
        # C-D5 — a schedule with no internal recipients AND no extra emails
        # would email nobody (template defaults aside). Warn rather than saving
        # a silently-empty schedule.
        if not recipient_ids and not extra_emails:
            messages.warning(
                request,
                "Schedule created, but no recipients were selected — it will only reach the template's default recipients. Add subscribers or extra emails to distribute more widely.",
            )
        else:
            messages.success(request, "Schedule created.")
        return HttpResponseRedirect(reverse("mel_reports:scheduled_reports"))


class ScheduledReportToggleView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        sched = get_object_or_404(ScheduledReport, pk=pk)
        sched.is_active = not sched.is_active
        sched.save(update_fields=["is_active", "updated_at"])
        return HttpResponseRedirect(reverse("mel_reports:scheduled_reports"))


class ComplianceAlertListView(RoleRequiredMixin, ListView):
    """G12 — global, filterable compliance-alert inbox across all reports.

    M&E SRS Table 66 — management (programme managers/directors) reviews
    compliance issues alongside the M&E officers.
    """

    allowed_roles = OFFICER_ROLES + (
        UserRole.PROGRAM_MANAGER,
        UserRole.PROGRAM_DIRECTOR,
    )
    model = ComplianceAlert
    context_object_name = "alerts"
    paginate_by = 10
    template_name = "reports/compliance_alerts.html"

    def get_queryset(self):
        qs = (
            ComplianceAlert.objects.select_related(
                "report__template", "indicator", "resolved_by"
            )
            .order_by("resolved", "-created_at")
        )
        severity = self.request.GET.get("severity", "").strip()
        status = self.request.GET.get("status", "").strip()
        if severity:
            qs = qs.filter(severity=severity)
        if status == "open":
            qs = qs.filter(resolved=False)
        elif status == "resolved":
            qs = qs.filter(resolved=True)
        return qs

    def get_context_data(self, **kwargs):
        from django.db.models import Count, Q

        ctx = super().get_context_data(**kwargs)
        ctx["severity_choices"] = ComplianceSeverity.choices
        ctx["severity_filter"] = self.request.GET.get("severity", "")
        ctx["status_filter"] = self.request.GET.get("status", "open")
        # Triage signal — counts of OPEN alerts grouped by severity, plus a
        # resolved total so the hero strip can show progress at a glance.
        # Alias names must not collide with the ``resolved`` field — otherwise
        # Django treats them as aggregates inside subsequent ``Q`` filters and
        # raises ``Cannot compute Count('resolved'): 'resolved' is an aggregate``.
        stats = ComplianceAlert.objects.aggregate(
            critical=Count("pk", filter=Q(severity="critical", resolved=False)),
            high=Count("pk", filter=Q(severity="high", resolved=False)),
            medium=Count("pk", filter=Q(severity="medium", resolved=False)),
            low=Count("pk", filter=Q(severity="low", resolved=False)),
            resolved_total=Count("pk", filter=Q(resolved=True)),
            open_total=Count("pk", filter=Q(resolved=False)),
        )
        ctx["stats"] = stats
        return ctx


class ComplianceAlertCreateView(RoleRequiredMixin, View):
    """M&E SRS Table 66 — record a manual compliance-review finding.

    The automated scan only covers RED indicators and budget overruns; a
    Compliance Officer reviewing implementation against donor agreements,
    regulations, contracts or internal procedures records issues here. New
    alerts notify the M&E officers; CRITICAL ones escalate to management.
    """

    allowed_roles = OFFICER_ROLES + (
        UserRole.PROGRAM_MANAGER,
        UserRole.PROGRAM_DIRECTOR,
    )
    template_name = "reports/compliance_alert_form.html"

    def get(self, request):
        from apps.mel.reports.forms import ComplianceAlertCreateForm

        return render(request, self.template_name, {"form": ComplianceAlertCreateForm()})

    def post(self, request):
        from apps.core.audit.mixins import log_audit
        from apps.mel.reports.forms import ComplianceAlertCreateForm
        from apps.mel.reports.services import notify_compliance_alerts

        form = ComplianceAlertCreateForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {"form": form})
        alert = form.save(commit=False)
        alert.raised_by = request.user
        alert.code = f"manual.{alert.requirement_type or 'other'}"
        alert.save()
        log_audit(
            actor=request.user,
            action=AuditLog.Action.CREATE,
            target_app="mel_reports",
            target_model="ComplianceAlert",
            object_id=alert.pk,
            object_repr=str(alert),
            changes={
                "severity": alert.severity,
                "requirement_type": alert.requirement_type,
            },
        )
        notified = notify_compliance_alerts([alert])
        messages.success(
            request,
            f"Compliance issue recorded ({alert.get_severity_display()}) — "
            f"{notified} notification{'' if notified == 1 else 's'} sent.",
        )
        return HttpResponseRedirect(reverse("mel_reports:compliance_alerts"))


class ComplianceAlertResolveView(RoleRequiredMixin, View):
    """Acknowledge or dismiss a ComplianceAlert with an audit-friendly reason."""

    # Officers handle routine alerts; managers must be able to reach the endpoint
    # too so they can sign off the budget/critical alerts the gate below reserves
    # for management (otherwise only admins could ever close them).
    allowed_roles = OFFICER_ROLES + (
        UserRole.PROGRAM_MANAGER,
        UserRole.PROGRAM_DIRECTOR,
    )

    # M&E SRS Table 66 — "Budget variances exceeding approved tolerance
    # thresholds shall require management approval before closure." Significant
    # budget-variance and CRITICAL breaches may only be resolved by management.
    MANAGEMENT_ROLES = (
        UserRole.PROGRAM_MANAGER,
        UserRole.PROGRAM_DIRECTOR,
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
    )

    def _needs_management_approval(self, alert) -> bool:
        return (
            alert.code.startswith("budget.overrun")
            or alert.severity == ComplianceSeverity.CRITICAL
        )

    def post(self, request, pk: int):
        alert = get_object_or_404(ComplianceAlert, pk=pk)
        reason = (request.POST.get("reason") or "").strip()
        if not reason:
            messages.error(request, "A short reason is required to resolve an alert.")
        elif alert.resolved:
            messages.info(request, "Alert is already resolved.")
        elif (
            self._needs_management_approval(alert)
            and getattr(request.user, "role", None) not in self.MANAGEMENT_ROLES
            and not request.user.is_superuser
        ):
            messages.error(
                request,
                "This is a significant budget / critical-compliance issue — it "
                "requires management approval (Program Manager or Director) before "
                "it can be closed.",
            )
        else:
            alert.resolved = True
            alert.resolved_at = timezone.now()
            alert.resolved_by = request.user
            # M&E SRS Table 66 — audit records shall not be modified: the
            # original message stays untouched and the reason lands in its own
            # resolution_note field (simple_history snapshots both).
            alert.resolution_note = reason
            alert.save(
                update_fields=["resolved", "resolved_at", "resolved_by", "resolution_note"]
            )
            messages.success(request, "Alert resolved.")
        redirect_to = request.POST.get("next") or reverse("mel_reports:compliance_alerts")
        return HttpResponseRedirect(redirect_to)


class ScheduledReportRunNowView(RoleRequiredMixin, View):
    """G15 — force a one-off run of a schedule and distribute to its recipients.

    The underlying ``generate_render_and_publish`` is idempotent per
    ``(template, period_label)`` so clicking twice in a quarter refreshes the
    same Report row instead of duplicating it. Distribution is targeted at the
    schedule's per-run recipients in addition to the template defaults.
    """

    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        sched = get_object_or_404(ScheduledReport, pk=pk)
        from apps.mel.reports.services import (
            distribute_report,
            generate_render_and_publish,
            quarter_window,
        )

        label, start, end = quarter_window()
        report = generate_render_and_publish(
            template=sched.template,
            period_label=label,
            period_start=start,
            period_end=end,
            user=request.user,
            distribute=False,
            formats=list(sched.formats or []) or None,
        )
        # Per-schedule subscribers — both internal users and extra emails.
        schedule_users = list(sched.recipients.all())
        # Mirror them into template.default_recipients for downstream
        # `distribute_report` so the existing email pipeline picks them up.
        if schedule_users:
            sched.template.default_recipients.add(*schedule_users)
        distribute_report(report, extra_emails=list(sched.extra_emails or []))
        sched.last_run_at = timezone.now()
        sched.save(update_fields=["last_run_at", "updated_at"])
        messages.success(
            request,
            f"Generated and distributed report {report.pk} ({label}).",
        )
        return HttpResponseRedirect(reverse("mel_reports:report_detail", args=[report.pk]))


# ---------------------------------------------------------------------------
# G18 — Webhook distribution CRUD (admin-only)
# ---------------------------------------------------------------------------


class WebhookListView(RoleRequiredMixin, ListView):
    allowed_roles = OFFICER_ROLES
    model = ReportDistributionWebhook
    template_name = "reports/webhooks_list.html"
    context_object_name = "webhooks"
    paginate_by = 10

    def get_queryset(self):
        return ReportDistributionWebhook.objects.select_related("template").order_by(
            "-is_active", "-created_at",
        )


class WebhookCreateView(RoleRequiredMixin, CreateView):
    allowed_roles = OFFICER_ROLES
    model = ReportDistributionWebhook
    form_class = ReportDistributionWebhookForm
    template_name = "reports/webhooks_form.html"
    success_url = reverse_lazy("mel_reports:webhooks_list")

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)


class WebhookUpdateView(RoleRequiredMixin, UpdateView):
    allowed_roles = OFFICER_ROLES
    model = ReportDistributionWebhook
    form_class = ReportDistributionWebhookForm
    template_name = "reports/webhooks_form.html"
    success_url = reverse_lazy("mel_reports:webhooks_list")


class WebhookDeleteView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        hook = get_object_or_404(ReportDistributionWebhook, pk=pk)
        hook.delete()
        messages.success(request, "Webhook removed.")
        return HttpResponseRedirect(reverse("mel_reports:webhooks_list"))
