"""RIMS dashboard widgets — grants, applications, awards, finance."""
from __future__ import annotations

import json
from datetime import timedelta

from django.db.models import F as models_F
from django.urls import reverse
from django.utils import timezone

from apps.core.dashboard.base import (
    ZONE_ACTIONS,
    ZONE_KPI,
    ZONE_KPI_FELLOWSHIP,
    ZONE_KPI_SCHOLARSHIP,
    ZONE_PRIMARY,
    ZONE_SIDE,
    DashboardWidget,
)
from apps.core.dashboard.registry import register
from apps.core.dashboard.role_groups import (
    FINANCE_ROLES,
    REVIEWER_ROLES,
    RIMS_APPLICANT_ROLES,
    RIMS_DASHBOARD_MANAGER_ROLES,
    RIMS_MANAGEMENT_ROLES,
    has_role,
    is_admin,
)


# ── Helpers ──────────────────────────────────────────────────────────────


def _grant_models():
    from apps.rims.grants.models import Application, Award, GrantCall

    return Application, Award, GrantCall


# ═══════════════════════════════════════════════════════════════════════════
# RESEARCH GRANTS — KPI zone (ZONE_KPI)
# ═══════════════════════════════════════════════════════════════════════════


def _is_grants_manager(user):
    return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not is_admin(user) and not has_role(user, FINANCE_ROLES)


class _ResearchKPIBase(DashboardWidget):
    zone = ZONE_KPI
    template = "core/dashboard/widgets/kpi_card.html"
    zone_heading = "Research grants"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)


@register
class ResearchGrantCallsKPI(_ResearchKPIBase):
    key = "rims.kpi.rg.calls"
    priority = 10

    def get_context(self, user) -> dict:
        _, _, GrantCall = _grant_models()
        total = GrantCall.objects.filter(call_type=GrantCall.CallType.GRANT).count()
        active = GrantCall.objects.filter(call_type=GrantCall.CallType.GRANT, status=GrantCall.Status.PUBLISHED).count()
        return {
            "label": "Grant calls",
            "value": f"{total:,}",
            "hint": f"{active} active" if active else "None active",
            "accent": "emerald" if active else "primary",
            "href": reverse("rims_grants:manager_call_list"),
        }


@register
class ResearchReviewQueueKPI(_ResearchKPIBase):
    key = "rims.kpi.rg.review"
    priority = 11

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        n = Application.objects.filter(
            status=Application.Status.SUBMITTED,
            call__call_type="grant",
        ).count()
        return {
            "label": "Proposals to review",
            "value": f"{n:,}",
            "hint": "Research grant proposals only",
            "accent": "amber" if n else "primary",
            "href": reverse("rims_grants:manager_all_applications"),
        }


@register
class ResearchShortlistedKPI(_ResearchKPIBase):
    key = "rims.kpi.rg.shortlisted"
    priority = 12

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        n = Application.objects.filter(
            status=Application.Status.SHORTLISTED,
            call__call_type="grant",
        ).count()
        return {
            "label": "Shortlisted",
            "value": f"{n:,}",
            "hint": "Passed review, awaiting award",
            "accent": "sky",
            "href": reverse("rims_grants:manager_all_applications"),
        }


@register
class ResearchBudgetKPI(_ResearchKPIBase):
    key = "rims.kpi.rg.budget"
    priority = 13

    def get_context(self, user) -> dict:
        from django.db.models import Sum
        _, Award, _ = _grant_models()
        total = Award.objects.exclude(status=Award.Status.CANCELLED).aggregate(t=Sum("amount"))["t"] or 0
        if total >= 1_000_000:
            display = f"${total / 1_000_000:.1f}M"
        else:
            display = f"${total / 1_000:.0f}K"
        active = Award.objects.filter(status=Award.Status.ACTIVE).count()
        closed = Award.objects.filter(status=Award.Status.CLOSED).count()
        return {
            "label": "Budget committed",
            "value": display,
            "hint": f"{active} active · {closed} closed",
            "accent": "emerald",
            "href": reverse("rims_grants:award_dashboard"),
        }


# ═══════════════════════════════════════════════════════════════════════════
# SCHOLARSHIPS — KPI zone (ZONE_KPI_SCHOLARSHIP)
# ═══════════════════════════════════════════════════════════════════════════


class _ScholarshipKPIBase(DashboardWidget):
    zone = ZONE_KPI_SCHOLARSHIP
    template = "core/dashboard/widgets/kpi_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)


@register
class ScholarshipApplicantsKPI(_ScholarshipKPIBase):
    key = "rims.kpi.sc.applicants"
    priority = 10

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        total = Application.objects.filter(call__call_type="scholarship").count()
        submitted = Application.objects.filter(
            call__call_type="scholarship", status="submitted"
        ).count()
        return {
            "label": "Scholarship applicants",
            "value": f"{total:,}",
            "hint": f"{submitted:,} awaiting screening",
            "accent": "sky",
            "href": reverse("rims_grants:manager_all_applications"),
        }


@register
class ScholarshipSelectedKPI(_ScholarshipKPIBase):
    key = "rims.kpi.sc.selected"
    priority = 11

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        from apps.rims.scholarships.models import Scholar
        awarded = Application.objects.filter(
            call__call_type="scholarship", status__in=["awarded", "shortlisted"]
        ).count()
        scholars = Scholar.objects.filter(scholarship__name__icontains="TAGDev").count()
        return {
            "label": "Selected scholars",
            "value": f"{awarded:,}",
            "hint": f"{scholars} enrolled in programmes",
            "accent": "emerald",
            "href": reverse("rims_scholarships:scholar_list"),
        }


@register
class ScholarshipRejectedKPI(_ScholarshipKPIBase):
    key = "rims.kpi.sc.rejected"
    priority = 12

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        total = Application.objects.filter(call__call_type="scholarship").count()
        rejected = Application.objects.filter(
            call__call_type="scholarship", status="rejected"
        ).count()
        rate = int(rejected / total * 100) if total else 0
        return {
            "label": "Rejection rate",
            "value": f"{rate}%",
            "hint": f"{rejected:,} of {total:,} applications",
            "accent": "rose" if rate > 50 else "amber",
        }


@register
class ScholarshipSocioKPI(_ScholarshipKPIBase):
    key = "rims.kpi.sc.socio"
    priority = 13

    def get_context(self, user) -> dict:
        from django.db.models import Count, Q
        from apps.rims.grants.models import ScholarshipApplicationProfile
        stats = ScholarshipApplicationProfile.objects.aggregate(
            total=Count("id"),
            no_elec=Count("id", filter=Q(electricity=False)),
            refugee=Count("id", filter=Q(is_refugee=True)),
            female=Count("id", filter=Q(gender="female")),
        )
        total = stats["total"] or 1
        pct_no_elec = int(stats["no_elec"] / total * 100)
        pct_female = int(stats["female"] / total * 100)
        return {
            "label": "Without electricity",
            "value": f"{pct_no_elec}%",
            "hint": f"{stats['no_elec']:,} applicants · {stats['refugee']:,} refugees · {pct_female}% female",
            "accent": "amber",
            "substat": [
                {"label": "Refugees", "value": f"{stats['refugee']:,}", "color": "#d97706"},
                {"label": "Female", "value": f"{pct_female}%", "color": "#db2777"},
            ],
        }


# ═══════════════════════════════════════════════════════════════════════════
# FELLOWSHIPS — KPI zone (ZONE_KPI_FELLOWSHIP)
# ═══════════════════════════════════════════════════════════════════════════


class _FellowshipKPIBase(DashboardWidget):
    zone = ZONE_KPI_FELLOWSHIP
    template = "core/dashboard/widgets/kpi_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)


@register
class FellowshipApplicantsKPI(_FellowshipKPIBase):
    key = "rims.kpi.fe.applicants"
    priority = 10

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        total = Application.objects.filter(call__call_type="fellowship").count()
        submitted = Application.objects.filter(
            call__call_type="fellowship", status="submitted"
        ).count()
        return {
            "label": "Fellowship applications",
            "value": f"{total:,}",
            "hint": f"{submitted} pending review",
            "accent": "sky",
            "href": reverse("rims_grants:manager_all_applications"),
        }


@register
class FellowshipShortlistedKPI(_FellowshipKPIBase):
    key = "rims.kpi.fe.shortlisted"
    priority = 11

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        n = Application.objects.filter(
            call__call_type="fellowship", status="shortlisted"
        ).count()
        return {
            "label": "Fellows selected",
            "value": f"{n:,}",
            "hint": "Passed review",
            "accent": "emerald",
            "href": reverse("rims_grants:manager_all_applications"),
        }


@register
class FellowshipCallsKPI(_FellowshipKPIBase):
    key = "rims.kpi.fe.calls"
    priority = 12

    def get_context(self, user) -> dict:
        _, _, GrantCall = _grant_models()
        total = GrantCall.objects.filter(call_type="fellowship").count()
        return {
            "label": "Fellowship calls",
            "value": f"{total:,}",
            "hint": "GTA · Post-Doc · Staff Mobility",
            "href": reverse("rims_grants:manager_call_list"),
        }


@register
class FellowshipGenderKPI(_FellowshipKPIBase):
    key = "rims.kpi.fe.gender"
    priority = 13

    def get_context(self, user) -> dict:
        from django.db.models import Count, Q
        Application, _, _ = _grant_models()
        rows = Application.objects.filter(call__call_type="fellowship").values(
            "applicant__profile__gender"
        ).annotate(c=Count("id"))
        gender = {r["applicant__profile__gender"]: r["c"] for r in rows}
        female = gender.get("female", 0)
        male = gender.get("male", 0)
        total = female + male or 1
        pct_f = int(female / total * 100)
        return {
            "label": "Fellowship gender split",
            "value": f"{pct_f}% F",
            "hint": f"{female} female · {male} male",
            "accent": "sky",
            "substat": [
                {"label": "F", "value": str(female), "color": "#db2777"},
                {"label": "M", "value": str(male), "color": "#0284c7"},
            ],
        }


# ═══════════════════════════════════════════════════════════════════════════
# QUICK ACTIONS
# ═══════════════════════════════════════════════════════════════════════════


@register
class ManagerQuickActions(DashboardWidget):
    key = "rims.manager_actions"
    zone = ZONE_ACTIONS
    priority = 20
    template = "core/dashboard/widgets/quick_actions.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        pending_grants = Application.objects.filter(
            status=Application.Status.SUBMITTED, call__call_type="grant"
        ).count()
        pending_sc = Application.objects.filter(
            status=Application.Status.SUBMITTED, call__call_type="scholarship"
        ).count()
        return {
            "actions": [
                {
                    "label": "New call",
                    "href": reverse("rims_grants:call_create"),
                    "primary": True,
                    "icon": "M12 4v16m8-8H4",
                },
                {
                    "label": f"Grant review{f' ({pending_grants})' if pending_grants else ''}",
                    "href": reverse("rims_grants:review_queue"),
                    "alert": pending_grants > 0,
                    "icon": "M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
                },
                {
                    "label": f"Scholarship screening{f' ({pending_sc})' if pending_sc else ''}",
                    "href": reverse("rims_scholarships:officer_dashboard"),
                    "alert": pending_sc > 0,
                    "icon": "M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222",
                },
                {
                    "label": "Awards",
                    "href": reverse("rims_grants:award_dashboard"),
                    "icon": "M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z",
                },
            ],
        }


# ═══════════════════════════════════════════════════════════════════════════
# PRIMARY ZONE — Programme-separated charts and lists
# ═══════════════════════════════════════════════════════════════════════════


@register
class ResearchGrantPipelineDonut(DashboardWidget):
    """Research grants only — application pipeline donut."""

    key = "rims.primary.rg.pipeline_donut"
    zone = ZONE_PRIMARY
    priority = 10
    template = "core/dashboard/widgets/donut_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        from django.db.models import Count
        Application, _, _ = _grant_models()

        rows = Application.objects.filter(call__call_type="grant").values(
            "status"
        ).annotate(c=Count("id")).order_by()

        palette = {
            "submitted":   ("Submitted",    "#d97706"),
            "shortlisted": ("Shortlisted",  "#7c3aed"),
            "under_review":("Under review", "#0284c7"),
            "awarded":     ("Awarded",      "#059669"),
            "rejected":    ("Rejected",     "#94a3b8"),
            "ineligible":  ("Ineligible",   "#e5e7eb"),
            "draft":       ("Draft",        "#cbb98a"),
        }
        segs, labels, values, colors, total = [], [], [], [], 0
        for r in sorted(rows, key=lambda x: -x["c"]):
            if r["status"] not in palette:
                continue
            label, color = palette[r["status"]]
            segs.append({"label": label, "value": r["c"], "color": color})
            labels.append(label)
            values.append(r["c"])
            colors.append(color)
            total += r["c"]

        return {
            "title": "Research grant pipeline",
            "subtitle": "Grant applications only — excludes scholarships & fellowships",
            "segments": segs,
            "labels_json": json.dumps(labels),
            "values_json": json.dumps(values),
            "colors_json": json.dumps(colors),
            "total": total,
            "total_label": "proposals",
            "footer_href": reverse("rims_grants:manager_all_applications"),
            "footer_label": "All research applications",
        }


@register
class TAGDevUniversityChart(DashboardWidget):
    """Bar chart — TAGDev scholarship applications by university."""

    key = "rims.primary.sc.tagdev_universities"
    zone = ZONE_PRIMARY
    priority = 15
    template = "core/dashboard/widgets/chart_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        from django.db.models import Count, Q
        Application, _, GrantCall = _grant_models()

        rows = (
            GrantCall.objects.filter(call_type="scholarship")
            .annotate(
                total=Count("applications"),
                awaiting=Count("applications",
                    filter=Q(applications__status="submitted")),
                home_val=Count("applications__scholarship_profile",
                    filter=Q(applications__scholarship_profile__home_validation_status="pending")),
                rejected=Count("applications",
                    filter=Q(applications__status="rejected")),
                selected=Count("applications",
                    filter=Q(applications__status__in=["awarded", "shortlisted"])),
            )
            .filter(total__gt=50)
            .order_by("-total")[:12]
        )

        def short_title(title):
            # Strip long boilerplate from TAGDev call titles
            for kw in [
                "Call for Scholarship Applications under the TAGDev 2.0 Programme for the 2025/2026 Academic Year at the ",
                "Call for Scholarship Applications under the TAGDev 2.0 Programme for the 2025/2026 Academic Year at ",
                "Call for Scholarship Applications under the  TAGDev 2.0 Programme for the 2025/2026  Academic Year at ",
                "Extended BSc. Programmes' Deadline: Call for Scholarship Applications under the TAGDev 2.0 Programme for the 2025/2026 Academic Year at the ",
                "Call for Scholarship Applications ",
            ]:
                title = title.replace(kw, "")
            return title.strip()[:35]

        labels        = [short_title(r.title) for r in rows]
        awaiting_vals = [r.awaiting  for r in rows]
        home_vals     = [r.home_val  for r in rows]
        rejected_vals = [r.rejected  for r in rows]
        selected_vals = [r.selected  for r in rows]

        chart_json = json.dumps({
            "type": "bar",
            "data": {
                "labels": labels,
                "datasets": [
                    {"label": "Awaiting screening", "data": awaiting_vals,
                     "backgroundColor": "#d97706", "borderRadius": 3},
                    {"label": "Home validation",    "data": home_vals,
                     "backgroundColor": "#7c3aed", "borderRadius": 3},
                    {"label": "Rejected",           "data": rejected_vals,
                     "backgroundColor": "#94a3b8", "borderRadius": 3},
                    {"label": "Selected",           "data": selected_vals,
                     "backgroundColor": "#059669", "borderRadius": 3},
                ],
            },
            "options": {
                "indexAxis": "y",
                "plugins": {
                    "legend": {
                        "display": True, "position": "top",
                        "labels": {"font": {"family": "Barlow", "size": 11},
                                   "color": "#72675c", "boxWidth": 10},
                    },
                    "tooltip": {"enabled": True},
                },
                "scales": {
                    "x": {"beginAtZero": True,
                          "grid": {"color": "#ecdfca"},
                          "ticks": {"font": {"family": "Barlow", "size": 10}, "color": "#72675c"}},
                    "y": {"grid": {"display": False},
                          "ticks": {"font": {"family": "Barlow", "size": 10}, "color": "#1c1610"}},
                },
            },
        })

        total_apps    = sum(r.total    for r in rows)
        total_waiting = sum(r.awaiting for r in rows)
        total_hv      = sum(r.home_val for r in rows)
        total_rej     = sum(r.rejected for r in rows)
        total_sel     = sum(r.selected for r in rows)

        if total_sel == 0:
            subtitle = (
                f"2025/2026 cycle in progress — selection not yet complete. "
                f"{total_waiting:,} awaiting screening · {total_hv:,} in home validation."
            )
        else:
            rate = int(total_sel / total_apps * 100) if total_apps else 0
            subtitle = f"{total_sel} selected from {total_apps:,} applicants ({rate}% selection rate)"

        return {
            "title": "Scholarship applications by call",
            "subtitle": subtitle,
            "chart_json": chart_json,
            "height": 340,
            "summary_items": [
                {"label": "Total applied",      "value": f"{total_apps:,}"},
                {"label": "Awaiting screening", "value": f"{total_waiting:,}"},
                {"label": "Home validation",    "value": f"{total_hv:,}"},
                {"label": "Rejected",           "value": f"{total_rej:,}"},
            ],
            "footer_href": reverse("rims_scholarships:officer_dashboard"),
            "footer_label": "Scholarship officer dashboard",
        }


# @register  # removed from dashboard per user request
class ResearchGrantReviewQueue(DashboardWidget):
    """Research grant proposals needing review — grants only."""

    key = "rims.primary.rg.review_queue"
    zone = ZONE_PRIMARY
    priority = 20
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        qs = (
            Application.objects.filter(
                status=Application.Status.SUBMITTED,
                call__call_type="grant",
            )
            .select_related("call", "applicant")
            .order_by("submitted_at")[:8]
        )
        items = []
        now = timezone.now()
        for app in qs:
            ts = getattr(app, "submitted_at", None)
            days = max(0, (now - ts).days) if ts else 0
            applicant = app.applicant.get_full_name() or app.applicant.email
            items.append({
                "label": app.call.title,
                "sublabel": applicant,
                "badge": f"{days}d waiting" if days else "New",
                "badge_color": "rose" if days > 30 else "amber" if days > 7 else "sky",
                "href": reverse("rims_grants:manager_application_detail", args=[app.pk]),
            })
        total = Application.objects.filter(
            status=Application.Status.SUBMITTED, call__call_type="grant"
        ).count()
        return {
            "title": "Research proposals to review",
            "subtitle": f"{total} grant proposals — scholarships and fellowships excluded",
            "items": items,
            "empty_text": "No grant proposals awaiting review.",
            "footer_href": reverse("rims_grants:review_queue"),
            "footer_label": "Open review queue",
        }


@register
class AwardsByYearChartNew(DashboardWidget):
    """Bar + line chart — research awards and budget by year."""

    key = "rims.primary.rg.awards_by_year"
    zone = ZONE_PRIMARY
    priority = 30
    template = "core/dashboard/widgets/chart_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        from django.db.models import Sum, Count
        _, Award, _ = _grant_models()

        rows = (
            Award.objects.exclude(status=Award.Status.CANCELLED)
            .values(yr=models_F("application__call__closes_at__year"))
            .annotate(c=Count("id"), total=Sum("amount"))
            .order_by("yr")
        )

        labels, counts, budgets = [], [], []
        for r in rows:
            if not r["yr"]:
                continue
            labels.append(str(r["yr"]))
            counts.append(r["c"])
            budgets.append(float(r["total"] or 0) / 1000)

        chart_json = json.dumps({
            "type": "bar",
            "data": {
                "labels": labels,
                "datasets": [
                    {"label": "Awards", "data": counts, "backgroundColor": "#6b5317", "borderRadius": 4, "yAxisID": "y"},
                    {"label": "Budget (USD thousands)", "data": budgets,
                     "backgroundColor": "rgba(107,83,23,0.15)", "borderColor": "#6b5317",
                     "borderWidth": 1.5, "type": "line", "yAxisID": "y1", "tension": 0.3,
                     "pointRadius": 3, "fill": False},
                ],
            },
            "options": {
                "plugins": {
                    "legend": {"display": True, "position": "top",
                               "labels": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c", "boxWidth": 12}},
                },
                "scales": {
                    "x": {"grid": {"display": False}, "ticks": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c"}},
                    "y": {"beginAtZero": True, "position": "left", "grid": {"color": "#ecdfca"},
                          "ticks": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c", "precision": 0},
                          "title": {"display": True, "text": "Awards", "font": {"size": 10}, "color": "#72675c"}},
                    "y1": {"beginAtZero": True, "position": "right", "grid": {"drawOnChartArea": False},
                           "ticks": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c"},
                           "title": {"display": True, "text": "Budget (K USD)", "font": {"size": 10}, "color": "#72675c"}},
                },
            },
        })

        total_awards = sum(counts)
        total_budget = sum(budgets)
        return {
            "title": "Research awards & budget by year",
            "subtitle": "Number of research grants awarded and total budget per year",
            "chart_json": chart_json,
            "height": 220,
            "summary_items": [
                {"label": "Total awards", "value": f"{total_awards:,}"},
                {"label": "Budget committed", "value": f"${total_budget / 1000:.1f}M"},
                {"label": "Avg per award", "value": f"${(total_budget * 1000 / total_awards / 1000):.0f}K" if total_awards else "—"},
            ],
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "All awards",
        }


# ═══════════════════════════════════════════════════════════════════════════
# SIDE ZONE — Socioeconomic, geography, gender, placement
# ═══════════════════════════════════════════════════════════════════════════


@register
class ScholarshipGeographyWidget(DashboardWidget):
    """Bubble map — scholarship applicant country of residence."""

    key = "rims.side.sc.geography"
    zone = ZONE_SIDE
    priority = 10
    template = "core/dashboard/widgets/map_card.html"

    # African country centroids (ISO 3166-1 alpha-2 → [lat, lng, display name])
    _CENTROIDS = {
        "UG": ( 1.37,  32.29, "Uganda"),
        "KE": (-0.02,  37.91, "Kenya"),
        "CM": ( 7.37,  12.35, "Cameroon"),
        "MW": (-13.25, 34.30, "Malawi"),
        "BJ": ( 9.31,   2.32, "Benin"),
        "NG": ( 9.08,   8.68, "Nigeria"),
        "GH": ( 7.95,  -1.02, "Ghana"),
        "ZW": (-19.02, 29.15, "Zimbabwe"),
        "ZA": (-30.56, 22.94, "South Africa"),
        "SS": ( 6.88,  31.31, "South Sudan"),
        "RW": (-1.94,  29.87, "Rwanda"),
        "MA": (31.79,  -7.09, "Morocco"),
        "TZ": (-6.37,  34.89, "Tanzania"),
        "BI": (-3.37,  29.92, "Burundi"),
        "ET": ( 9.15,  40.49, "Ethiopia"),
        "LR": ( 6.43,  -9.43, "Liberia"),
        "NA": (-22.96, 18.49, "Namibia"),
        "CD": (-4.04,  21.76, "DR Congo"),
        "SO": ( 5.15,  46.20, "Somalia"),
        "ZM": (-13.13, 27.85, "Zambia"),
        "LS": (-29.61, 28.23, "Lesotho"),
        "SD": (12.86,  30.22, "Sudan"),
        "SL": ( 8.46, -11.78, "Sierra Leone"),
        "MZ": (-18.67, 35.53, "Mozambique"),
        "GM": (13.44, -15.31, "Gambia"),
        "LY": (26.34,  17.23, "Libya"),
        "TG": ( 8.62,   0.82, "Togo"),
        "BW": (-22.33, 24.68, "Botswana"),
        "CI": ( 7.54,  -5.55, "Côte d'Ivoire"),
        "TD": (15.45,  18.73, "Chad"),
        "SN": (14.50, -14.45, "Senegal"),
        "GA": (-0.80,  11.61, "Gabon"),
        "IN": (20.59,  78.96, "India"),
        "MY": ( 4.21, 101.98, "Malaysia"),
        "KW": (29.31,  47.48, "Kuwait"),
    }

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        from django.db.models import Count
        from apps.rims.grants.models import ScholarshipApplicationProfile

        rows = (
            ScholarshipApplicationProfile.objects.exclude(country_of_residence="")
            .values("country_of_residence")
            .annotate(c=Count("id"))
            .order_by("-c")
        )

        total = sum(r["c"] for r in rows)
        country_count = len([r for r in rows if r["country_of_residence"] in self._CENTROIDS])

        points = []
        for r in rows:
            code = r["country_of_residence"]
            if code not in self._CENTROIDS:
                continue
            lat, lng, name = self._CENTROIDS[code]
            pct = round(r["c"] / total * 100, 1) if total else 0
            points.append({
                "lat": lat,
                "lng": lng,
                "name": name,
                "count": r["c"],
                "pct": pct,
            })

        # Legend: show scale reference
        if points:
            max_count = max(p["count"] for p in points)
            legend = [
                {"label": f"Max: {max_count:,} ({points[0]['name']})", "size": 18},
                {"label": f"Mid: ~{int(max_count/2):,}", "size": 12},
                {"label": "Min: <100", "size": 6},
            ]
        else:
            legend = []

        return {
            "title": "Applicant reach",
            "subtitle": (
                f"{total:,} scholarship applicants across {country_count} countries "
                f"— bubble size = number of applicants"
            ),
            "map_id": "applicant-reach",
            "points_json": json.dumps(points),
            "circle_color": "#6b5317",
            "height": 340,
            "legend": legend,
            "footer_href": reverse("rims_grants:applicant_reach_map"),
            "footer_label": "Full map view →",
        }


@register
class SocioeconomicSummaryWidget(DashboardWidget):
    """Mastercard Foundation M&E indicators from scholarship profile data."""

    key = "rims.side.sc.socioeconomic"
    zone = ZONE_SIDE
    priority = 15
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        from django.db.models import Count, Q, Avg
        from apps.rims.grants.models import ScholarshipApplicationProfile

        stats = ScholarshipApplicationProfile.objects.aggregate(
            total=Count("id"),
            no_elec=Count("id", filter=Q(electricity=False)),
            refugee=Count("id", filter=Q(is_refugee=True)),
            disability=Count("id", filter=Q(have_physical_disability=True)),
            female=Count("id", filter=Q(gender="female")),
            male=Count("id", filter=Q(gender="male")),
            avg_house=Avg("people_in_house"),
        )
        total = stats["total"] or 1

        def pct(n):
            return f"{int(n / total * 100)}%"

        items = [
            {"label": "Without electricity",       "value": f"{stats['no_elec']:,}",  "sublabel": pct(stats["no_elec"]),  "badge": pct(stats["no_elec"]),  "badge_color": "amber"},
            {"label": "Refugees",                  "value": f"{stats['refugee']:,}",  "sublabel": pct(stats["refugee"]),  "badge": pct(stats["refugee"]),  "badge_color": "rose"},
            {"label": "With disability",            "value": f"{stats['disability']:,}","sublabel": pct(stats["disability"]),"badge": pct(stats["disability"]),"badge_color": "sky"},
            {"label": "Female applicants",         "value": f"{stats['female']:,}",   "sublabel": pct(stats["female"]),   "badge": pct(stats["female"]),   "badge_color": "sky"},
            {"label": "Male applicants",           "value": f"{stats['male']:,}",     "sublabel": pct(stats["male"])},
            {"label": "Avg household size",        "value": f"{stats['avg_house']:.1f}" if stats["avg_house"] else "—", "sublabel": "people per household"},
        ]

        return {
            "title": "Socioeconomic indicators",
            "subtitle": f"From {total:,} scholarship application profiles — Mastercard M&E",
            "items": items,
            "empty_text": "No socioeconomic data yet.",
        }


@register
class GenderByProgrammeWidget(DashboardWidget):
    """Gender split across all three programme types."""

    key = "rims.side.gender_by_programme"
    zone = ZONE_SIDE
    priority = 20
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        from django.db.models import Count, Q
        Application, Award, _ = _grant_models()

        def gender_for(call_type):
            rows = Application.objects.filter(call__call_type=call_type).values(
                "applicant__profile__gender"
            ).annotate(c=Count("applicant", distinct=True))
            g = {r["applicant__profile__gender"]: r["c"] for r in rows}
            f, m = g.get("female", 0), g.get("male", 0)
            total = f + m or 1
            return f, m, int(f / total * 100)

        rg_f, rg_m, rg_pct = gender_for("grant")
        sc_f, sc_m, sc_pct = gender_for("scholarship")
        fe_f, fe_m, fe_pct = gender_for("fellowship")

        # Research grantees (awarded)
        award_rows = Award.objects.exclude(status="cancelled").values(
            "application__applicant__profile__gender"
        ).annotate(c=Count("application__applicant", distinct=True))
        aw = {r["application__applicant__profile__gender"]: r["c"] for r in award_rows}
        aw_f, aw_m = aw.get("female", 0), aw.get("male", 0)

        items = [
            {"label": "Research applicants",   "value": f"{rg_pct}% F", "sublabel": f"{rg_f}F · {rg_m}M",  "badge": "Grants",       "badge_color": "neutral"},
            {"label": "Research awardees",     "value": f"{int(aw_f/(aw_f+aw_m or 1)*100)}% F", "sublabel": f"{aw_f}F · {aw_m}M", "badge": "Awardees",     "badge_color": "emerald"},
            {"label": "Scholarship applicants","value": f"{sc_pct}% F", "sublabel": f"{sc_f:,}F · {sc_m:,}M", "badge": "Scholarships", "badge_color": "sky"},
            {"label": "Fellowship applicants", "value": f"{fe_pct}% F", "sublabel": f"{fe_f}F · {fe_m}M",  "badge": "Fellowships",  "badge_color": "amber"},
        ]

        return {
            "title": "Gender by programme",
            "subtitle": "Female participation across all programme types",
            "items": items,
            "empty_text": "No gender data yet.",
        }


@register
class ActiveAwardsDetailSide(DashboardWidget):
    """Active research awards with end date and amount."""

    key = "rims.side.rg.active_awards"
    zone = ZONE_SIDE
    priority = 25
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        from django.db.models import Sum, Count
        _, Award, _ = _grant_models()
        today = timezone.now().date()

        qs = (
            Award.objects.filter(status__in=[
                Award.Status.ACTIVE, Award.Status.AGREEMENT_PENDING,
                Award.Status.READY_FOR_CLOSEOUT,
            ])
            .select_related("application__call", "application__applicant")
            .order_by("project_end_date")[:8]
        )

        items = []
        for award in qs:
            days_left = (award.project_end_date - today).days
            if days_left < 0:
                badge, color = f"{abs(days_left)}d overdue", "rose"
            elif days_left <= 30:
                badge, color = f"{days_left}d left", "amber"
            elif days_left <= 90:
                badge, color = f"{days_left}d left", "sky"
            else:
                badge, color = award.get_status_display(), "emerald"
            applicant = (award.application.applicant.get_full_name() or
                         award.application.applicant.email)
            items.append({
                "label": award.application.call.title,
                "sublabel": f"{applicant} · ${award.amount:,.0f} · ends {award.project_end_date.strftime('%b %Y')}",
                "badge": badge,
                "badge_color": color,
                "href": reverse("rims_grants:award_dashboard"),
            })

        totals = Award.objects.filter(
            status__in=[Award.Status.ACTIVE, Award.Status.AGREEMENT_PENDING]
        ).aggregate(c=Count("id"), t=Sum("amount"))

        return {
            "title": "Active awards",
            "subtitle": f"{totals['c']} in execution · ${(totals['t'] or 0):,.0f} committed",
            "items": items,
            "empty_text": "No active awards.",
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "Full award dashboard",
        }


@register
class ClosingSoonSide(DashboardWidget):
    """Calls closing within 14 days."""

    key = "rims.side.closing_soon"
    zone = ZONE_PRIMARY
    priority = 35
    priority = 30
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_grants_manager(user)

    def get_context(self, user) -> dict:
        _, _, GrantCall = _grant_models()
        now = timezone.now()
        soon = now + timedelta(days=14)
        qs = GrantCall.objects.filter(
            status=GrantCall.Status.PUBLISHED,
            closes_at__gte=now,
            closes_at__lte=soon,
        ).order_by("closes_at")[:6]

        items = []
        for c in qs:
            days_left = (c.closes_at.date() - now.date()).days
            color = "rose" if days_left <= 2 else "amber" if days_left <= 7 else "sky"
            items.append({
                "label": c.title,
                "sublabel": f"{c.get_call_type_display()} · {c.closes_at.strftime('%b %d')}",
                "badge": f"{days_left}d left",
                "badge_color": color,
                "href": reverse("rims_grants:call_detail_manage", kwargs={"slug": c.slug}),
            })

        active = GrantCall.objects.filter(status=GrantCall.Status.PUBLISHED).count()
        return {
            "title": "Closing soon",
            "subtitle": "Active calls closing in the next 14 days",
            "items": items,
            "empty_text": "No calls closing in the next 14 days.",
            "footer_href": reverse("rims_grants:manager_call_list"),
            "footer_label": f"All {active} active calls",
            "title_badge": str(len(items)) if items else None,
            "title_badge_color": "rose" if items else "neutral",
        }



# ── Manager KPIs: new additions ──────────────────────────────────────────


# @register  # superseded by programme-specific widget
class ManagerFundingCommittedKPI(_ResearchKPIBase):
    """Total USD committed across all awards (excluding cancelled)."""

    key = "rims.kpi.funding_committed"
    priority = 24

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not is_admin(user)

    def get_context(self, user) -> dict:
        from django.db.models import Sum
        _, Award, _ = _grant_models()
        total = (
            Award.objects.exclude(status=Award.Status.CANCELLED)
            .aggregate(t=Sum("amount"))["t"]
            or 0
        )
        # Format as $X.XM or $X,XXX
        if total >= 1_000_000:
            display = f"${total / 1_000_000:.1f}M"
        elif total >= 1_000:
            display = f"${total / 1_000:.0f}K"
        else:
            display = f"${total:,.0f}"
        return {
            "label": "Funding committed",
            "value": display,
            "hint": "Total USD across all awards",
            "accent": "emerald",
            "href": reverse("rims_grants:award_dashboard"),
        }


# @register  # superseded by programme-specific widget
class ManagerGranteesKPI(_ResearchKPIBase):
    """Unique grantees (applicants with at least one non-cancelled award)."""

    key = "rims.kpi.grantees"
    priority = 25

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not is_admin(user)

    def get_context(self, user) -> dict:
        from django.db.models import Count
        _, Award, _ = _grant_models()
        n = (
            Award.objects.exclude(status=Award.Status.CANCELLED)
            .values("application__applicant")
            .distinct()
            .count()
        )
        # Gender breakdown via UserProfile
        gender_rows = (
            Award.objects.exclude(status=Award.Status.CANCELLED)
            .values("application__applicant__profile__gender")
            .annotate(c=Count("application__applicant", distinct=True))
        )
        female = next((r["c"] for r in gender_rows if r["application__applicant__profile__gender"] == "female"), 0)
        male = next((r["c"] for r in gender_rows if r["application__applicant__profile__gender"] == "male"), 0)

        substat = []
        if female or male:
            substat = [
                {"label": "F", "value": f"{female}", "color": "#db2777"},
                {"label": "M", "value": f"{male}", "color": "#0284c7"},
            ]

        return {
            "label": "Unique grantees",
            "value": f"{n:,}",
            "hint": "Applicants with an active or closed award",
            "accent": "sky",
            "href": reverse("rims_grants:award_dashboard"),
            "substat": substat,
        }


# @register  # superseded by programme-specific widget
class ManagerSuccessRateKPI(_ResearchKPIBase):
    """Shortlisted / (all non-draft, non-ineligible, non-rejected) as a pipeline %."""

    key = "rims.kpi.success_rate"
    priority = 26

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not is_admin(user)

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        terminal_neg = [
            Application.Status.DRAFT,
            Application.Status.EXPIRED_DRAFT,
        ]
        total = Application.objects.exclude(status__in=terminal_neg).count()
        progressed = Application.objects.filter(
            status__in=[
                Application.Status.SHORTLISTED,
                Application.Status.APPROVED,
                Application.Status.AWARDED,
            ]
        ).count()
        rate = int(progressed / total * 100) if total else 0
        return {
            "label": "Shortlist rate",
            "value": f"{rate}%",
            "hint": f"{progressed:,} of {total:,} applications shortlisted",
            "accent": "amber" if rate < 20 else "emerald",
        }


# @register  # superseded by programme-specific widget
class ManagerClosedAwardsKPI(_ResearchKPIBase):
    """Count of fully closed / completed awards."""

    key = "rims.kpi.closed_awards"
    priority = 27

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not is_admin(user)

    def get_context(self, user) -> dict:
        _, Award, _ = _grant_models()
        n = Award.objects.filter(status=Award.Status.CLOSED).count()
        return {
            "label": "Completed awards",
            "value": f"{n:,}",
            "hint": "Fully closed and archived",
            "href": reverse("rims_grants:award_dashboard"),
        }


# ── Manager charts ────────────────────────────────────────────────────────


# @register  # superseded by programme-specific widget
class ManagerAwardsByYearChart(DashboardWidget):
    """Bar chart — number of awards and total budget committed per year."""

    key = "rims.chart.awards_by_year"
    zone = ZONE_PRIMARY
    priority = 12
    template = "core/dashboard/widgets/chart_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Sum, Count
        _, Award, _ = _grant_models()

        rows = (
            Award.objects.exclude(status=Award.Status.CANCELLED)
            .values(yr=models_F("application__call__closes_at__year"))
            .annotate(c=Count("id"), total=Sum("amount"))
            .order_by("yr")
        )

        labels, counts, budgets = [], [], []
        for r in rows:
            if not r["yr"]:
                continue
            labels.append(str(r["yr"]))
            counts.append(r["c"])
            budgets.append(float(r["total"] or 0) / 1000)  # in thousands

        total_awards = sum(counts)
        total_budget = sum(budgets)

        chart_json = json.dumps({
            "type": "bar",
            "data": {
                "labels": labels,
                "datasets": [
                    {
                        "label": "Awards",
                        "data": counts,
                        "backgroundColor": "#6b5317",
                        "borderRadius": 4,
                        "yAxisID": "y",
                    },
                    {
                        "label": "Budget (USD thousands)",
                        "data": budgets,
                        "backgroundColor": "rgba(107,83,23,0.18)",
                        "borderColor": "#6b5317",
                        "borderWidth": 1.5,
                        "type": "line",
                        "yAxisID": "y1",
                        "tension": 0.3,
                        "pointRadius": 3,
                        "fill": False,
                    },
                ],
            },
            "options": {
                "plugins": {
                    "legend": {"display": True, "position": "top",
                               "labels": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c", "boxWidth": 12}},
                    "tooltip": {"enabled": True},
                },
                "scales": {
                    "x": {"grid": {"display": False},
                          "ticks": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c"}},
                    "y": {"beginAtZero": True, "position": "left",
                          "grid": {"color": "#ecdfca"},
                          "ticks": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c", "precision": 0},
                          "title": {"display": True, "text": "Awards", "font": {"size": 10}, "color": "#72675c"}},
                    "y1": {"beginAtZero": True, "position": "right",
                           "grid": {"drawOnChartArea": False},
                           "ticks": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c"},
                           "title": {"display": True, "text": "Budget (K USD)", "font": {"size": 10}, "color": "#72675c"}},
                },
            },
        })

        return {
            "title": "Awards & budget by year",
            "subtitle": "Number of awards granted and total budget committed per year",
            "chart_json": chart_json,
            "height": 240,
            "summary_items": [
                {"label": "Total awards", "value": f"{total_awards:,}"},
                {"label": "Budget committed", "value": f"${total_budget / 1000:.1f}M"},
                {"label": "Avg per award", "value": f"${(total_budget * 1000 / total_awards / 1000):.0f}K" if total_awards else "—"},
            ],
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "All awards",
        }


# @register  # superseded by programme-specific widget
class ManagerPipelineFunnelChart(DashboardWidget):
    """Horizontal bar — application pipeline funnel by status."""

    key = "rims.chart.pipeline_funnel"
    zone = ZONE_PRIMARY
    priority = 15
    template = "core/dashboard/widgets/chart_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count
        Application, _, _ = _grant_models()

        counts = {
            r["status"]: r["c"]
            for r in Application.objects.values("status").annotate(c=Count("id"))
        }

        stages = [
            ("submitted",    "Submitted",    "#d97706"),
            ("under_review", "Under Review",  "#0284c7"),
            ("shortlisted",  "Shortlisted",   "#7c3aed"),
            ("approved",     "Approved",      "#059669"),
            ("awarded",      "Awarded",       "#166534"),
            ("rejected",     "Rejected",      "#94a3b8"),
            ("ineligible",   "Ineligible",    "#e5e7eb"),
        ]

        labels = [s[1] for s in stages]
        values = [counts.get(s[0], 0) for s in stages]
        colors = [s[2] for s in stages]

        chart_json = json.dumps({
            "type": "bar",
            "data": {
                "labels": labels,
                "datasets": [{
                    "label": "Applications",
                    "data": values,
                    "backgroundColor": colors,
                    "borderRadius": 4,
                }],
            },
            "options": {
                "indexAxis": "y",
                "plugins": {"legend": {"display": False}, "tooltip": {"enabled": True}},
                "scales": {
                    "x": {"beginAtZero": True, "grid": {"color": "#ecdfca"},
                          "ticks": {"font": {"family": "Barlow", "size": 11}, "color": "#72675c", "precision": 0}},
                    "y": {"grid": {"display": False},
                          "ticks": {"font": {"family": "Barlow", "size": 12}, "color": "#1c1610"}},
                },
            },
        })

        total = Application.objects.exclude(
            status__in=["draft", "expired_draft"]
        ).count()
        draft_count = counts.get("draft", 0) + counts.get("expired_draft", 0)

        return {
            "title": "Application pipeline",
            "subtitle": "All applications by current stage",
            "chart_json": chart_json,
            "height": 220,
            "summary_items": [
                {"label": "Submitted", "value": f"{counts.get('submitted', 0):,}"},
                {"label": "Shortlisted", "value": f"{counts.get('shortlisted', 0):,}"},
                {"label": "Awarded", "value": f"{counts.get('awarded', 0):,}"},
                {"label": "Drafts", "value": f"{draft_count:,}"},
            ],
            "footer_href": reverse("rims_grants:manager_all_applications"),
            "footer_label": "All applications",
        }


@register
class ManagerCallTypeBreakdownChart(DashboardWidget):
    """Donut — awards by grant call type (Grant / Fellowship / Scholarship / Challenge)."""

    key = "rims.chart.call_type_breakdown"
    zone = ZONE_SIDE
    priority = 28
    template = "core/dashboard/widgets/donut_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count, Sum
        _, Award, GrantCall = _grant_models()

        rows = (
            Award.objects.exclude(status=Award.Status.CANCELLED)
            .values(ct=models_F("application__call__call_type"))
            .annotate(c=Count("id"))
            .order_by("-c")
        )

        palette = {
            GrantCall.CallType.GRANT:       ("#6b5317", "Grant"),
            GrantCall.CallType.FELLOWSHIP:  ("#7c3aed", "Fellowship"),
            GrantCall.CallType.SCHOLARSHIP: ("#0284c7", "Scholarship"),
            GrantCall.CallType.CHALLENGE:   ("#059669", "Challenge"),
        }

        segs, labels, values, colors = [], [], [], []
        total = 0
        for r in rows:
            ct = r["ct"] or "grant"
            color, label = palette.get(ct, ("#94a3b8", ct.title()))
            segs.append({"label": label, "value": r["c"], "color": color})
            labels.append(label)
            values.append(r["c"])
            colors.append(color)
            total += r["c"]

        return {
            "title": "Awards by type",
            "subtitle": "Breakdown of all grants awarded",
            "segments": segs,
            "labels_json": json.dumps(labels),
            "values_json": json.dumps(values),
            "colors_json": json.dumps(colors),
            "total": total,
            "total_label": "awards",
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "All awards",
        }


# ── Manager: Stale applications (no action > 30 days) ────────────────────


@register
class ManagerStaleApplicationsWidget(DashboardWidget):
    """Submitted applications with no review activity for > 30 days."""

    key = "rims.manager_stale_apps"
    zone = ZONE_PRIMARY
    priority = 40
    priority = 22
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        threshold = timezone.now() - timedelta(days=30)

        qs = (
            Application.objects.filter(
                status=Application.Status.SUBMITTED,
                submitted_at__lt=threshold,
            )
            .select_related("call", "applicant")
            .order_by("submitted_at")[:6]
        )

        items = []
        now = timezone.now()
        for app in qs:
            days = (now - app.submitted_at).days if app.submitted_at else 0
            applicant = app.applicant.get_full_name() or app.applicant.email
            items.append({
                "label": app.call.title,
                "sublabel": applicant,
                "badge": f"{days}d waiting",
                "badge_color": "rose" if days > 60 else "amber",
                "href": reverse("rims_grants:manager_application_detail", args=[app.pk]),
            })

        stale_count = Application.objects.filter(
            status=Application.Status.SUBMITTED,
            submitted_at__lt=threshold,
        ).count()

        return {
            "title": "Stale applications",
            "subtitle": "Submitted but no review in 30+ days",
            "items": items,
            "empty_text": "No stale applications — inbox is up to date.",
            "footer_href": reverse("rims_grants:manager_all_applications"),
            "footer_label": f"{stale_count} stale" if stale_count else "All applications",
            "title_badge": str(stale_count) if stale_count else None,
            "title_badge_color": "rose",
        }


# ── Manager: Active awards detail ────────────────────────────────────────


# @register  # superseded by programme-specific widget
class ManagerActiveAwardsDetailWidget(DashboardWidget):
    """Active awards with end date, amount, and days remaining."""

    key = "rims.manager_active_awards_detail"
    zone = ZONE_PRIMARY
    priority = 18
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Sum
        _, Award, _ = _grant_models()
        today = timezone.now().date()

        active_qs = (
            Award.objects.filter(status__in=[
                Award.Status.ACTIVE,
                Award.Status.AGREEMENT_PENDING,
                Award.Status.READY_FOR_CLOSEOUT,
                Award.Status.CLOSEOUT_IN_PROGRESS,
            ])
            .select_related("application__call", "application__applicant")
            .order_by("project_end_date")[:10]
        )

        items = []
        for award in active_qs:
            days_left = (award.project_end_date - today).days
            applicant = (
                award.application.applicant.get_full_name()
                or award.application.applicant.email
            )
            if days_left < 0:
                badge = f"{abs(days_left)}d overdue"
                badge_color = "rose"
            elif days_left <= 30:
                badge = f"{days_left}d left"
                badge_color = "amber"
            elif days_left <= 90:
                badge = f"{days_left}d left"
                badge_color = "sky"
            else:
                badge = award.get_status_display()
                badge_color = "emerald"

            amt = f"${award.amount:,.0f}"
            items.append({
                "label": award.application.call.title,
                "sublabel": f"{applicant} · {amt} · ends {award.project_end_date.strftime('%b %d, %Y')}",
                "badge": badge,
                "badge_color": badge_color,
                "href": reverse("rims_grants:award_dashboard"),
            })

        from django.db.models import Count
        totals = Award.objects.filter(status__in=[
            Award.Status.ACTIVE,
            Award.Status.AGREEMENT_PENDING,
            Award.Status.READY_FOR_CLOSEOUT,
            Award.Status.CLOSEOUT_IN_PROGRESS,
        ]).aggregate(c=Count("id"), t=Sum("amount"))

        subtitle = (
            f"{totals['c']} in execution · "
            f"${(totals['t'] or 0):,.0f} committed"
        )

        return {
            "title": "Active awards",
            "subtitle": subtitle,
            "items": items,
            "empty_text": "No active awards at the moment.",
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "Full award dashboard",
        }


# ── Manager: Gender breakdown of grantees ────────────────────────────────


# @register  # superseded by programme-specific widget
class ManagerGenderBreakdownWidget(DashboardWidget):
    """Donut — male vs female grantees with applicant-level breakdown."""

    key = "rims.manager_gender_breakdown"
    zone = ZONE_SIDE
    priority = 27
    template = "core/dashboard/widgets/donut_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count
        _, Award, _ = _grant_models()

        # Grantees by gender
        award_rows = (
            Award.objects.exclude(status=Award.Status.CANCELLED)
            .values("application__applicant__profile__gender")
            .annotate(c=Count("application__applicant", distinct=True))
        )
        gender_map = {r["application__applicant__profile__gender"]: r["c"] for r in award_rows}

        female = gender_map.get("female", 0)
        male   = gender_map.get("male", 0)
        other  = sum(v for k, v in gender_map.items() if k not in ("female", "male"))
        total  = female + male + other

        # Also show applicant-level gender (all non-draft applications)
        Application, _, _ = _grant_models()
        app_rows = (
            Application.objects.exclude(status__in=["draft", "expired_draft"])
            .values("applicant__profile__gender")
            .annotate(c=Count("applicant", distinct=True))
        )
        app_map = {r["applicant__profile__gender"]: r["c"] for r in app_rows}
        app_female = app_map.get("female", 0)
        app_male   = app_map.get("male", 0)

        segs, labels, values, colors = [], [], [], []
        for label, count, color in [
            ("Female", female, "#db2777"),
            ("Male",   male,   "#0284c7"),
            ("Other",  other,  "#94a3b8"),
        ]:
            if count == 0:
                continue
            segs.append({"label": label, "value": count, "color": color})
            labels.append(label)
            values.append(count)
            colors.append(color)

        pct_female = int(female / total * 100) if total else 0

        return {
            "title": "Grantee gender breakdown",
            "subtitle": (
                f"{pct_female}% female · "
                f"Applicants: {app_female}F / {app_male}M"
            ),
            "segments": segs,
            "labels_json": json.dumps(labels),
            "values_json": json.dumps(values),
            "colors_json": json.dumps(colors),
            "total": total,
            "total_label": "grantees",
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "All awards",
        }


# ── Manager: Calls closing soon ─────────────────────────────────────────


# @register  # superseded by programme-specific widget
class ManagerDeadlinesWidget(DashboardWidget):
    """Calls closing within the next 14 days — urgent attention needed."""

    key = "rims.manager_deadlines"
    zone = ZONE_SIDE
    priority = 10
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        _, _, GrantCall = _grant_models()
        now = timezone.now()
        soon = now + timedelta(days=14)
        qs = (
            GrantCall.objects.filter(
                status=GrantCall.Status.PUBLISHED,
                closes_at__gte=now,
                closes_at__lte=soon,
            )
            .order_by("closes_at")[:8]
        )
        items = []
        for c in qs:
            days_left = (c.closes_at.date() - now.date()).days
            if days_left <= 2:
                color = "rose"
            elif days_left <= 7:
                color = "amber"
            else:
                color = "sky"
            items.append({
                "label": c.title,
                "sublabel": c.closes_at.strftime("%b %d, %Y"),
                "badge": f"{days_left}d left" if days_left > 0 else "Closes today",
                "badge_color": color,
                "href": reverse("rims_grants:call_detail_manage", kwargs={"slug": c.slug}),
            })

        all_published = GrantCall.objects.filter(status=GrantCall.Status.PUBLISHED).count()
        return {
            "title": "Closing soon",
            "subtitle": "Calls closing in the next 14 days",
            "items": items,
            "empty_text": "No calls closing in the next 14 days.",
            "footer_href": reverse("rims_grants:manager_call_list"),
            "footer_label": f"All {all_published} active calls",
            "title_badge": str(len(items)) if items else None,
            "title_badge_color": "rose" if items else "neutral",
        }


# ── Manager: Applications per call breakdown ─────────────────────────────


# @register  # removed from dashboard per user request
class ManagerApplicationsPerCallWidget(DashboardWidget):
    """Top grant calls ranked by application volume — shows where demand is."""

    key = "rims.manager_apps_per_call"
    zone = ZONE_PRIMARY
    priority = 25
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count, Q
        Application, _, GrantCall = _grant_models()

        rows = (
            GrantCall.objects.annotate(
                total=Count("applications"),
                submitted=Count(
                    "applications",
                    filter=Q(applications__status=Application.Status.SUBMITTED),
                ),
                shortlisted=Count(
                    "applications",
                    filter=Q(applications__status=Application.Status.SHORTLISTED),
                ),
                awarded=Count(
                    "applications",
                    filter=Q(applications__status=Application.Status.AWARDED),
                ),
            )
            .filter(total__gt=0)
            .order_by("-total")[:8]
        )

        items = []
        for c in rows:
            parts = []
            if c.submitted:
                parts.append(f"{c.submitted} pending")
            if c.shortlisted:
                parts.append(f"{c.shortlisted} shortlisted")
            if c.awarded:
                parts.append(f"{c.awarded} awarded")
            status_color = {
                GrantCall.Status.PUBLISHED: "emerald",
                GrantCall.Status.CLOSED: "amber",
                GrantCall.Status.AWARDING: "sky",
                GrantCall.Status.COMPLETED: "neutral",
                GrantCall.Status.DRAFT: "neutral",
            }.get(c.status, "neutral")
            items.append({
                "label": c.title,
                "sublabel": " · ".join(parts) if parts else "No active reviews",
                "value": f"{c.total:,}",
                "badge": c.get_status_display(),
                "badge_color": status_color,
                "href": reverse("rims_grants:call_detail_manage", kwargs={"slug": c.slug}),
            })

        return {
            "title": "Applications by call",
            "subtitle": "Top calls by volume",
            "items": items,
            "empty_text": "No applications yet.",
            "footer_href": reverse("rims_grants:manager_all_applications"),
            "footer_label": "All applications",
        }


# ── Manager: Awards at risk (overdue) ────────────────────────────────────


@register
class ManagerAwardsAtRiskWidget(DashboardWidget):
    """Active awards past their project end date — require urgent follow-up."""

    key = "rims.manager_awards_at_risk"
    zone = ZONE_SIDE
    priority = 20
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import F
        _, Award, _ = _grant_models()
        today = timezone.now().date()

        # Overdue: active but past end date
        overdue = (
            Award.objects.filter(
                status=Award.Status.ACTIVE,
                project_end_date__lt=today,
            )
            .select_related("application__call", "application__applicant")
            .order_by("project_end_date")[:5]
        )

        # Expiring soon: active, ending within 60 days
        expiring = (
            Award.objects.filter(
                status=Award.Status.ACTIVE,
                project_end_date__gte=today,
                project_end_date__lte=today + timedelta(days=60),
            )
            .select_related("application__call", "application__applicant")
            .order_by("project_end_date")[:5]
        )

        items = []
        for award in overdue:
            days_over = (today - award.project_end_date).days
            applicant = (
                award.application.applicant.get_full_name()
                or award.application.applicant.email
            )
            items.append({
                "label": award.application.call.title,
                "sublabel": applicant,
                "badge": f"{days_over}d overdue",
                "badge_color": "rose",
                "href": reverse("rims_grants:award_dashboard"),
            })

        for award in expiring:
            days_left = (award.project_end_date - today).days
            applicant = (
                award.application.applicant.get_full_name()
                or award.application.applicant.email
            )
            items.append({
                "label": award.application.call.title,
                "sublabel": applicant,
                "badge": f"Ends in {days_left}d",
                "badge_color": "amber" if days_left <= 30 else "sky",
                "href": reverse("rims_grants:award_dashboard"),
            })

        overdue_count = Award.objects.filter(
            status=Award.Status.ACTIVE, project_end_date__lt=today
        ).count()

        return {
            "title": "Awards needing attention",
            "subtitle": f"{overdue_count} overdue · expiring within 60 days",
            "items": items,
            "empty_text": "All active awards are on track.",
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "Award dashboard",
            "title_badge": str(overdue_count) if overdue_count else None,
            "title_badge_color": "rose",
        }


# ── Manager: Review progress per call ────────────────────────────────────


# @register  # removed from dashboard per user request
class ManagerReviewProgressWidget(DashboardWidget):
    """Per-call review completion rate — how many shortlisted vs total submitted."""

    key = "rims.manager_review_progress"
    zone = ZONE_PRIMARY
    priority = 32
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count, Q
        Application, _, GrantCall = _grant_models()

        active_statuses = [
            Application.Status.SUBMITTED,
            Application.Status.UNDER_REVIEW,
            Application.Status.SHORTLISTED,
        ]

        rows = (
            GrantCall.objects.annotate(
                in_pipeline=Count(
                    "applications",
                    filter=Q(applications__status__in=active_statuses),
                ),
                submitted=Count(
                    "applications",
                    filter=Q(applications__status=Application.Status.SUBMITTED),
                ),
                shortlisted=Count(
                    "applications",
                    filter=Q(applications__status=Application.Status.SHORTLISTED),
                ),
            )
            .filter(in_pipeline__gt=0)
            .order_by("-submitted")[:7]
        )

        items = []
        for c in rows:
            reviewed = c.in_pipeline - c.submitted
            pct = int(reviewed / c.in_pipeline * 100) if c.in_pipeline else 0
            items.append({
                "label": c.title,
                "sublabel": (
                    f"{reviewed}/{c.in_pipeline} reviewed"
                    f" · {c.shortlisted} shortlisted"
                ),
                "value": f"{pct}%",
                "badge": "Complete" if pct == 100 else f"{c.submitted} pending",
                "badge_color": "emerald" if pct == 100 else ("amber" if c.submitted else "sky"),
                "href": reverse("rims_grants:call_detail_manage", kwargs={"slug": c.slug}),
            })

        return {
            "title": "Review progress",
            "subtitle": "Calls with active applications",
            "items": items,
            "empty_text": "No calls currently in review.",
            "footer_href": reverse("rims_grants:manager_all_applications"),
            "footer_label": "All applications",
        }


# ── Manager: Country breakdown of applications ────────────────────────────


# @register  # superseded by programme-specific widget
class ManagerGeographyWidget(DashboardWidget):
    """Top countries by application volume — geographic reach of the programme."""

    key = "rims.manager_geography"
    zone = ZONE_SIDE
    priority = 45
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES) and not has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count
        Application, _, _ = _grant_models()

        rows = (
            Application.objects.exclude(institution__isnull=True)
            .values("institution__country")
            .annotate(c=Count("id"))
            .order_by("-c")[:10]
        )

        total = Application.objects.count()
        items = []
        for r in rows:
            country = r["institution__country"] or "Unknown"
            count = r["c"]
            pct = int(count / total * 100) if total else 0
            items.append({
                "label": country,
                "value": f"{count:,}",
                "sublabel": f"{pct}% of applications",
            })

        country_count = (
            Application.objects.exclude(institution__isnull=True)
            .values("institution__country")
            .distinct()
            .count()
        )

        return {
            "title": "Geographic reach",
            "subtitle": f"Applications across {country_count} countries",
            "items": items,
            "empty_text": "No geographic data yet.",
        }


# ── Finance: KPIs (subset of management) ─────────────────────────────────


@register
class FinanceDisbursementsKPI(DashboardWidget):
    key = "rims.kpi.disbursements_pending"
    zone = ZONE_KPI
    priority = 25
    template = "core/dashboard/widgets/kpi_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        # Admins see this via the Platform Overview module snapshot already.
        return has_role(user, FINANCE_ROLES)

    def get_context(self, user) -> dict:
        try:
            from apps.rims.finance.models import DisbursementRequest
        except Exception:  # noqa: BLE001
            return {"label": "Pending disbursements", "value": "—"}
        # Best-effort across schemas: most have a status="pending" or similar.
        statuses = getattr(DisbursementRequest, "Status", None)
        n = DisbursementRequest.objects.exclude(
            status=getattr(statuses, "PAID", "paid") if statuses else "paid"
        ).count() if statuses else DisbursementRequest.objects.count()
        return {
            "label": "Pending disbursements",
            "value": f"{n:,}",
            "accent": "amber" if n else "primary",
            "hint": "Awaiting finance action",
            "href": reverse("rims_finance:disbursement_list"),
        }


# ── Applicant KPIs ───────────────────────────────────────────────────────


class _ApplicantKPIBase(DashboardWidget):
    zone = ZONE_KPI
    template = "core/dashboard/widgets/kpi_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES) and not has_role(user, RIMS_MANAGEMENT_ROLES)


@register
class ApplicantTotalApplicationsKPI(_ApplicantKPIBase):
    key = "rims.kpi.app_total"
    priority = 10

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        n = Application.objects.filter(applicant=user).count()
        return {
            "label": "My applications",
            "value": f"{n:,}",
            "hint": "All time",
            "href": reverse("rims_grants:application_list"),
        }


@register
class ApplicantOpenCallsKPI(_ApplicantKPIBase):
    key = "rims.kpi.app_open_calls"
    priority = 11

    def get_context(self, user) -> dict:
        _, _, GrantCall = _grant_models()
        n = GrantCall.objects.filter(
            status=GrantCall.Status.PUBLISHED, closes_at__gt=timezone.now()
        ).count()
        return {
            "label": "Open calls",
            "value": f"{n:,}",
            "accent": "emerald" if n else "primary",
            "hint": "Accepting applications",
            "href": reverse("rims_grants:call_list"),
        }


@register
class ApplicantDraftsKPI(_ApplicantKPIBase):
    key = "rims.kpi.app_drafts"
    priority = 12

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        n = Application.objects.filter(applicant=user, status=Application.Status.DRAFT).count()
        return {
            "label": "Drafts in progress",
            "value": f"{n:,}",
            "accent": "amber" if n else "primary",
            "hint": "Resume to submit" if n else "No drafts",
            "href": reverse("rims_grants:application_list"),
        }


@register
class ApplicantAwardedKPI(_ApplicantKPIBase):
    key = "rims.kpi.app_awarded"
    priority = 13

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        n = Application.objects.filter(applicant=user, status=Application.Status.AWARDED).count()
        return {
            "label": "Awarded grants",
            "value": f"{n:,}",
            "accent": "emerald" if n else "primary",
            "hint": "Congratulations" if n else "Keep applying",
        }


# ── Applicant: Open calls banner ─────────────────────────────────────────


@register
class IncompleteProfileBanner(DashboardWidget):
    """Nudges applicants to complete their candidate profile before they hit
    the hard block in ProfileCompleteRequiredMixin at apply-time — several
    eligibility rule types (sector/age/degree/nationality) evaluate these
    same fields, and an applicant who never filled them in fails those
    rules outright rather than being treated as "not applicable"."""

    key = "rims.incomplete_profile_banner"
    zone = ZONE_PRIMARY
    priority = 1
    template = "core/dashboard/widgets/banner_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES)

    def get_context(self, user) -> dict:
        from apps.rims.grants.profile_gate import missing_profile_fields

        missing = missing_profile_fields(user)
        if not missing:
            return {}
        return {
            "title": "Complete your profile before applying",
            "body": "Missing: " + ", ".join(missing) + ". Calls may screen on these fields.",
            "cta_label": "Complete profile",
            "cta_href": f"{reverse('accounts:profile')}?next={reverse('core:dashboard')}",
        }


@register
class OpenCallsBanner(DashboardWidget):
    key = "rims.open_calls_banner"
    zone = ZONE_PRIMARY
    priority = 5
    template = "core/dashboard/widgets/banner_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES)

    def get_context(self, user) -> dict:
        _, _, GrantCall = _grant_models()
        n = GrantCall.objects.filter(
            status=GrantCall.Status.PUBLISHED, closes_at__gt=timezone.now()
        ).count()
        if n == 0:
            return {}
        return {
            "title": f"{n} open call{'s' if n != 1 else ''} accepting applications",
            "body": "Active funding opportunities you can apply for now.",
            "cta_label": "Browse calls",
            "cta_href": reverse("rims_grants:call_list"),
        }


# ── Applicant: Open calls closing soon ───────────────────────────────────


@register
class ClosingSoonCallsWidget(DashboardWidget):
    key = "rims.closing_soon"
    zone = ZONE_PRIMARY
    priority = 10
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES)

    def get_context(self, user) -> dict:
        _, _, GrantCall = _grant_models()
        now = timezone.now()
        soon = now + timedelta(days=30)
        qs = (
            GrantCall.objects.filter(status=GrantCall.Status.PUBLISHED, closes_at__gt=now, closes_at__lte=soon)
            .order_by("closes_at")[:5]
        )
        items = []
        for call in qs:
            days_left = max(0, (call.closes_at - now).days)
            items.append({
                "label": call.title,
                "sublabel": f"Closes {call.closes_at.strftime('%b %d, %Y')}",
                "badge": f"{days_left}d left" if days_left else "Closing today",
                "badge_color": "amber" if days_left <= 7 else "neutral",
                "href": reverse("rims_grants:call_detail", kwargs={"slug": call.slug}),
            })
        return {
            "title": "Closing soon",
            "subtitle": "Calls closing in the next 30 days",
            "items": items,
            "empty_text": "No calls closing soon.",
            "footer_href": reverse("rims_grants:call_list"),
            "footer_label": "Browse all calls",
        }


# ── Applicant: My applications by status ─────────────────────────────────


@register
class MyApplicationsWidget(DashboardWidget):
    key = "rims.my_applications"
    zone = ZONE_PRIMARY
    priority = 20
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES)

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        qs = Application.objects.filter(applicant=user).select_related("call").order_by("-id")[:6]
        items = []
        for app in qs:
            color_map = {
                Application.Status.DRAFT: "neutral",
                Application.Status.SUBMITTED: "amber",
                Application.Status.UNDER_REVIEW: "sky",
                Application.Status.SHORTLISTED: "amber",
                Application.Status.AWARDED: "emerald",
                Application.Status.REJECTED: "rose",
            }
            items.append({
                "label": app.call.title,
                "sublabel": f"Status: {app.get_status_display()}",
                "badge": app.get_status_display(),
                "badge_color": color_map.get(app.status, "neutral"),
                "href": reverse("rims_grants:application_list"),
            })
        return {
            "title": "Your applications",
            "items": items,
            "empty_text": "You haven't applied to any calls yet.",
            "footer_href": reverse("rims_grants:application_list"),
            "footer_label": "View all applications",
        }


# ── Applicant: My applications status donut ──────────────────────────────


@register
class MyApplicationsDonut(DashboardWidget):
    key = "rims.my_apps_donut"
    zone = ZONE_SIDE
    priority = 20
    template = "core/dashboard/widgets/donut_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count

        Application, _, _ = _grant_models()
        rows = (
            Application.objects.filter(applicant=user)
            .values("status").annotate(c=Count("id"))
        )
        palette = {
            Application.Status.DRAFT: ("Draft", "#cbb98a"),
            Application.Status.SUBMITTED: ("Submitted", "#d97706"),
            Application.Status.UNDER_REVIEW: ("Under review", "#0284c7"),
            Application.Status.SHORTLISTED: ("Shortlisted", "#7c3aed"),
            Application.Status.AWARDED: ("Awarded", "#059669"),
            Application.Status.REJECTED: ("Rejected", "#94a3b8"),
        }
        segs, labels, values, colors = [], [], [], []
        total = 0
        for r in rows:
            if r["status"] not in palette:
                continue
            label, color = palette[r["status"]]
            segs.append({"label": label, "value": r["c"], "color": color})
            labels.append(label)
            values.append(r["c"])
            colors.append(color)
            total += r["c"]
        return {
            "title": "Your application status",
            "segments": segs,
            "labels_json": json.dumps(labels),
            "values_json": json.dumps(values),
            "colors_json": json.dumps(colors),
            "total": total,
            "total_label": "submitted",
            "footer_href": reverse("rims_grants:application_list"),
            "footer_label": "View applications",
        }


# ── Applicant: Recent decisions / status changes (from notifications) ────


@register
class ApplicantRecentDecisionsWidget(DashboardWidget):
    key = "rims.applicant_recent_decisions"
    zone = ZONE_SIDE
    priority = 25
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES) and not has_role(user, RIMS_MANAGEMENT_ROLES)

    def get_context(self, user) -> dict:
        from apps.core.notifications.models import Notification
        verbs = (
            Notification.Verb.APPLICATION_STATUS,
            Notification.Verb.GRANT_AWARDED,
            Notification.Verb.CALL_PUBLISHED,
        )
        qs = (
            Notification.objects.filter(recipient=user, verb__in=verbs)
            .order_by("-created_at")[:6]
        )
        items = []
        for n in qs:
            items.append({
                "label": n.message[:120],
                "sublabel": timezone.localtime(n.created_at).strftime("%b %d, %H:%M"),
                "badge": n.get_verb_display(),
                "badge_color": "emerald" if n.verb == Notification.Verb.GRANT_AWARDED else "sky",
            })
        return {
            "title": "Recent decisions",
            "subtitle": "Status updates on your applications",
            "items": items,
            "empty_text": "No status updates yet.",
        }


# ── Applicant: Quick actions ─────────────────────────────────────────────


@register
class ApplicantQuickActions(DashboardWidget):
    key = "rims.applicant_actions"
    zone = ZONE_ACTIONS
    priority = 30
    template = "core/dashboard/widgets/quick_actions.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_APPLICANT_ROLES) and not has_role(user, RIMS_MANAGEMENT_ROLES)

    def get_context(self, user) -> dict:
        Application, _, _ = _grant_models()
        has_draft = Application.objects.filter(applicant=user, status=Application.Status.DRAFT).exists()
        actions = [
            {
                "label": "Browse calls",
                "href": reverse("rims_grants:call_list"),
                "primary": True,
                "icon": "M21 21l-4.35-4.35M11 18a7 7 0 100-14 7 7 0 000 14z",
            },
            {
                "label": "My applications",
                "href": reverse("rims_grants:application_list"),
                "icon": "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4",
            },
        ]
        if has_draft:
            actions.append({
                "label": "Continue draft",
                "href": reverse("rims_grants:application_list"),
                "alert": True,
                "icon": "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z",
            })
        return {"actions": actions}


# ── Reviewer / Judge: KPIs, queues, recent activity ──────────────────────


def _review_qs():
    from apps.rims.grants.models import Review
    return Review


class _ReviewerKPIBase(DashboardWidget):
    zone = ZONE_KPI
    template = "core/dashboard/widgets/kpi_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, REVIEWER_ROLES)


@register
class ReviewerPendingKPI(_ReviewerKPIBase):
    key = "rims.kpi.reviewer_pending"
    priority = 15

    def get_context(self, user) -> dict:
        Review = _review_qs()
        n = Review.objects.filter(reviewer=user, submitted_at__isnull=True).count()
        return {
            "label": "Reviews to do",
            "value": f"{n:,}",
            "accent": "amber" if n else "primary",
            "hint": "Pending your decision" if n else "Inbox empty",
            "href": reverse("rims_grants:review_queue"),
        }


@register
class ReviewerCompletedKPI(_ReviewerKPIBase):
    key = "rims.kpi.reviewer_completed"
    priority = 16

    def get_context(self, user) -> dict:
        Review = _review_qs()
        total = Review.objects.filter(reviewer=user, submitted_at__isnull=False).count()
        this_month = Review.objects.filter(
            reviewer=user,
            submitted_at__isnull=False,
            submitted_at__year=timezone.now().year,
            submitted_at__month=timezone.now().month,
        ).count()
        return {
            "label": "Reviews completed",
            "value": f"{total:,}",
            "accent": "emerald" if total else "primary",
            "hint": f"{this_month} this month",
        }


@register
class ReviewerTrendKPI(_ReviewerKPIBase):
    key = "rims.kpi.reviewer_trend"
    priority = 17

    def get_context(self, user) -> dict:
        from django.db.models import Count
        from django.db.models.functions import TruncDate

        Review = _review_qs()
        start = (timezone.now() - timedelta(days=29)).replace(hour=0, minute=0, second=0, microsecond=0)
        rows = (
            Review.objects.filter(reviewer=user, submitted_at__gte=start)
            .annotate(day=TruncDate("submitted_at"))
            .values("day").annotate(c=Count("id")).order_by("day")
        )
        by_day = {str(r["day"]): r["c"] for r in rows}
        labels: list[str] = []
        values: list[int] = []
        total = 0
        for i in range(30):
            day = (start + timedelta(days=i)).date()
            v = by_day.get(str(day), 0)
            labels.append(day.strftime("%d"))
            values.append(v)
            total += v
        return {
            "label": "Reviews (30d)",
            "value": f"{total:,}",
            "hint": "Submissions you've completed",
            "sparkline": {"labels_json": json.dumps(labels), "values_json": json.dumps(values)},
        }


@register
class ReviewerTurnaroundKPI(_ReviewerKPIBase):
    key = "rims.kpi.reviewer_turnaround"
    priority = 18

    def get_context(self, user) -> dict:
        from django.db.models import Avg, ExpressionWrapper, F, fields

        Review = _review_qs()
        recent = Review.objects.filter(
            reviewer=user, submitted_at__isnull=False, assigned_at__isnull=False
        ).order_by("-submitted_at")[:30]
        total_days = 0
        n = 0
        for r in recent:
            if r.submitted_at and r.assigned_at:
                total_days += max(0, (r.submitted_at - r.assigned_at).days)
                n += 1
        avg = round(total_days / n, 1) if n else 0
        return {
            "label": "Avg turnaround",
            "value": f"{avg}d" if n else "—",
            "hint": f"Last {n} reviews" if n else "No data yet",
        }


@register
class ReviewerQueueWidget(DashboardWidget):
    key = "rims.reviewer_queue"
    zone = ZONE_PRIMARY
    priority = 15
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, REVIEWER_ROLES)

    def get_context(self, user) -> dict:
        Review = _review_qs()
        qs = (
            Review.objects.filter(reviewer=user, submitted_at__isnull=True)
            .select_related("application", "application__call", "application__applicant")
            .order_by("due_at", "assigned_at")[:8]
        )
        items = []
        now = timezone.now()
        for r in qs:
            call = getattr(r.application, "call", None)
            sublabel_bits = []
            if r.due_at:
                days_left = (r.due_at - now).days
                if days_left < 0:
                    sublabel_bits.append(f"Overdue by {-days_left}d")
                else:
                    sublabel_bits.append(f"Due in {days_left}d")
            elif r.assigned_at:
                sublabel_bits.append(f"Assigned {(now - r.assigned_at).days}d ago")
            badge_color = "rose" if (r.due_at and r.due_at < now) else "amber"
            items.append({
                "label": call.title if call else f"Application #{r.application_id}",
                "sublabel": " · ".join(sublabel_bits) or "Awaiting your review",
                "href": reverse("rims_grants:review_submit", args=[r.pk]),
                "badge": "Overdue" if (r.due_at and r.due_at < now) else "Pending",
                "badge_color": badge_color,
            })
        return {
            "title": "Your review queue",
            "subtitle": "Sorted by due date",
            "items": items,
            "empty_text": "No reviews assigned to you.",
            "footer_href": reverse("rims_grants:review_queue"),
            "footer_label": "Open reviewer queue",
        }


@register
class ReviewerRecentlyCompletedWidget(DashboardWidget):
    key = "rims.reviewer_completed_list"
    zone = ZONE_SIDE
    priority = 25
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, REVIEWER_ROLES)

    def get_context(self, user) -> dict:
        Review = _review_qs()
        qs = (
            Review.objects.filter(reviewer=user, submitted_at__isnull=False)
            .select_related("application", "application__call")
            .order_by("-submitted_at")[:5]
        )
        items = []
        for r in qs:
            call = getattr(r.application, "call", None)
            color = {
                "approve": "emerald",
                "revise": "amber",
                "reject": "rose",
                "hold": "sky",
            }.get(r.recommendation_code or "", "neutral")
            items.append({
                "label": call.title if call else f"Application #{r.application_id}",
                "sublabel": f"Submitted {r.submitted_at.strftime('%b %d')}",
                "badge": (r.get_recommendation_code_display() or "Submitted") if r.recommendation_code else "Submitted",
                "badge_color": color,
            })
        return {
            "title": "Recently completed",
            "items": items,
            "empty_text": "No completed reviews yet.",
        }


@register
class ReviewerQuickActions(DashboardWidget):
    key = "rims.reviewer_actions"
    zone = ZONE_ACTIONS
    priority = 25
    template = "core/dashboard/widgets/quick_actions.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, REVIEWER_ROLES)

    def get_context(self, user) -> dict:
        Review = _review_qs()
        pending = Review.objects.filter(reviewer=user, submitted_at__isnull=True).count()
        return {
            "actions": [
                {
                    "label": "Reviewer queue",
                    "href": reverse("rims_grants:review_queue"),
                    "primary": True,
                    "icon": "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4",
                    "alert": pending > 0,
                    "badge": pending if pending else None,
                },
                {
                    "label": "All grant calls",
                    "href": reverse("rims_grants:call_list"),
                    "icon": "M21 21l-4.35-4.35M11 18a7 7 0 100-14 7 7 0 000 14z",
                },
            ],
        }


# ── Finance: tactical KPIs, queues, recoveries ───────────────────────────


class _FinanceBase(DashboardWidget):
    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, FINANCE_ROLES)


def _tranche_models():
    from apps.rims.grants.models import AwardTranche, RecoveryRequest

    return AwardTranche, RecoveryRequest


@register
class FinanceOverdueTranchesKPI(_FinanceBase):
    key = "rims.kpi.finance_overdue_tranches"
    zone = ZONE_KPI
    priority = 26
    template = "core/dashboard/widgets/kpi_card.html"

    def get_context(self, user) -> dict:
        AwardTranche, _ = _tranche_models()
        today = timezone.now().date()
        n = AwardTranche.objects.filter(
            status__in=[
                AwardTranche.Status.PLANNED,
                AwardTranche.Status.REQUESTED,
                AwardTranche.Status.APPROVED,
            ],
            due_on__lt=today,
        ).count()
        return {
            "label": "Overdue tranches",
            "value": f"{n:,}",
            "accent": "rose" if n else "primary",
            "hint": "Past due_on, not yet disbursed",
        }


@register
class FinanceTrancheQueueWidget(_FinanceBase):
    key = "rims.finance_tranche_queue"
    zone = ZONE_PRIMARY
    priority = 30
    template = "core/dashboard/widgets/list_card.html"

    def get_context(self, user) -> dict:
        AwardTranche, _ = _tranche_models()
        items: list = []
        try:
            qs = (
                AwardTranche.objects.filter(
                    status__in=[
                        AwardTranche.Status.REQUESTED,
                        AwardTranche.Status.APPROVED,
                    ],
                )
                .select_related("award", "award__call")
                .order_by("due_on")[:6]
            )
            color_map = {
                "requested": "amber",
                "approved": "sky",
            }
            for t in qs:
                title = t.award.call.title if t.award and t.award.call else t.label
                due = t.due_on.strftime("%b %d") if t.due_on else "no due date"
                items.append({
                    "label": f"{t.label} — {title}",
                    "sublabel": f"Due {due} · {t.amount:,.0f}",
                    "badge": t.get_status_display(),
                    "badge_color": color_map.get(t.status, "neutral"),
                    "href": reverse("rims_grants:award_detail", args=[t.award_id]),
                })
        except Exception:  # noqa: BLE001
            pass
        return {
            "title": "Tranches awaiting disbursement",
            "subtitle": "Requested or approved",
            "items": items,
            "empty_text": "No tranches awaiting action.",
            "footer_href": reverse("rims_grants:award_dashboard"),
            "footer_label": "Open award dashboard",
        }


@register
class FinanceRecentDisbursementsWidget(_FinanceBase):
    key = "rims.finance_recent_disbursements"
    zone = ZONE_PRIMARY
    priority = 35
    template = "core/dashboard/widgets/list_card.html"

    def get_context(self, user) -> dict:
        AwardTranche, _ = _tranche_models()
        items: list = []
        try:
            qs = (
                AwardTranche.objects.filter(status=AwardTranche.Status.DISBURSED)
                .select_related("award", "award__call")
                .order_by("-disbursed_at")[:6]
            )
            for t in qs:
                title = t.award.call.title if t.award and t.award.call else t.label
                when = timezone.localtime(t.disbursed_at).strftime("%b %d") if t.disbursed_at else ""
                items.append({
                    "label": f"{t.label} — {title}",
                    "sublabel": f"Disbursed {when}",
                    "value": f"{t.amount:,.0f}",
                    "href": reverse("rims_grants:award_detail", args=[t.award_id]),
                })
        except Exception:  # noqa: BLE001
            pass
        return {
            "title": "Recent disbursements",
            "subtitle": "Latest tranche payments",
            "items": items,
            "empty_text": "No disbursements yet.",
        }


@register
class FinanceRecoveryRequestsWidget(_FinanceBase):
    key = "rims.finance_recovery_requests"
    zone = ZONE_SIDE
    priority = 35
    template = "core/dashboard/widgets/list_card.html"

    def get_context(self, user) -> dict:
        _, RecoveryRequest = _tranche_models()
        items: list = []
        try:
            qs = (
                RecoveryRequest.objects.select_related("closeout", "closeout__award", "closeout__award__call")
                .order_by("-created_at")[:5]
            )
            for r in qs:
                award = r.closeout.award if r.closeout else None
                title = award.call.title if award and award.call else "Recovery"
                received = r.received_on.strftime("%b %d") if r.received_on else "Pending"
                items.append({
                    "label": title,
                    "sublabel": f"{received} · {r.amount:,.0f}",
                    "badge": "Pending" if not r.received_on else "Received",
                    "badge_color": "amber" if not r.received_on else "emerald",
                })
        except Exception:  # noqa: BLE001
            pass
        return {
            "title": "Recovery requests",
            "subtitle": "Closeout adjustments",
            "items": items,
            "empty_text": "No recovery requests.",
        }


@register
class FinanceQuickActions(_FinanceBase):
    key = "rims.finance_actions"
    zone = ZONE_ACTIONS
    priority = 22
    template = "core/dashboard/widgets/quick_actions.html"

    def get_context(self, user) -> dict:
        return {
            "actions": [
                {
                    "label": "Award dashboard",
                    "href": reverse("rims_grants:award_dashboard"),
                    "primary": True,
                    "icon": "M9 19V6l11-3v13M9 19a3 3 0 11-6 0 3 3 0 016 0zM20 16a3 3 0 11-6 0 3 3 0 016 0zM9 10l11-3",
                },
                {
                    "label": "All grant calls",
                    "href": reverse("rims_grants:manager_call_list"),
                    "icon": "M21 21l-4.35-4.35M11 18a7 7 0 100-14 7 7 0 000 14z",
                },
            ],
        }


# ═══════════════════════════════════════════════════════════════════════════
# SCHOLAR / STUDENT SELF-SERVICE DASHBOARD WIDGETS
# Shown to users with role=scholar — links into the student portal.
# ═══════════════════════════════════════════════════════════════════════════

def _is_scholar(user) -> bool:
    from apps.core.dashboard.role_groups import role_of
    from apps.core.permissions.roles import UserRole
    return role_of(user) == UserRole.SCHOLAR


# ── Scholar KPI zone — enrolment status, reports, stipends ───────────────

class _ScholarStudentKPIBase(DashboardWidget):
    zone = ZONE_KPI_SCHOLARSHIP
    template = "core/dashboard/widgets/kpi_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_scholar(user)


@register
class ScholarEnrolmentStatusKPI(_ScholarStudentKPIBase):
    key = "rims.scholar.enrolment_status"
    priority = 1

    def get_context(self, user) -> dict:
        from apps.rims.scholarships.models import Scholar
        scholars = Scholar.objects.filter(user=user).select_related("scholarship")
        count = scholars.count()
        if count == 0:
            return {
                "label": "My scholarships",
                "value": "0",
                "hint": "Not enrolled in any programme",
                "href": reverse("rims_scholarships:my_scholarships"),
            }
        active = scholars.filter(status__in=["enrolled", "active"]).count()
        latest = scholars.order_by("-enrolled_at").first()
        return {
            "label": "My scholarship",
            "value": latest.get_status_display(),
            "hint": latest.scholarship.name[:50] if latest else "",
            "accent": "emerald" if active else "amber",
            "href": reverse("rims_scholarships:my_dashboard"),
        }


@register
class ScholarProgressReportsKPI(_ScholarStudentKPIBase):
    key = "rims.scholar.progress_reports"
    priority = 2

    def get_context(self, user) -> dict:
        from apps.rims.scholarships.models import ProgressReport, Scholar
        scholar_ids = Scholar.objects.filter(user=user).values_list("pk", flat=True)
        total = ProgressReport.objects.filter(scholar_id__in=scholar_ids).count()
        pending = ProgressReport.objects.filter(
            scholar_id__in=scholar_ids,
            status="revision_required",
        ).count()
        return {
            "label": "Progress reports",
            "value": f"{total:,}",
            "hint": f"{pending} need revision" if pending else "All up to date",
            "accent": "amber" if pending else "primary",
            "href": reverse("rims_scholarships:my_dashboard"),
        }


@register
class ScholarStipendsKPI(_ScholarStudentKPIBase):
    key = "rims.scholar.stipends"
    priority = 3

    def get_context(self, user) -> dict:
        from apps.rims.scholarships.models import Scholar, Stipend
        scholar_ids = Scholar.objects.filter(user=user).values_list("pk", flat=True)
        total = Stipend.objects.filter(scholar_id__in=scholar_ids, status="paid").count()
        latest = Stipend.objects.filter(scholar_id__in=scholar_ids, status="paid").order_by("-paid_on").first()
        hint = f"Last: {latest.paid_on.strftime('%b %Y')}" if latest else "None recorded yet"
        return {
            "label": "Stipends received",
            "value": f"{total:,}",
            "hint": hint,
            "href": reverse("rims_scholarships:my_dashboard"),
        }


@register
class ScholarREPCoursesKPI(_ScholarStudentKPIBase):
    key = "rims.scholar.rep_courses"
    priority = 4

    def get_context(self, user) -> dict:
        # Scholar course completions came from the REP LMS (retired for Moodle).
        # A Moodle-backed count can be sourced from MEL Moodle DataPoints later.
        completed = 0
        return {
            "label": "Courses completed",
            "value": f"{completed:,}",
            "hint": "eLearning (Moodle)",
            "accent": "primary",
            "href": "",
        }


# ── Scholar quick actions ─────────────────────────────────────────────────

@register
class ScholarQuickActions(DashboardWidget):
    key = "rims.scholar.quick_actions"
    zone = ZONE_ACTIONS
    priority = 5
    template = "core/dashboard/widgets/quick_actions.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_scholar(user)

    def get_context(self, user) -> dict:
        from apps.rims.scholarships.models import Scholar
        scholar = Scholar.objects.filter(user=user).order_by("-enrolled_at").first()
        actions = [
            {
                "label": "My studies dashboard",
                "href": reverse("rims_scholarships:my_dashboard"),
                "primary": True,
                "icon": "M12 14l9-5-9-5-9 5 9 5z M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z",
            },
        ]
        if scholar:
            actions += [
                {
                    "label": "Submit progress report",
                    "href": reverse("rims_scholarships:my_progress_report_create", kwargs={"scholar_pk": scholar.pk}),
                    "icon": "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
                },
                {
                    "label": "Update academic profile",
                    "href": reverse("rims_scholarships:my_academic_profile", kwargs={"scholar_pk": scholar.pk}),
                    "icon": "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
                },
            ]
            if not hasattr(scholar, "graduation") and scholar.status in ("active", "completed"):
                actions.append({
                    "label": "Submit graduation",
                    "href": reverse("rims_scholarships:my_graduation_submit", kwargs={"scholar_pk": scholar.pk}),
                    "icon": "M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z",
                })
        return {"actions": actions}


# ── Scholar: My scholarship details list card ────────────────────────────

@register
class ScholarMyEnrolmentsWidget(DashboardWidget):
    key = "rims.scholar.my_enrolments"
    zone = ZONE_PRIMARY
    priority = 5
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_scholar(user)

    def get_context(self, user) -> dict:
        from apps.rims.scholarships.models import Scholar
        scholars = (
            Scholar.objects.filter(user=user)
            .select_related("scholarship", "institution")
            .order_by("-enrolled_at")
        )
        status_colors = {
            "enrolled": "sky",
            "active": "emerald",
            "paused": "amber",
            "deferred": "amber",
            "completed": "violet",
            "withdrawn": "rose",
            "discontinued": "rose",
        }
        items = []
        for s in scholars:
            items.append({
                "label": s.scholarship.name,
                "sublabel": (
                    f"{s.get_degree_level_display()} · {s.institution.name}"
                    if s.institution_id else s.get_degree_level_display()
                ) or "—",
                "badge": s.get_status_display(),
                "badge_color": status_colors.get(s.status, "neutral"),
                "href": reverse("rims_scholarships:my_scholarship_detail", kwargs={"pk": s.pk}),
            })
        return {
            "title": "My scholarships",
            "subtitle": "Click a programme to view details, reports and stipends",
            "items": items,
            "empty_text": "You are not enrolled in any scholarship programme yet.",
            "footer_href": reverse("rims_scholarships:my_dashboard"),
            "footer_label": "Open studies dashboard",
        }


# ── Scholar: Recent progress reports ────────────────────────────────────

@register
class ScholarRecentReportsWidget(DashboardWidget):
    key = "rims.scholar.recent_reports"
    zone = ZONE_SIDE
    priority = 10
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return _is_scholar(user)

    def get_context(self, user) -> dict:
        from apps.rims.scholarships.models import ProgressReport, Scholar
        scholar_ids = Scholar.objects.filter(user=user).values_list("pk", flat=True)
        reports = (
            ProgressReport.objects.filter(scholar_id__in=scholar_ids)
            .order_by("-submitted_at")[:5]
        )
        status_colors = {
            "draft": "neutral",
            "submitted": "amber",
            "accepted": "emerald",
            "revision_required": "rose",
        }
        items = []
        for r in reports:
            items.append({
                "label": r.period_label,
                "sublabel": r.submitted_at.strftime("%b %d, %Y") if r.submitted_at else "—",
                "badge": r.get_status_display(),
                "badge_color": status_colors.get(r.status, "neutral"),
            })
        first_scholar = Scholar.objects.filter(user=user).order_by("-enrolled_at").first()
        return {
            "title": "Recent reports",
            "subtitle": "Your progress report history",
            "items": items,
            "empty_text": "No progress reports submitted yet.",
            "footer_href": reverse("rims_scholarships:my_progress_report_create", kwargs={"scholar_pk": first_scholar.pk}) if first_scholar else reverse("rims_scholarships:my_dashboard"),
            "footer_label": "Submit new report",
        }


# ═══════════════════════════════════════════════════════════════════════════
# MANAGER / GRANTS-MANAGER: SCHOLAR OVERSIGHT WIDGETS
# ═══════════════════════════════════════════════════════════════════════════


@register
class ScholarOverviewWidget(DashboardWidget):
    """Manager view: scholar pipeline by status with clickable links."""
    key = "rims.manager.scholar_overview"
    zone = ZONE_PRIMARY
    priority = 55
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES)

    def get_context(self, user) -> dict:
        from django.db.models import Count
        from apps.rims.scholarships.models import Scholar

        status_order = [
            Scholar.Status.ACTIVE,
            Scholar.Status.ENROLLED,
            Scholar.Status.PAUSED,
            Scholar.Status.DEFERRED,
            Scholar.Status.COMPLETED,
            Scholar.Status.WITHDRAWN,
            Scholar.Status.DISCONTINUED,
        ]
        counts = {
            row["status"]: row["n"]
            for row in Scholar.objects.values("status").annotate(n=Count("id"))
        }
        total = sum(counts.values())

        color_map = {
            Scholar.Status.ACTIVE:       "emerald",
            Scholar.Status.ENROLLED:     "sky",
            Scholar.Status.PAUSED:       "amber",
            Scholar.Status.DEFERRED:     "amber",
            Scholar.Status.COMPLETED:    "violet",
            Scholar.Status.WITHDRAWN:    "rose",
            Scholar.Status.DISCONTINUED: "rose",
        }
        items = []
        for st in status_order:
            n = counts.get(st, 0)
            if n == 0:
                continue
            label_map = dict(Scholar.Status.choices)
            items.append({
                "label": label_map.get(st, st),
                "sublabel": f"{n:,} scholar{'s' if n != 1 else ''}",
                "badge": f"{n:,}",
                "badge_color": color_map.get(st, "neutral"),
                "href": f"{reverse('rims_scholarships:scholar_list')}?status={st}",
            })

        return {
            "title": "Scholars by status",
            "subtitle": f"{total:,} total scholars across all programmes",
            "items": items,
            "footer_href": reverse("rims_scholarships:scholar_list"),
            "footer_label": "View all scholars →",
        }


@register
class ScholarOverdueReportsWidget(DashboardWidget):
    """Manager view: scholars with overdue progress reports."""
    key = "rims.manager.overdue_reports"
    zone = ZONE_SIDE
    priority = 55
    template = "core/dashboard/widgets/list_card.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES)

    def get_context(self, user) -> dict:
        from datetime import timedelta
        from apps.rims.scholarships.models import Scholar

        cutoff = timezone.now() - timedelta(days=180)
        overdue = (
            Scholar.objects.filter(
                status__in=[Scholar.Status.ACTIVE, Scholar.Status.ENROLLED],
                enrolled_at__lte=cutoff,
            )
            .exclude(progress_reports__submitted_at__gte=cutoff)
            .select_related("user", "scholarship")
            .distinct()[:8]
        )
        items = []
        for s in overdue:
            name = s.user.get_full_name() or s.user.email
            items.append({
                "label": name,
                "sublabel": s.scholarship.name[:50],
                "badge": "Overdue",
                "badge_color": "rose",
                "href": reverse("rims_scholarships:scholar_detail", kwargs={"pk": s.pk}),
            })
        return {
            "title": "Overdue progress reports",
            "subtitle": "Active scholars with no report in 6 months",
            "items": items,
            "empty_text": "No overdue reports — all scholars are up to date.",
            "footer_href": reverse("rims_scholarships:officer_dashboard"),
            "footer_label": "Officer dashboard →",
        }


@register
class ScholarManagerQuickActions(DashboardWidget):
    """Quick-action buttons for scholar management on the manager dashboard."""
    key = "rims.manager.scholar_actions"
    zone = ZONE_ACTIONS
    priority = 45
    template = "core/dashboard/widgets/quick_actions.html"

    @classmethod
    def is_enabled(cls, user) -> bool:
        return has_role(user, RIMS_DASHBOARD_MANAGER_ROLES)

    def get_context(self, user) -> dict:
        from apps.rims.scholarships.models import Scholar, ProgressReport
        from django.db.models import Count
        from datetime import timedelta

        total_scholars = Scholar.objects.filter(
            status__in=[Scholar.Status.ACTIVE, Scholar.Status.ENROLLED]
        ).count()

        cutoff = timezone.now() - timedelta(days=180)
        overdue_count = (
            Scholar.objects.filter(
                status__in=[Scholar.Status.ACTIVE, Scholar.Status.ENROLLED],
                enrolled_at__lte=cutoff,
            )
            .exclude(progress_reports__submitted_at__gte=cutoff)
            .distinct()
            .count()
        )

        pending_reports = ProgressReport.objects.filter(status="submitted").count()

        actions = [
            {
                "label": f"Students ({total_scholars:,})",
                "href": reverse("rims_scholarships:scholar_list"),
                "primary": True,
                "icon": "M12 14l9-5-9-5-9 5 9 5z M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z",
            },
            {
                "label": f"Pending reports{f' ({pending_reports})' if pending_reports else ''}",
                "href": reverse("rims_scholarships:officer_dashboard"),
                "icon": "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
            },
            {
                "label": "Enrol scholar",
                "href": reverse("rims_scholarships:scholar_enrol"),
                "icon": "M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z",
            },
            {
                "label": "Stipends",
                "href": reverse("rims_scholarships:stipend_list"),
                "icon": "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
            },
        ]
        if overdue_count:
            actions.append({
                "label": f"{overdue_count} overdue reports",
                "href": reverse("rims_scholarships:officer_dashboard"),
                "icon": "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
            })
        return {"actions": actions}
