"""Funding record balance math and publish-time validation (PRD §5.1 call setup)."""

from __future__ import annotations

import uuid
from decimal import Decimal

from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.utils import timezone

from apps.rims.grants.constants import CALL_TYPE_REQUIRED_ADMIN_DOCS
from apps.rims.grants.models import FundingRecord, GrantCall

_STATUSES_COMMITTING_BALANCE = (
    GrantCall.Status.PUBLISHED,
    GrantCall.Status.CLOSED,
    GrantCall.Status.AWARDING,
    GrantCall.Status.COMPLETED,
)


def committed_amount_excluding_call(funding: FundingRecord, *, exclude_call_id: int | None = None) -> Decimal:
    qs = funding.calls.filter(status__in=_STATUSES_COMMITTING_BALANCE).exclude(budget_ceiling__isnull=True)
    if exclude_call_id is not None:
        qs = qs.exclude(pk=exclude_call_id)
    agg = qs.aggregate(s=Coalesce(Sum("budget_ceiling"), Decimal("0")))
    return agg["s"] or Decimal("0")


def uncommitted_balance(funding: FundingRecord, *, exclude_call_id: int | None = None) -> Decimal:
    if funding.status != FundingRecord.Status.APPROVED:
        return Decimal("0")
    return (funding.amount - committed_amount_excluding_call(funding, exclude_call_id=exclude_call_id)).quantize(
        Decimal("0.01")
    )


def validate_call_publishable(call: GrantCall) -> None:
    """Raise ValidationError if the call cannot be published (eligibility, documents, dates, funding)."""
    errors: list[str] = []

    if not call.eligibility_rules.exists():
        errors.append("Add at least one eligibility rule before publishing this call.")

    # Documents are recommended but not required — staff can publish and add
    # documents later. The guideline and doc-kind checks are removed as blockers.

    if call.opens_at >= call.closes_at:
        errors.append("The submission deadline must be after the application opening date.")

    if call.review_starts_at is not None and call.review_starts_at <= call.closes_at:
        errors.append("The review period must start after the submission deadline.")

    if call.review_starts_at and call.review_ends_at and call.review_ends_at < call.review_starts_at:
        errors.append("Review period end must be on or after the review period start.")

    fr = call.funding_record
    if fr is not None:
        if fr.status != FundingRecord.Status.APPROVED:
            errors.append("The linked funding record must be approved before this call can be published.")
        ceiling = call.budget_ceiling
        # Scholarship and fellowship calls do not have per-applicant budgets —
        # only research grant calls require a budget ceiling when linked to funding.
        is_grant_call = getattr(call, "call_type", "") == "grant"
        if is_grant_call:
            if ceiling is None:
                errors.append("Set a call allocation (budget ceiling) when the call is linked to a funding record.")
            else:
                room = uncommitted_balance(fr, exclude_call_id=call.pk)
                if ceiling > room:
                    errors.append(
                        f"The call allocation ({ceiling}) is larger than the remaining balance on the funding record ({room}). "
                        "Reduce the allocation or adjust other calls."
                    )

    if errors:
        raise ValidationError(" ".join(errors))


@transaction.atomic
def submit_funding_for_approval(funding: FundingRecord, *, actor=None) -> None:
    """Move funding record to pending approval; assign provisional grant identifier.

    PRD §5.1 FRFA-GPS026 — every (re)submission writes a FundingRecordVersion
    snapshot so reviewers can audit the diff between attempts.
    """
    if not funding.grant_manager_id:
        raise ValidationError("Assign a grant manager before submitting this funding record for approval.")
    if not funding.reporting_financial_frequency or not funding.reporting_narrative_frequency:
        raise ValidationError("Set both financial and narrative reporting frequencies before submitting for approval.")
    funding.full_clean()
    if funding.status not in (FundingRecord.Status.DRAFT, FundingRecord.Status.DRAFT_REVISION_REQUIRED):
        raise ValidationError("Only draft funding records can be submitted for approval.")
    funding.status = FundingRecord.Status.PENDING_APPROVAL
    if not funding.public_grant_id:
        funding.public_grant_id = f"PND-{uuid.uuid4().hex[:10].upper()}"
    funding.save(update_fields=["status", "public_grant_id", "updated_at"])
    try:
        from apps.rims.grants.services import snapshot_funding_record_version

        snapshot_funding_record_version(funding, actor=actor)
    except Exception:  # pragma: no cover - snapshot failures must not block submit
        import logging

        logging.getLogger(__name__).exception(
            "snapshot_funding_record_version failed funding_id=%s", funding.pk
        )


@transaction.atomic
def approve_funding_record(funding: FundingRecord) -> None:
    """Approver marks funding approved and issues final Grant ID."""
    if funding.status != FundingRecord.Status.PENDING_APPROVAL:
        raise ValidationError("Only pending funding records can be approved.")
    funding.status = FundingRecord.Status.APPROVED
    funding.public_grant_id = f"GR-{timezone.now().year}-{funding.pk:05d}"
    funding.rejection_comment = ""
    funding.save(update_fields=["status", "public_grant_id", "rejection_comment", "updated_at"])


@transaction.atomic
def reject_funding_record(funding: FundingRecord, comment: str) -> None:
    if funding.status != FundingRecord.Status.PENDING_APPROVAL:
        raise ValidationError("Only pending funding records can be rejected.")
    if not (comment or "").strip():
        raise ValidationError("Add a short comment explaining why this version is being returned for revision.")
    funding.status = FundingRecord.Status.DRAFT_REVISION_REQUIRED
    funding.rejection_comment = comment.strip()
    funding.save(update_fields=["status", "rejection_comment", "updated_at"])
