from django.conf import settings
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.permissions import BasePermission
from rest_framework.response import Response

from apps.core.tenancy import get_active_organization
from apps.core.models import AuditLog, Comment, Invitation, User
from apps.core.permissions import HasMenuPermission
from apps.meetings.models import Meeting
from apps.projects.models import (
    Deliverable,
    FundDocument,
    FundReport,
    Project,
    ProjectFund,
    ProjectFundAllocation,
    ProjectFundTranche,
    ProjectIndicator,
    ProjectRisk,
    ProjectSchedule,
)
from apps.notifications.utils import notify
from apps.notifications.email import _send
from .serializers import (
    DonorProjectSerializer,
    DonorFundSerializer,
    DonorFundAllocationSerializer,
    DonorFundTrancheSerializer,
    DonorFundReportSerializer,
    DonorFundDocumentSerializer,
    DonorCommentSerializer,
    DonorDeliverableSerializer,
    DonorMilestoneSerializer,
    DonorIndicatorSerializer,
    DonorRiskSerializer,
    DonorMeetingSerializer,
    DonorTeamUserSerializer,
    DonorTeamInviteSerializer,
    DonorInviteSerializer,
)


class IsDonor(BasePermission):
    """organization_role='donor' accounts, plus superusers (for support/QA
    preview of the donor portal)."""

    def has_permission(self, request, view):
        user = request.user
        if not (user and user.is_authenticated):
            return False
        return user.organization_role == "donor" or user.is_superuser


def _require_donor_admin(request):
    """Reviews and team management need the donor-admin role (superusers pass
    for support/QA preview)."""
    user = request.user
    if user.is_superuser:
        return
    if user.donor_role != "admin":
        raise PermissionDenied("Only donor administrators may do this.")


def _donor_funds_queryset(request):
    """This donor's funds within their active organization. A superuser sees
    every fund in the org (support/QA preview, not scoped to one donor).
    Empty if a donor account has no donor_company (shouldn't happen, but
    fails closed rather than leaking every fund)."""
    org = get_active_organization(request)
    if request.user.is_superuser:
        return ProjectFund.objects.filter(organization=org).select_related("project", "organization")
    company_id = request.user.donor_company_id
    if not company_id:
        return ProjectFund.objects.none()
    return ProjectFund.objects.filter(donor_id=company_id, organization=org).select_related("project", "organization")


class DonorFundViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: the requesting donor's own funds."""
    permission_classes = [IsDonor]
    serializer_class = DonorFundSerializer

    def get_queryset(self):
        return _donor_funds_queryset(self.request)


def _donor_project_ids(request):
    return _donor_funds_queryset(request).exclude(project__isnull=True).values_list("project_id", flat=True)


def _notify_project_managers(fund, actor, *, title, message, link):
    """Notify the fund's project owner/manager staff of a donor action."""
    if not fund.project_id:
        return
    for member in fund.project.team_members.filter(role__in=["owner", "manager"]).select_related("user"):
        if member.user_id and member.user_id != actor.id:
            notify(member.user, title=title, message=message, link=link,
                   notification_type="project", icon="folder")


def _apply_donor_review(viewset, request):
    """Shared donor review handler for reports and deliverables: role check,
    legal-transition check, audit log, staff notification."""
    _require_donor_admin(request)
    item = viewset.get_object()
    action_name = request.data.get("action", "")
    note = request.data.get("note", "")
    previous_status = item.status
    try:
        item.apply_review(action_name, request.user, note)
    except ValueError as exc:
        return Response({"detail": str(exc)}, status=400)
    AuditLog.objects.create(
        organization=item.fund.organization,
        user=request.user,
        action=f"donor_{action_name}",
        model_name=type(item).__name__,
        object_id=item.id,
        changes={"status": [previous_status, item.status], "note": note},
    )
    actor = request.user.get_full_name() or request.user.email
    _notify_project_managers(
        item.fund, request.user,
        title=f"Donor {item.get_status_display().lower()}",
        message=f"{actor} set '{item.title}' to {item.get_status_display()}.",
        link=f"/project/funds/{item.fund_id}",
    )
    return Response(viewset.get_serializer(item).data)


class DonorProjectViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: projects the requesting donor's funds support (My Projects)."""
    permission_classes = [IsDonor]
    serializer_class = DonorProjectSerializer

    def get_queryset(self):
        return Project.objects.filter(id__in=_donor_project_ids(self.request))


class DonorFundAllocationViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: allocations on the requesting donor's own funds. Supports
    ?fund=<id> to scope to one fund (the fund detail page's normal use)."""
    permission_classes = [IsDonor]
    serializer_class = DonorFundAllocationSerializer

    def get_queryset(self):
        qs = ProjectFundAllocation.objects.filter(fund__in=_donor_funds_queryset(self.request)).select_related("budget_item")
        fund_id = self.request.query_params.get("fund")
        return qs.filter(fund_id=fund_id) if fund_id else qs


class DonorFundTrancheViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: tranches on the requesting donor's own funds. Supports ?fund=<id>."""
    permission_classes = [IsDonor]
    serializer_class = DonorFundTrancheSerializer

    def get_queryset(self):
        qs = ProjectFundTranche.objects.filter(fund__in=_donor_funds_queryset(self.request))
        fund_id = self.request.query_params.get("fund")
        return qs.filter(fund_id=fund_id) if fund_id else qs


class DonorFundReportViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: reports owed on the requesting donor's own funds. Supports
    ?fund=<id> and ?kind=narrative|financial|final — the portal's Results &
    Impact, Reports, and Deliverables sections are this same data filtered
    by kind."""
    permission_classes = [IsDonor]
    serializer_class = DonorFundReportSerializer

    def get_queryset(self):
        qs = FundReport.objects.filter(fund__in=_donor_funds_queryset(self.request)).select_related("fund")
        fund_id = self.request.query_params.get("fund")
        if fund_id:
            qs = qs.filter(fund_id=fund_id)
        kind = self.request.query_params.get("kind")
        if kind:
            qs = qs.filter(kind=kind)
        return qs

    @action(detail=True, methods=["post"])
    def review(self, request, pk=None):
        """Donor decision on a submitted report: approve / request_revision / reject."""
        return _apply_donor_review(self, request)


class DonorFundDocumentViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: documents on the requesting donor's own funds. Supports ?fund=<id>."""
    permission_classes = [IsDonor]
    serializer_class = DonorFundDocumentSerializer

    def get_queryset(self):
        qs = FundDocument.objects.filter(fund__in=_donor_funds_queryset(self.request)).select_related("fund")
        fund_id = self.request.query_params.get("fund")
        return qs.filter(fund_id=fund_id) if fund_id else qs


class DonorDeliverableViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only + review: donor-visible deliverables on the donor's own funds.
    Supports ?fund=<id> and ?status=<donor status>."""
    permission_classes = [IsDonor]
    serializer_class = DonorDeliverableSerializer

    def get_queryset(self):
        qs = Deliverable.objects.filter(
            fund__in=_donor_funds_queryset(self.request), donor_visible=True,
        ).select_related("fund", "fund__project")
        fund_id = self.request.query_params.get("fund")
        return qs.filter(fund_id=fund_id) if fund_id else qs

    @action(detail=True, methods=["post"])
    def review(self, request, pk=None):
        """Donor decision on a submitted deliverable: approve / request_revision / reject."""
        return _apply_donor_review(self, request)


class DonorMilestoneViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: donor-visible timeline items on the donor's projects.
    Supports ?project=<id>."""
    permission_classes = [IsDonor]
    serializer_class = DonorMilestoneSerializer

    def get_queryset(self):
        qs = ProjectSchedule.objects.filter(
            project_id__in=_donor_project_ids(self.request), donor_visible=True,
        ).select_related("project")
        project_id = self.request.query_params.get("project")
        return qs.filter(project_id=project_id) if project_id else qs


class DonorIndicatorViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: donor-visible indicators on the donor's projects.
    Supports ?project=<id>."""
    permission_classes = [IsDonor]
    serializer_class = DonorIndicatorSerializer

    def get_queryset(self):
        qs = ProjectIndicator.objects.filter(
            project_id__in=_donor_project_ids(self.request), donor_visible=True,
        ).select_related("project").prefetch_related("records")
        project_id = self.request.query_params.get("project")
        return qs.filter(project_id=project_id) if project_id else qs


class DonorRiskViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: risks explicitly shared with the donor (donor_visible=True —
    fail-closed default is hidden). Supports ?project=<id>."""
    permission_classes = [IsDonor]
    serializer_class = DonorRiskSerializer

    def get_queryset(self):
        qs = ProjectRisk.objects.filter(
            project_id__in=_donor_project_ids(self.request), donor_visible=True,
        ).select_related("project")
        project_id = self.request.query_params.get("project")
        return qs.filter(project_id=project_id) if project_id else qs


class DonorMeetingViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only: donor-visible meetings tied to the donor's funds."""
    permission_classes = [IsDonor]
    serializer_class = DonorMeetingSerializer

    def get_queryset(self):
        return Meeting.objects.filter(
            fund__in=_donor_funds_queryset(self.request), donor_visible=True,
        ).select_related("fund", "project")


class DonorSummaryViewSet(viewsets.ViewSet):
    """GET /donor/summary/ — the Overview page's aggregates, computed entirely
    from the requesting donor's scoped rows."""
    permission_classes = [IsDonor]

    def list(self, request):
        from datetime import date, timedelta
        from decimal import Decimal
        from django.db.models import F, Sum

        funds = list(_donor_funds_queryset(request).select_related("project"))
        fund_ids = [f.id for f in funds]
        project_ids = [f.project_id for f in funds if f.project_id]
        today = date.today()
        soon = today + timedelta(days=30)

        committed = sum((f.amount for f in funds), Decimal("0"))
        disbursed = ProjectFundTranche.objects.filter(
            fund_id__in=fund_ids, status="received",
        ).aggregate(total=Sum("amount"))["total"] or Decimal("0")

        reports = FundReport.objects.filter(fund_id__in=fund_ids)
        deliverables = Deliverable.objects.filter(fund_id__in=fund_ids, donor_visible=True)
        pending_statuses = ["submitted", "under_review", "resubmitted"]

        projects = {f.project_id: f.project for f in funds if f.project_id}
        health = {"on_track": 0, "attention": 0, "at_risk": 0, "completed": 0}
        open_risks = set(
            ProjectRisk.objects.filter(
                project_id__in=project_ids, donor_visible=True,
                status__in=ProjectRisk.OPEN_STATUSES,
            ).values_list("project_id", flat=True)
        )
        high_risks = set(
            ProjectRisk.objects.filter(
                project_id__in=project_ids, donor_visible=True,
                status__in=ProjectRisk.OPEN_STATUSES,
            ).annotate(risk_score=F("probability") * F("impact"))
            .filter(risk_score__gte=15).values_list("project_id", flat=True)
        )
        overdue_projects = set(
            reports.filter(status__in=["upcoming", "overdue"], due_date__lt=today)
            .values_list("fund__project_id", flat=True)
        ) | set(
            deliverables.exclude(status__in=["approved", "cancelled"])
            .filter(due_date__lt=today).values_list("fund__project_id", flat=True)
        )
        for pid, project in projects.items():
            if project.status == "completed":
                health["completed"] += 1
            elif pid in high_risks or pid in overdue_projects:
                health["at_risk"] += 1
            elif pid in open_risks:
                health["attention"] += 1
            else:
                health["on_track"] += 1

        return Response({
            "funding": {
                "committed": f"{committed:.2f}",
                "disbursed": f"{disbursed:.2f}",
                "currencies": sorted({f.currency for f in funds}),
                "funds": len(funds),
            },
            "projects": {"total": len(projects), "health": health},
            "obligations": {
                "reports_due_soon": reports.filter(status="upcoming", due_date__range=(today, soon)).count(),
                "reports_overdue": reports.filter(status__in=["upcoming", "overdue"], due_date__lt=today).count(),
                "deliverables_due_soon": deliverables.exclude(
                    status__in=["approved", "cancelled"],
                ).filter(due_date__range=(today, soon)).count(),
                "deliverables_overdue": deliverables.exclude(
                    status__in=["approved", "cancelled"],
                ).filter(due_date__lt=today).count(),
                "pending_reviews": reports.filter(status__in=pending_statuses).count()
                + deliverables.filter(status__in=pending_statuses).count(),
            },
        })


class DonorFinanceSummaryViewSet(viewsets.ViewSet):
    """GET /donor/finance-summary/?fund=<id> — indicative budget-vs-actual per
    budget line: budget from allocations, actuals aggregated from approved
    project expenses (category level only, never row-level ledger data)."""
    permission_classes = [IsDonor]

    def list(self, request):
        from decimal import Decimal
        from django.db.models import Sum
        from apps.projects.models import ProjectFinance

        fund_id = request.query_params.get("fund")
        if not fund_id:
            raise ValidationError({"fund": "A fund id is required."})
        fund = _donor_funds_queryset(request).filter(pk=fund_id).select_related("project").first()
        if fund is None:
            raise NotFound("That fund is outside your access.")

        allocations = ProjectFundAllocation.objects.filter(fund=fund).select_related("budget_item")
        actuals = {}
        if fund.project_id:
            # Attribute spend by fund, not by project + budget_item. BudgetItem
            # is a shared catalog code, so filtering on it alone summed every
            # co-funder's expenses on the same line into this donor's actuals —
            # both a wrong number and a cross-donor leak. Entries with no fund
            # (unattributed, or internal budget) belong to no donor and are
            # excluded here by construction.
            rows = (
                ProjectFinance.objects.filter(
                    fund=fund, type="expense", approval_status="approved",
                    budget_item__in=[a.budget_item_id for a in allocations],
                )
                .values("budget_item")
                .annotate(total=Sum("amount"), total_idr=Sum("amount_idr"))
            )
            actuals = {r["budget_item"]: r for r in rows}

        lines = []
        total_budget = Decimal("0")
        total_actual = Decimal("0")
        for allocation in allocations:
            actual_row = actuals.get(allocation.budget_item_id, {})
            actual = actual_row.get("total") or Decimal("0")
            total_budget += allocation.amount
            total_actual += actual
            lines.append({
                "budget_item_code": allocation.budget_item.code,
                "budget_item_name": allocation.budget_item.name,
                "budget": f"{allocation.amount:.2f}",
                "actual": f"{actual:.2f}",
                "variance": f"{(allocation.amount - actual):.2f}",
                "utilization_pct": round(float(actual) / float(allocation.amount) * 100, 1) if allocation.amount else None,
            })
        return Response({
            "fund": str(fund.id),
            "currency": fund.currency,
            "note": "Indicative, unaudited figures aggregated from approved internal expenses. Official figures are in submitted financial reports.",
            "lines": lines,
            "totals": {
                "budget": f"{total_budget:.2f}",
                "actual": f"{total_actual:.2f}",
                "variance": f"{(total_budget - total_actual):.2f}",
                "utilization_pct": round(float(total_actual) / float(total_budget) * 100, 1) if total_budget else None,
            },
        })


class DonorTeamViewSet(viewsets.ViewSet):
    """Donor-side team management: list the donor company's portal users;
    admins may invite teammates (viewer or admin) to the same company."""
    permission_classes = [IsDonor]

    def list(self, request):
        company_id = request.user.donor_company_id
        if not company_id:
            return Response([])
        users = User.objects.filter(donor_company_id=company_id).order_by("created_at")
        return Response(DonorTeamUserSerializer(users, many=True).data)

    @action(detail=False, methods=["post"])
    def invite(self, request):
        _require_donor_admin(request)
        company = request.user.donor_company
        if company is None:
            raise PermissionDenied("Your account has no donor company.")
        serializer = DonorTeamInviteSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        email = serializer.validated_data["email"].strip().lower()
        donor_role = serializer.validated_data["donor_role"]
        organization = get_active_organization(request)

        if User.objects.filter(email__iexact=email).exists():
            return Response({"email": "This email already has an account."}, status=400)

        from apps.core.models import _invitation_token
        from django.utils import timezone
        invite, _ = Invitation.objects.update_or_create(
            organization=organization, email=email, status="pending",
            defaults={
                "donor_company": company, "donor_role": donor_role, "role": None,
                "invited_by": request.user,
                "token": _invitation_token(),
                "expires_at": timezone.now() + timezone.timedelta(days=7),
            },
        )
        link = DonorInviteSerializer(invite, context={"request": request}).data["link"]
        try:
            _send(
                invite.email, f"You're invited to the {company.name} donor portal",
                f"You've been invited to view your organization's funds and reports on "
                f"{settings.PRODUCT_NAME}. Use the link below to accept.",
                link,
            )
        except Exception:
            pass
        return Response(DonorInviteSerializer(invite, context={"request": request}).data, status=201)


class DonorCommentViewSet(viewsets.ModelViewSet):
    """List/create comments on the requesting donor's own funds. No update/delete
    — a submitted request/question is immutable from the donor side."""
    permission_classes = [IsDonor]
    serializer_class = DonorCommentSerializer
    http_method_names = ["get", "post", "head", "options"]

    def _fund(self):
        fund_id = self.request.query_params.get("fund") or self.request.data.get("fund")
        if not fund_id:
            raise PermissionDenied("A fund id is required.")
        fund = _donor_funds_queryset(self.request).filter(pk=fund_id).first()
        if fund is None:
            raise PermissionDenied("That fund is outside your access.")
        return fund

    def get_queryset(self):
        from django.contrib.contenttypes.models import ContentType
        fund = self._fund()
        content_type = ContentType.objects.get_for_model(ProjectFund)
        return Comment.objects.filter(content_type=content_type, object_id=str(fund.id)).select_related("author")

    def perform_create(self, serializer):
        from django.contrib.contenttypes.models import ContentType
        fund = self._fund()
        content_type = ContentType.objects.get_for_model(ProjectFund)
        comment = serializer.save(author=self.request.user, content_type=content_type, object_id=str(fund.id))
        if fund.project_id:
            for member in fund.project.team_members.filter(role__in=["owner", "manager"]).select_related("user"):
                if member.user_id and member.user_id != self.request.user.id:
                    notify(
                        member.user,
                        title="Donor comment",
                        message=f"{self.request.user.get_full_name() or self.request.user.email} commented on the fund from {fund.source}.",
                        link=f"/project/funds/{fund.id}",
                        notification_type="project", icon="folder",
                    )
        return comment


class DonorInviteViewSet(viewsets.ModelViewSet):
    """Staff-side: invite a donor account for a specific fund's donor Company.
    List/create/revoke only, scoped to the caller's active org — mirrors
    core.rbac_views.InvitationViewSet's shape for the plain-staff invite flow."""
    rbac_domain = "donors"
    permission_classes = [HasMenuPermission]
    serializer_class = DonorInviteSerializer
    http_method_names = ["get", "post", "delete"]

    def get_queryset(self):
        return Invitation.objects.filter(
            organization=get_active_organization(self.request), donor_company__isnull=False,
        ).select_related("donor_company")

    def create(self, request, *args, **kwargs):
        organization = get_active_organization(request)
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        email = serializer.validated_data["email"].strip().lower()
        donor_company = serializer.validated_data["donor_company"]
        donor_role = serializer.validated_data.get("donor_role") or "admin"

        if not ProjectFund.objects.filter(organization=organization, donor=donor_company).exists():
            raise ValidationError({"donor_company": "This company has no funds in your organization."})

        from apps.companies.models import OrganizationMembership
        if OrganizationMembership.objects.filter(organization=organization, user__email__iexact=email).exists():
            return Response({"email": "This user is already a member of your organization."}, status=400)

        from apps.core.models import _invitation_token
        from django.utils import timezone
        invite, _ = Invitation.objects.update_or_create(
            organization=organization, email=email, status="pending",
            defaults={
                "donor_company": donor_company, "donor_role": donor_role, "role": None,
                "invited_by": request.user,
                "token": _invitation_token(), "expires_at": timezone.now() + timezone.timedelta(days=7),
            },
        )

        link = DonorInviteSerializer(invite, context={"request": request}).data["link"]
        try:
            _send(
                invite.email, f"You're invited to the {donor_company.name} donor portal",
                f"You've been invited to view your organization's funds and reports on "
                f"{settings.PRODUCT_NAME}. Use the link below to accept.",
                link,
            )
        except Exception:
            pass
        return Response(DonorInviteSerializer(invite, context={"request": request}).data, status=201)

    def perform_destroy(self, instance):
        instance.status = "revoked"
        instance.save(update_fields=["status"])
