"""
Amortization engine — all 5 paydown strategies.

Boundary 6 enforced: zero imports outside stdlib + decimal + datetime.
All monetary arithmetic uses Decimal. No ORM references anywhere.
"""
from __future__ import annotations

import calendar
import datetime
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from typing import Any

_CENT = Decimal("0.01")
_ZERO = Decimal("0")


@dataclass
class AmortizationResult:
    strategy: str
    months_to_payoff: int
    total_interest_paid: Decimal
    payoff_date: datetime.date
    per_card_schedule: list[dict[str, Any]]


# ── internal helpers ──────────────────────────────────────────────────────────

def _cents(d: Decimal) -> Decimal:
    return d.quantize(_CENT, rounding=ROUND_HALF_UP)


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


def _run_schedule(
    cards: list[dict],
    extra: Decimal,
    strategy_name: str,
    priority_fn,
) -> AmortizationResult:
    """
    Core amortization loop for ordered-priority strategies.

    cards: list of dicts with keys: id, name, balance, apr, min_payment (all convertible to Decimal)
    extra: total extra monthly payment beyond minimums
    priority_fn(active_cards, balances, aprs) -> list of card IDs in priority order
    """
    balances = {c["id"]: _cents(Decimal(str(c["balance"]))) for c in cards}
    aprs = {c["id"]: Decimal(str(c["apr"])) for c in cards}
    mins = {c["id"]: _cents(Decimal(str(c["min_payment"]))) for c in cards}
    names = {c["id"]: c["name"] for c in cards}

    schedules: dict[int, list[dict]] = {c["id"]: [] for c in cards}
    total_interest: dict[int, Decimal] = {c["id"]: _ZERO for c in cards}

    today = datetime.date.today()
    month_num = 0
    max_months = 600  # safety cap: 50 years

    while any(b > _ZERO for b in balances.values()) and month_num < max_months:
        month_num += 1
        month_date = _add_months(today, month_num)

        # Step 1: accrue interest on all active cards
        interest_this_month: dict[int, Decimal] = {}
        for cid in balances:
            bal = balances[cid]
            if bal <= _ZERO:
                interest_this_month[cid] = _ZERO
                continue
            monthly_rate = aprs[cid] / Decimal("100") / Decimal("12")
            interest = _cents(bal * monthly_rate)
            balances[cid] = _cents(bal + interest)
            interest_this_month[cid] = interest
            total_interest[cid] += interest

        # Step 2: pay minimums on all active cards
        payments: dict[int, Decimal] = {cid: _ZERO for cid in balances}
        for cid in balances:
            bal = balances[cid]
            if bal <= _ZERO:
                continue
            pay = min(mins[cid], bal)
            balances[cid] = _cents(bal - pay)
            payments[cid] = pay

        # Step 3: allocate extra payment in priority order
        remaining_extra = _cents(extra)
        active_cards = [c for c in cards if balances[c["id"]] > _ZERO]
        ordered_ids = priority_fn(active_cards, balances, aprs)

        for cid in ordered_ids:
            if remaining_extra <= _ZERO:
                break
            bal = balances[cid]
            if bal <= _ZERO:
                continue
            extra_pay = min(remaining_extra, bal)
            balances[cid] = _cents(bal - extra_pay)
            payments[cid] += extra_pay
            remaining_extra -= extra_pay

        # Step 4: record row for each card
        for cid in balances:
            interest = interest_this_month[cid]
            payment = payments[cid]
            principal = _cents(payment - interest)
            schedules[cid].append({
                "month": month_num,
                "date": month_date.isoformat(),
                "payment": str(_cents(payment)),
                "principal": str(_cents(principal)),
                "interest": str(_cents(interest)),
                "balance": str(_cents(max(balances[cid], _ZERO))),
            })

    payoff_date = _add_months(today, month_num)
    grand_interest = _cents(sum(total_interest.values(), _ZERO))

    per_card = []
    for c in cards:
        cid = c["id"]
        payoff_month = next(
            (row["month"] for row in schedules[cid] if Decimal(row["balance"]) <= _ZERO),
            month_num,
        )
        per_card.append({
            "account_id": cid,
            "account_name": names[cid],
            "payoff_month": payoff_month,
            "payoff_date": _add_months(today, payoff_month).isoformat(),
            "total_interest": str(_cents(total_interest[cid])),
            "schedule": schedules[cid],
        })

    return AmortizationResult(
        strategy=strategy_name,
        months_to_payoff=month_num,
        total_interest_paid=grand_interest,
        payoff_date=payoff_date,
        per_card_schedule=per_card,
    )


# ── strategy public API ───────────────────────────────────────────────────────

def calculate_avalanche(cards: list[dict], extra_monthly: Decimal) -> AmortizationResult:
    """Highest APR first."""
    def priority(active, balances, aprs):
        return [c["id"] for c in sorted(active, key=lambda c: aprs[c["id"]], reverse=True)]
    return _run_schedule(cards, extra_monthly, "avalanche", priority)


def calculate_snowball(cards: list[dict], extra_monthly: Decimal) -> AmortizationResult:
    """Lowest balance first."""
    def priority(active, balances, aprs):
        return [c["id"] for c in sorted(active, key=lambda c: balances[c["id"]])]
    return _run_schedule(cards, extra_monthly, "snowball", priority)


def calculate_highest_balance(cards: list[dict], extra_monthly: Decimal) -> AmortizationResult:
    """Highest balance first."""
    def priority(active, balances, aprs):
        return [c["id"] for c in sorted(active, key=lambda c: balances[c["id"]], reverse=True)]
    return _run_schedule(cards, extra_monthly, "highest_balance", priority)


def calculate_proportional(cards: list[dict], extra_monthly: Decimal) -> AmortizationResult:
    """Distribute extra proportionally by balance each month."""
    balances = {c["id"]: _cents(Decimal(str(c["balance"]))) for c in cards}
    aprs = {c["id"]: Decimal(str(c["apr"])) for c in cards}
    mins = {c["id"]: _cents(Decimal(str(c["min_payment"]))) for c in cards}
    names = {c["id"]: c["name"] for c in cards}

    schedules: dict[int, list[dict]] = {c["id"]: [] for c in cards}
    total_interest: dict[int, Decimal] = {c["id"]: _ZERO for c in cards}

    today = datetime.date.today()
    month_num = 0
    max_months = 600

    while any(b > _ZERO for b in balances.values()) and month_num < max_months:
        month_num += 1
        month_date = _add_months(today, month_num)

        interest_this_month: dict[int, Decimal] = {}
        for cid in balances:
            bal = balances[cid]
            if bal <= _ZERO:
                interest_this_month[cid] = _ZERO
                continue
            monthly_rate = aprs[cid] / Decimal("100") / Decimal("12")
            interest = _cents(bal * monthly_rate)
            balances[cid] = _cents(bal + interest)
            interest_this_month[cid] = interest
            total_interest[cid] += interest

        payments: dict[int, Decimal] = {cid: _ZERO for cid in balances}
        for cid in balances:
            bal = balances[cid]
            if bal <= _ZERO:
                continue
            pay = min(mins[cid], bal)
            balances[cid] = _cents(bal - pay)
            payments[cid] = pay

        # Proportional extra by current balance
        active_ids = [c["id"] for c in cards if balances[c["id"]] > _ZERO]
        total_bal = sum(balances[cid] for cid in active_ids)

        if total_bal > _ZERO and extra_monthly > _ZERO:
            remaining_extra = _cents(extra_monthly)
            for i, cid in enumerate(active_ids):
                if remaining_extra <= _ZERO:
                    break
                if i == len(active_ids) - 1:
                    extra_pay = min(remaining_extra, balances[cid])
                else:
                    share = _cents(extra_monthly * balances[cid] / total_bal)
                    extra_pay = min(share, balances[cid], remaining_extra)
                if extra_pay > _ZERO:
                    balances[cid] = _cents(balances[cid] - extra_pay)
                    payments[cid] += extra_pay
                    remaining_extra -= extra_pay

        for cid in balances:
            interest = interest_this_month[cid]
            payment = payments[cid]
            principal = _cents(payment - interest)
            schedules[cid].append({
                "month": month_num,
                "date": month_date.isoformat(),
                "payment": str(_cents(payment)),
                "principal": str(_cents(principal)),
                "interest": str(_cents(interest)),
                "balance": str(_cents(max(balances[cid], _ZERO))),
            })

    payoff_date = _add_months(today, month_num)
    grand_interest = _cents(sum(total_interest.values(), _ZERO))

    per_card = []
    for c in cards:
        cid = c["id"]
        payoff_month = next(
            (row["month"] for row in schedules[cid] if Decimal(row["balance"]) <= _ZERO),
            month_num,
        )
        per_card.append({
            "account_id": cid,
            "account_name": names[cid],
            "payoff_month": payoff_month,
            "payoff_date": _add_months(today, payoff_month).isoformat(),
            "total_interest": str(_cents(total_interest[cid])),
            "schedule": schedules[cid],
        })

    return AmortizationResult(
        strategy="proportional",
        months_to_payoff=month_num,
        total_interest_paid=grand_interest,
        payoff_date=payoff_date,
        per_card_schedule=per_card,
    )


def calculate_custom(
    cards: list[dict],
    custom_allocations: dict[int, Decimal],
) -> AmortizationResult:
    """
    User-specified per-card monthly payments.
    custom_allocations: {card_id: monthly_payment_amount}
    Allocations below minimum are raised to minimum.
    """
    balances = {c["id"]: _cents(Decimal(str(c["balance"]))) for c in cards}
    aprs = {c["id"]: Decimal(str(c["apr"])) for c in cards}
    mins = {c["id"]: _cents(Decimal(str(c["min_payment"]))) for c in cards}
    names = {c["id"]: c["name"] for c in cards}

    allocations = {
        c["id"]: max(
            _cents(Decimal(str(custom_allocations.get(c["id"], _ZERO)))),
            mins[c["id"]],
        )
        for c in cards
    }

    schedules: dict[int, list[dict]] = {c["id"]: [] for c in cards}
    total_interest: dict[int, Decimal] = {c["id"]: _ZERO for c in cards}

    today = datetime.date.today()
    month_num = 0
    max_months = 600

    while any(b > _ZERO for b in balances.values()) and month_num < max_months:
        month_num += 1
        month_date = _add_months(today, month_num)

        for cid in list(balances.keys()):
            bal = balances[cid]
            if bal <= _ZERO:
                schedules[cid].append({
                    "month": month_num,
                    "date": month_date.isoformat(),
                    "payment": "0.00",
                    "principal": "0.00",
                    "interest": "0.00",
                    "balance": "0.00",
                })
                continue

            monthly_rate = aprs[cid] / Decimal("100") / Decimal("12")
            interest = _cents(bal * monthly_rate)
            new_bal = _cents(bal + interest)
            total_interest[cid] += interest

            payment = min(allocations[cid], new_bal)
            new_bal = _cents(new_bal - payment)
            balances[cid] = max(new_bal, _ZERO)

            principal = _cents(payment - interest)
            schedules[cid].append({
                "month": month_num,
                "date": month_date.isoformat(),
                "payment": str(_cents(payment)),
                "principal": str(_cents(principal)),
                "interest": str(_cents(interest)),
                "balance": str(_cents(balances[cid])),
            })

    payoff_date = _add_months(today, month_num)
    grand_interest = _cents(sum(total_interest.values(), _ZERO))

    per_card = []
    for c in cards:
        cid = c["id"]
        payoff_month = next(
            (row["month"] for row in schedules[cid] if Decimal(row["balance"]) <= _ZERO),
            month_num,
        )
        per_card.append({
            "account_id": cid,
            "account_name": names[cid],
            "payoff_month": payoff_month,
            "payoff_date": _add_months(today, payoff_month).isoformat(),
            "total_interest": str(_cents(total_interest[cid])),
            "schedule": schedules[cid],
        })

    return AmortizationResult(
        strategy="custom",
        months_to_payoff=month_num,
        total_interest_paid=grand_interest,
        payoff_date=payoff_date,
        per_card_schedule=per_card,
    )
