"""
Dashboard summary service.

Boundary 1 (Flask/Service) & Boundary 5 (Analytics+Insights/Flask) enforced:
no Flask context, no direct DB session — all inputs are plain parameters.
All aggregations use SQL func.sum — no Python loops over transaction rows.
"""
from __future__ import annotations

import datetime
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any

from sqlalchemy import func

from app.extensions import db
from app.models.budget import Budget
from app.models.category import Category
from app.models.transaction import Transaction
from app.models.bill import Bill
from app.models.debt import Debt
from app.models.paydown_plan import PaydownPlan
from app.models.paydown_plan_card import PaydownPlanCard
from app.models.account import Account
from app.models.paydown_balance_update import PaydownBalanceUpdate
from app.services.amortization import (
    calculate_avalanche, calculate_snowball,
    calculate_highest_balance, calculate_proportional, calculate_custom,
)
from app.services.plan_monitor import get_monitor_statuses


_STRATEGY_CALC = {
    "avalanche":       calculate_avalanche,
    "snowball":        calculate_snowball,
    "highest_balance": calculate_highest_balance,
    "proportional":    calculate_proportional,
}


@dataclass
class BudgetBurnRow:
    category_name: str
    budgeted: Decimal
    spent: Decimal
    remaining: Decimal
    pct: int
    state: str            # safe / warning / danger
    has_budget: bool


@dataclass
class UpcomingBillRow:
    name: str
    amount: Decimal
    due_date: str         # ISO 8601
    days_until_due: int
    status: str           # Upcoming / Due Today / Overdue
    item_type: str        # bill / debt


@dataclass
class RecentTransaction:
    date: str
    merchant: str
    amount: Decimal
    category_name: str


@dataclass
class DashboardData:
    # Widget 1: Budget Burn
    budget_burn_rows: list[BudgetBurnRow]

    # Widget 2: Monthly Spending Summary
    current_month_total: Decimal
    prior_month_total: Decimal
    month_delta: Decimal

    # Widget 3: Upcoming Bills
    upcoming_bills: list[UpcomingBillRow]

    # Widget 4: Recent Transactions
    recent_transactions: list[RecentTransaction]

    # Widget 5: Debt Paydown Progress
    paydown_statuses: list[Any]   # list[CardMonitorStatus]
    active_plan: Any | None       # PaydownPlan or None

    # Meta
    has_budgets: bool
    has_bills: bool
    has_transactions: bool
    has_active_plan: bool


def get_dashboard_data(
    year: int,
    month: int,
    active_plan_id: int | None,
    monthly_income: Decimal | None,
) -> DashboardData:
    """
    Collect all dashboard data in a single pass.

    Parameters extracted at the route layer:
        year, month — current month for budget/spending
        active_plan_id — Settings-agnostic plan reference
        monthly_income — from Settings, passed as plain value
    """
    today = datetime.date.today()
    month_prefix = f"{year}-{month:02d}-%"
    prior_month = datetime.date(year, month, 1) - datetime.timedelta(days=1)
    prior_prefix = f"{prior_month.year}-{prior_month.month:02d}-%"

    # ── Widget 1: Budget Burn ────────────────────────────────────────────────
    active_categories = (
        Category.query.filter_by(is_active=True).order_by(Category.name).all()
    )
    budgets = {
        b.category_id: b
        for b in Budget.query.filter_by(month=month, year=year).all()
    }
    spent_rows = (
        db.session.query(Transaction.category_id, func.sum(Transaction.amount).label("total"))
        .filter(Transaction.date.like(month_prefix), Transaction.is_credit == False)  # noqa: E712
        .group_by(Transaction.category_id)
        .all()
    )
    actuals = {r.category_id: r.total for r in spent_rows}

    budget_burn_rows = []
    for cat in active_categories:
        budget = budgets.get(cat.id)
        budgeted = budget.amount if budget else Decimal("0")
        spent = actuals.get(cat.id, Decimal("0"))
        remaining = budgeted - spent
        pct = int(spent / budgeted * 100) if budgeted > 0 else 0
        state = "danger" if pct >= 100 else ("warning" if pct >= 80 else "safe")
        budget_burn_rows.append(BudgetBurnRow(
            category_name=cat.name,
            budgeted=budgeted,
            spent=spent,
            remaining=remaining,
            pct=pct,
            state=state,
            has_budget=budget is not None,
        ))

    has_budgets = any(r.has_budget for r in budget_burn_rows)

    # ── Widget 2: Monthly Spending Summary ───────────────────────────────────
    current_sum_row = (
        db.session.query(func.sum(Transaction.amount))
        .filter(Transaction.date.like(month_prefix), Transaction.is_credit == False)  # noqa: E712
        .scalar()
    )
    current_month_total = Decimal(str(current_sum_row or 0))

    prior_sum_row = (
        db.session.query(func.sum(Transaction.amount))
        .filter(Transaction.date.like(prior_prefix), Transaction.is_credit == False)  # noqa: E712
        .scalar()
    )
    prior_month_total = Decimal(str(prior_sum_row or 0))
    month_delta = current_month_total - prior_month_total

    has_transactions = Transaction.query.count() > 0

    # ── Widget 3: Upcoming Bills ──────────────────────────────────────────────
    cutoff = (today + datetime.timedelta(days=30)).isoformat()
    active_bills = Bill.query.filter_by(is_active=True).all()
    active_debts = Debt.query.filter_by(is_active=True).all()

    upcoming_bills: list[UpcomingBillRow] = []
    for bill in active_bills:
        if not bill.due_date:
            continue
        if bill.due_date > cutoff:
            continue
        delta = (datetime.date.fromisoformat(bill.due_date) - today).days
        if delta < 0:
            status = "Overdue"
        elif delta == 0:
            status = "Due Today"
        else:
            status = "Upcoming"
        upcoming_bills.append(UpcomingBillRow(
            name=bill.name,
            amount=bill.amount,
            due_date=bill.due_date,
            days_until_due=delta,
            status=status,
            item_type="bill",
        ))

    for debt in active_debts:
        if not debt.due_date:
            continue
        if debt.due_date > cutoff:
            continue
        delta = (datetime.date.fromisoformat(debt.due_date) - today).days
        if delta < 0:
            status = "Overdue"
        elif delta == 0:
            status = "Due Today"
        else:
            status = "Upcoming"
        upcoming_bills.append(UpcomingBillRow(
            name=debt.name,
            amount=debt.min_payment,
            due_date=debt.due_date,
            days_until_due=delta,
            status=status,
            item_type="debt",
        ))

    upcoming_bills.sort(key=lambda x: x.due_date)
    has_bills = bool(upcoming_bills)

    # ── Widget 4: Recent Transactions ────────────────────────────────────────
    cat_names = {c.id: c.name for c in active_categories}
    recent_txns_raw = (
        Transaction.query
        .order_by(Transaction.date.desc(), Transaction.id.desc())
        .limit(10)
        .all()
    )
    recent_transactions = [
        RecentTransaction(
            date=t.date,
            merchant=t.merchant_normalized,
            amount=t.amount,
            category_name=cat_names.get(t.category_id, "—"),
        )
        for t in recent_txns_raw
    ]

    # ── Widget 5: Debt Paydown Progress ───────────────────────────────────────
    active_plan = None
    paydown_statuses = []
    has_active_plan = False

    if active_plan_id:
        active_plan = PaydownPlan.query.filter_by(id=active_plan_id, status="active").first()

    if active_plan:
        has_active_plan = True
        plan_card_ids = [pc.account_id for pc in active_plan.cards]
        cards = Account.query.filter(Account.id.in_(plan_card_ids)).all()
        card_dicts = [
            {
                "id": c.id,
                "name": c.name,
                "balance": c.current_balance or Decimal("0"),
                "apr": c.apr or Decimal("0"),
                "min_payment": c.min_payment or Decimal("0"),
            }
            for c in cards
        ]
        fn = _STRATEGY_CALC.get(active_plan.strategy)
        if fn:
            result = fn(card_dicts, active_plan.extra_monthly)
        else:
            result = calculate_custom(card_dicts, {})

        updates_raw = (
            PaydownBalanceUpdate.query
            .filter(PaydownBalanceUpdate.account_id.in_(plan_card_ids))
            .all()
        )
        update_dicts = [
            {"account_id": u.account_id, "balance": u.balance, "updated_at": u.updated_at}
            for u in updates_raw
        ]
        paydown_statuses = get_monitor_statuses(result, update_dicts)

    return DashboardData(
        budget_burn_rows=budget_burn_rows,
        current_month_total=current_month_total,
        prior_month_total=prior_month_total,
        month_delta=month_delta,
        upcoming_bills=upcoming_bills,
        recent_transactions=recent_transactions,
        paydown_statuses=paydown_statuses,
        active_plan=active_plan,
        has_budgets=has_budgets,
        has_bills=has_bills,
        has_transactions=has_transactions,
        has_active_plan=has_active_plan,
    )
