from __future__ import annotations

import calendar
from datetime import date
from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.rims.projects.models import Milestone, Project


def _months_for_frequency(freq: str) -> int:
    mapping = {
        "monthly": 1,
        "quarterly": 3,
        "semi_annual": 6,
        "annual": 12,
    }
    return mapping.get(freq, 3)


def _add_months(d: date, months: int) -> date:
    month = d.month - 1 + months
    year = d.year + month // 12
    month = month % 12 + 1
    day = min(d.day, calendar.monthrange(year, month)[1])
    return date(year, month, day)


def generate_reporting_milestones(project: Project) -> int:
    """
    Seed project milestones from linked funding reporting frequency.
    Idempotent: only generates when no milestones exist.
    """
    if project.milestones.exists():
        return 0
    funding = getattr(project.award.application.call, "funding_record", None)
    step_months = _months_for_frequency(getattr(funding, "report_narrative_frequency", "") if funding else "")
    cursor = project.start_date
    idx = 0
    created = 0
    while cursor < project.end_date:
        cursor = _add_months(cursor, step_months)
        if cursor > project.end_date:
            cursor = project.end_date
        idx += 1
        Milestone.objects.create(
            project=project,
            name=f"{idx * step_months}-month progress report",
            due_date=cursor,
            completion_percent=0,
            status=Milestone.Status.PLANNED,
            report_due=True,
        )
        created += 1
        if cursor == project.end_date:
            break
    return created


def refresh_overdue_milestone_states(project: Project) -> None:
    today = timezone.now().date()
    project.milestones.filter(
        due_date__lt=today,
        report_due=True,
        status__in=[Milestone.Status.PLANNED, Milestone.Status.REVISION_REQUIRED],
    ).update(status=Milestone.Status.OVERDUE)


def project_milestone_compliance(project: Project) -> dict:
    milestones = list(project.milestones.all())
    total = len(milestones)
    accepted = sum(1 for m in milestones if m.status == Milestone.Status.ACCEPTED)
    pending = sum(1 for m in milestones if m.status in (Milestone.Status.SUBMITTED, Milestone.Status.UNDER_REVIEW))
    overdue = sum(1 for m in milestones if m.status == Milestone.Status.OVERDUE)
    required = sum(1 for m in milestones if m.report_due)
    return {
        "total": total,
        "required": required,
        "accepted": accepted,
        "pending_review": pending,
        "overdue": overdue,
        "completion_rate": (round((accepted / total) * 100, 1) if total else 0.0),
    }


def project_budget_progress(project: Project) -> dict:
    """Aggregate project-level budget allocation vs. tracked spend.

    Reads `Project.budget_lines` (allocated/spent) — the project-side picture,
    distinct from finance.Budget which captures the formal FM ceiling.
    """
    from decimal import Decimal

    lines = list(project.budget_lines.all())
    allocated = sum((line.allocated for line in lines), Decimal("0"))
    spent = sum((line.spent for line in lines), Decimal("0"))
    remaining = allocated - spent
    spent_pct = float(spent / allocated * 100) if allocated else 0.0
    bar_width = min(100.0, spent_pct)
    compliance = project_milestone_compliance(project)
    return {
        "allocated": allocated,
        "spent": spent,
        "remaining": remaining,
        "spent_pct": spent_pct,
        "bar_width": bar_width,
        "overspend": spent > allocated and allocated > 0,
        "milestone_required": compliance["required"],
        "milestone_accepted": compliance["accepted"],
        "milestone_overdue": compliance["overdue"],
    }


def assert_disbursement_milestone_gate(project: Project, milestone: Milestone | None) -> None:
    """
    If a milestone is supplied on disbursement request, it must be accepted.
    """
    if milestone is None:
        return
    if milestone.project_id != project.id:
        raise ValidationError("Selected milestone does not belong to this project.")
    if milestone.status != Milestone.Status.ACCEPTED:
        raise ValidationError("Disbursement milestone gate not met: milestone must be accepted first.")


def assert_award_accepts_milestone_submission(project: Project) -> None:
    """PRD §5.1 FRFA-CO002 — block milestone evidence uploads once the award has entered
    close-out or been closed/cancelled."""
    # Lazy import to avoid circular import at module load.
    from apps.rims.grants.models import Award

    award = getattr(project, "award", None)
    if award is None:
        return
    if award.status in {
        Award.Status.CLOSEOUT_IN_PROGRESS,
        Award.Status.CLOSED,
        Award.Status.CANCELLED,
    }:
        raise ValidationError(
            "This award is in close-out or closed; new milestone submissions are blocked."
        )

