"""Weekly mission generation — the "one thing to do this week".

Candidate missions are checked in priority order against the user's live data;
the first one that applies becomes the week's mission. Everything is derived
from real rows, so a mission is only offered when there's something concrete
behind it. The last candidate is an always-available fallback.
"""

from datetime import date, timedelta
from decimal import Decimal

from django.db.models import Sum

from apps.expenses.models import Expense
from apps.goals.models import Goal
from apps.goals.serializers import months_between
from apps.invoicing.models import Invoice


def current_week_start(today: date | None = None) -> date:
    today = today or date.today()
    return today - timedelta(days=today.weekday())  # Monday


def _money(value) -> str:
    return f'KSh {Decimal(value):,.0f}'


def _round_to(value: Decimal, step: int = 100) -> int:
    """Round a suggested amount to something a person would actually transfer."""
    stepped = int((Decimal(value) / step).to_integral_value(rounding='ROUND_CEILING') * step)
    return max(step, stepped)


def _mission(key, title, detail):
    return {'key': key, 'title': title, 'detail': detail}


def generate_mission_for(user) -> dict:
    today = date.today()
    company = user.company

    live_invoices = (
        Invoice.objects.filter(company=company)
        .exclude(status__in=[Invoice.Status.CANCELLED, Invoice.Status.DRAFT])
        .select_related('customer')
        if company
        else Invoice.objects.none()
    )

    # 1. Overdue money is the fastest cash there is.
    overdue = [
        inv for inv in live_invoices
        if inv.balance_due > 0 and inv.status != Invoice.Status.PAID and inv.due_date < today
    ]
    if overdue:
        worst = max(overdue, key=lambda inv: (today - inv.due_date).days)
        days_late = (today - worst.due_date).days
        name = worst.customer.name if worst.customer else 'your customer'
        return _mission(
            'chase_overdue',
            f'Chase {name} on invoice {worst.invoice_number}',
            f'{_money(worst.balance_due)} has been overdue for {days_late} day'
            f'{"" if days_late == 1 else "s"}. One friendly message today is the fastest cash you can raise.',
        )

    # 2. A draft invoice cannot get paid.
    if company:
        draft = (
            Invoice.objects.filter(company=company, status=Invoice.Status.DRAFT)
            .select_related('customer')
            .order_by('issue_date')
            .first()
        )
        if draft:
            name = draft.customer.name if draft.customer else 'your customer'
            return _mission(
                'send_draft',
                f'Send the draft invoice for {name}',
                f'{_money(draft.total)} is sitting unsent. It cannot get paid until it goes out — '
                'send it before the week ends.',
            )

    goals = list(Goal.objects.filter(user=user))
    active = [g for g in goals if g.current_amount < g.target_amount]

    # 3. A goal that is falling behind its own date.
    behind = []
    for goal in active:
        months = months_between(today, goal.target_date)
        remaining = goal.target_amount - goal.current_amount
        required = remaining / months if months > 0 else remaining
        if goal.monthly_contribution > 0 and required > goal.monthly_contribution:
            behind.append((goal, required - goal.monthly_contribution))
    if behind:
        goal, shortfall = max(behind, key=lambda pair: pair[1])
        top_up = _round_to(shortfall)
        return _mission(
            'fund_goal',
            f'Add {_money(top_up)} to {goal.name}',
            f'It is drifting behind its date. A one-off top-up of {_money(top_up)} this week pulls it '
            'back toward schedule.',
        )

    # 4. A goal with no monthly number is a wish, not a plan.
    no_plan = next((g for g in active if g.monthly_contribution <= 0), None)
    if no_plan:
        return _mission(
            'set_goal_plan',
            f'Set a monthly amount for {no_plan.name}',
            'Without a monthly number it is a wish, not a plan. Pick an amount you can actually keep to '
            'every month, even a small one.',
        )

    # 5. No safety net yet.
    if not any(g.category == Goal.Category.EMERGENCY for g in goals):
        return _mission(
            'start_emergency_fund',
            'Start an emergency fund',
            'Even KSh 500 a week builds the buffer that stops a bad month from becoming a crisis. '
            'Create the goal and make the first deposit.',
        )

    # 6. Trim the biggest recurring cost.
    if company:
        top_expense = (
            Expense.objects.filter(company=company, expense_date__gte=today - timedelta(days=90))
            .values('category__name')
            .annotate(total=Sum('amount'))
            .order_by('-total')
            .first()
        )
        if top_expense and (top_expense['total'] or 0) > 0:
            name = top_expense['category__name'] or 'your largest category'
            return _mission(
                'trim_expense',
                f'Find one cut in {name}',
                f'{_money(top_expense["total"])} over the last 90 days makes it your biggest cost. '
                'Cancel, downgrade, or renegotiate exactly one thing this week.',
            )

    # 7. Always-available fallback.
    return _mission(
        'save_small',
        'Put aside KSh 500 this week',
        'Small, boring, and repeated is how a buffer actually gets built. Move it somewhere you will '
        'not casually spend it.',
    )


def compute_streak(user, today: date | None = None) -> int:
    """Consecutive weeks with a completed mission, counting back from this week
    (or last week, if this week's mission is still open)."""
    from .models import WeeklyMission

    completed = set(
        WeeklyMission.objects.filter(user=user, completed_at__isnull=False).values_list('week_start', flat=True)
    )
    week = current_week_start(today)
    if week not in completed:
        week -= timedelta(days=7)

    streak = 0
    while week in completed:
        streak += 1
        week -= timedelta(days=7)
    return streak
