from decimal import Decimal, InvalidOperation
from datetime import date, datetime

from flask import render_template, redirect, url_for, flash, request, abort

from app.blueprints.paydown import paydown_bp
from app.blueprints.paydown.forms import CreditCardForm, ExtraPaymentForm, ActionForm
from app.extensions import db
from app.models.account import Account
from app.models.settings import Settings
from app.models.paydown_plan import PaydownPlan
from app.models.paydown_plan_card import PaydownPlanCard
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


_STRATEGIES = [
    ("avalanche",       "Avalanche",       "Highest APR first"),
    ("snowball",        "Snowball",         "Lowest balance first"),
    ("highest_balance", "Highest Balance",  "Largest balance first"),
    ("proportional",    "Proportional",     "Distributed by balance"),
    ("custom",          "Custom",           "You specify each card"),
]

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


def _credit_cards():
    return Account.query.filter_by(type="credit", is_active=True).all()


def _cards_as_dicts(cards):
    return [
        {
            "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
        if (c.current_balance or Decimal("0")) > Decimal("0")
    ]


def _active_plan():
    return PaydownPlan.query.filter_by(status="active").first()


# ── landing / card list ───────────────────────────────────────────────────────

@paydown_bp.route("/")
def index():
    cards = _credit_cards()
    active_plan = _active_plan()
    settings = Settings.query.first()
    extra = settings.extra_monthly_payment if settings else None
    form = ActionForm()
    return render_template(
        "paydown/index.html",
        cards=cards,
        extra=extra,
        active_plan=active_plan,
        form=form,
        active_page="paydown",
    )


# ── credit card CRUD (Story 6.1) ───────────────────────────────────────────────

@paydown_bp.route("/cards/create", methods=["GET", "POST"])
def create_card():
    form = CreditCardForm()
    if form.validate_on_submit():
        errors = _validate_card_form(form)
        if errors:
            for msg in errors:
                flash(msg, "error")
            return render_template("paydown/card_form.html", form=form,
                                   action_url=url_for("paydown.create_card"),
                                   title="Add Credit Card", active_page="paydown")

        card = Account(
            name=form.name.data.strip(),
            institution_name=(form.institution_name.data or "").strip() or None,
            type="credit",
            is_active=True,
            current_balance=Decimal(form.current_balance.data.strip()),
            apr=Decimal(form.apr.data.strip()),
            min_payment=Decimal(form.min_payment.data.strip()),
            credit_limit=Decimal(form.credit_limit.data.strip()) if (form.credit_limit.data or "").strip() else None,
        )
        db.session.add(card)
        db.session.commit()
        flash("Card added.", "success")
        return redirect(url_for("paydown.index"))

    return render_template("paydown/card_form.html", form=form,
                           action_url=url_for("paydown.create_card"),
                           title="Add Credit Card", active_page="paydown")


@paydown_bp.route("/cards/<int:card_id>/edit", methods=["GET", "POST"])
def edit_card(card_id):
    card = Account.query.filter_by(id=card_id, type="credit").first_or_404()
    form = CreditCardForm(obj=card)

    if request.method == "GET":
        form.current_balance.data = str(card.current_balance or "")
        form.apr.data = str(card.apr or "")
        form.min_payment.data = str(card.min_payment or "")
        form.credit_limit.data = str(card.credit_limit or "")
        return render_template("paydown/card_form.html", form=form,
                               action_url=url_for("paydown.edit_card", card_id=card_id),
                               title="Edit Credit Card", active_page="paydown")

    if form.validate_on_submit():
        errors = _validate_card_form(form)
        if errors:
            for msg in errors:
                flash(msg, "error")
            return render_template("paydown/card_form.html", form=form,
                                   action_url=url_for("paydown.edit_card", card_id=card_id),
                                   title="Edit Credit Card", active_page="paydown")

        card.name = form.name.data.strip()
        card.institution_name = (form.institution_name.data or "").strip() or None
        card.current_balance = Decimal(str(form.current_balance.data).strip())
        card.apr = Decimal(str(form.apr.data).strip())
        card.min_payment = Decimal(str(form.min_payment.data).strip())
        raw_limit = str(form.credit_limit.data or "").strip()
        card.credit_limit = Decimal(raw_limit) if raw_limit else None
        db.session.commit()
        flash("Card updated.", "success")
        return redirect(url_for("paydown.index"))

    return render_template("paydown/card_form.html", form=form,
                           action_url=url_for("paydown.edit_card", card_id=card_id),
                           title="Edit Credit Card", active_page="paydown")


# ── extra monthly payment ─────────────────────────────────────────────────────

@paydown_bp.route("/extra-payment", methods=["GET", "POST"])
def extra_payment():
    settings = Settings.query.first()
    form = ExtraPaymentForm()

    if request.method == "GET":
        if settings and settings.extra_monthly_payment is not None:
            form.extra_monthly_payment.data = str(settings.extra_monthly_payment)
        return render_template("paydown/extra_payment.html", form=form, active_page="paydown")

    if form.validate_on_submit():
        raw = (form.extra_monthly_payment.data or "").strip()
        try:
            amount = Decimal(raw)
        except InvalidOperation:
            form.extra_monthly_payment.errors.append("Enter a valid numeric amount.")
            return render_template("paydown/extra_payment.html", form=form, active_page="paydown")
        if amount < 0:
            form.extra_monthly_payment.errors.append("Amount must be zero or greater.")
            return render_template("paydown/extra_payment.html", form=form, active_page="paydown")

        if settings is None:
            settings = Settings(id=1, extra_monthly_payment=amount)
            db.session.add(settings)
        else:
            settings.extra_monthly_payment = amount
        db.session.commit()
        flash("Extra monthly payment saved.", "success")
        return redirect(url_for("paydown.compare"))

    return render_template("paydown/extra_payment.html", form=form, active_page="paydown")


# ── strategy comparison (Story 6.3) ──────────────────────────────────────────

@paydown_bp.route("/compare")
def compare():
    cards = _credit_cards()
    card_dicts = _cards_as_dicts(cards)
    settings = Settings.query.first()
    extra = settings.extra_monthly_payment if settings else None
    active_plan = _active_plan()

    if extra is None:
        return redirect(url_for("paydown.extra_payment"))

    if not card_dicts:
        return render_template(
            "paydown/compare.html",
            no_debt=True,
            cards=cards,
            active_page="paydown",
        )

    results = {}
    for key, fn in _STRATEGY_CALC.items():
        results[key] = fn(card_dicts, extra)

    return render_template(
        "paydown/compare.html",
        no_debt=False,
        strategies=_STRATEGIES,
        results=results,
        cards=cards,
        extra=extra,
        active_plan=active_plan,
        active_page="paydown",
    )


# ── strategy detail (Story 6.4) ───────────────────────────────────────────────

@paydown_bp.route("/strategy/<strategy_name>")
def strategy_detail(strategy_name):
    if strategy_name not in _STRATEGY_CALC and strategy_name != "custom":
        abort(404)

    cards = _credit_cards()
    card_dicts = _cards_as_dicts(cards)
    settings = Settings.query.first()
    extra = settings.extra_monthly_payment if settings else Decimal("0")
    active_plan = _active_plan()

    if strategy_name == "custom":
        result = calculate_custom(card_dicts, {})
    else:
        result = _STRATEGY_CALC[strategy_name](card_dicts, extra)

    strategy_labels = {s[0]: s[1] for s in _STRATEGIES}

    action_form = ActionForm()
    return render_template(
        "paydown/strategy_detail.html",
        result=result,
        strategy_name=strategy_name,
        strategy_label=strategy_labels.get(strategy_name, strategy_name),
        cards=cards,
        extra=extra,
        active_plan=active_plan,
        action_form=action_form,
        active_page="paydown",
    )


# ── plan activation (Story 6.5) ───────────────────────────────────────────────

@paydown_bp.route("/activate/<strategy_name>", methods=["POST"])
def activate_plan(strategy_name):
    if strategy_name not in _STRATEGY_CALC and strategy_name != "custom":
        abort(404)

    cards = _credit_cards()
    card_dicts = _cards_as_dicts(cards)
    settings = Settings.query.first()
    extra = settings.extra_monthly_payment if settings else Decimal("0")

    if not card_dicts:
        flash("No credit card debt to plan.", "error")
        return redirect(url_for("paydown.index"))

    if strategy_name == "custom":
        result = calculate_custom(card_dicts, {})
    else:
        result = _STRATEGY_CALC[strategy_name](card_dicts, extra)

    _plan_lifecycle_activate(strategy_name, extra, card_dicts, result)

    flash("Plan activated.", "success")
    return redirect(url_for("paydown.monitor"))


def _plan_lifecycle_activate(strategy_name, extra, card_dicts, result):
    """Archive any existing active plan and create a new one. Single transaction."""
    now = date.today()

    # Archive existing active plans
    existing = PaydownPlan.query.filter_by(status="active").all()
    from datetime import datetime
    for plan in existing:
        plan.status = "archived"
        plan.archived_at = datetime.utcnow()

    # Create new plan
    new_plan = PaydownPlan(
        strategy=strategy_name,
        extra_monthly=extra,
        status="active",
    )
    db.session.add(new_plan)
    db.session.flush()  # get plan.id

    # Create PaydownPlanCard for each card
    for card_dict in card_dicts:
        card_result = next(
            (c for c in result.per_card_schedule if c["account_id"] == card_dict["id"]),
            None,
        )
        monthly_alloc = card_dict["min_payment"]  # default
        if card_result and strategy_name != "custom":
            schedule = card_result.get("schedule", [])
            if schedule:
                monthly_alloc = Decimal(schedule[0]["payment"])

        plan_card = PaydownPlanCard(
            plan_id=new_plan.id,
            account_id=card_dict["id"],
            monthly_allocation=monthly_alloc,
            starting_balance=card_dict["balance"],
            starting_apr=card_dict["apr"],
        )
        db.session.add(plan_card)

    db.session.commit()
    return new_plan


# ── paydown monitor (Stories 7.1, 7.2, 7.3) ──────────────────────────────────

@paydown_bp.route("/monitor")
def monitor():
    active_plan = _active_plan()
    action_form = ActionForm()

    if not active_plan:
        return render_template(
            "paydown/monitor.html",
            active_plan=None,
            action_form=action_form,
            active_page="paydown",
        )

    # Rebuild AmortizationResult for this plan
    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, {})

    # Load all balance updates for plan cards
    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
    ]

    statuses = get_monitor_statuses(result, update_dicts)

    return render_template(
        "paydown/monitor.html",
        active_plan=active_plan,
        statuses=statuses,
        action_form=action_form,
        active_page="paydown",
    )


@paydown_bp.route("/monitor/update-balance", methods=["POST"])
def update_balance():
    """Story 7.2 — record a new balance update for a card."""
    active_plan = _active_plan()
    if not active_plan:
        flash("No active plan.", "error")
        return redirect(url_for("paydown.monitor"))

    plan_card_ids = {pc.account_id for pc in active_plan.cards}

    try:
        account_id = int(request.form.get("account_id", 0))
    except (TypeError, ValueError):
        flash("Invalid card.", "error")
        return redirect(url_for("paydown.monitor"))

    if account_id not in plan_card_ids:
        flash("Card not in active plan.", "error")
        return redirect(url_for("paydown.monitor"))

    raw = (request.form.get("balance") or "").strip()
    try:
        balance = Decimal(raw)
        if balance < 0:
            raise ValueError
    except (InvalidOperation, ValueError):
        flash("Enter a valid balance (zero or greater).", "error")
        return redirect(url_for("paydown.monitor"))

    upd = PaydownBalanceUpdate(account_id=account_id, balance=balance)
    db.session.add(upd)
    db.session.commit()
    flash("Balance updated.", "success")
    return redirect(url_for("paydown.monitor"))


@paydown_bp.route("/switch-strategy", methods=["POST"])
def switch_strategy():
    """Story 7.3 — switch to a different strategy, recalculating from current balances."""
    new_strategy = request.form.get("strategy", "")
    if new_strategy not in _STRATEGY_CALC and new_strategy != "custom":
        flash("Invalid strategy.", "error")
        return redirect(url_for("paydown.monitor"))

    active_plan = _active_plan()
    if not active_plan:
        flash("No active plan to switch from.", "error")
        return redirect(url_for("paydown.compare"))

    # Use most recent balance updates (or current account balance) as new starting points
    plan_card_ids = [pc.account_id for pc in active_plan.cards]
    cards = Account.query.filter(Account.id.in_(plan_card_ids)).all()

    # Build card_dicts using most-recent balance updates if available
    latest_updates = {}
    updates = (
        PaydownBalanceUpdate.query
        .filter(PaydownBalanceUpdate.account_id.in_(plan_card_ids))
        .all()
    )
    for u in updates:
        if u.account_id not in latest_updates or u.updated_at > latest_updates[u.account_id].updated_at:
            latest_updates[u.account_id] = u

    card_dicts = []
    for c in cards:
        upd = latest_updates.get(c.id)
        balance = upd.balance if upd else (c.current_balance or Decimal("0"))
        card_dicts.append({
            "id": c.id,
            "name": c.name,
            "balance": balance,
            "apr": c.apr or Decimal("0"),
            "min_payment": c.min_payment or Decimal("0"),
        })

    settings = Settings.query.first()
    extra = settings.extra_monthly_payment if settings else Decimal("0")

    fn = _STRATEGY_CALC.get(new_strategy)
    if fn:
        result = fn(card_dicts, extra)
    else:
        result = calculate_custom(card_dicts, {})

    _plan_lifecycle_activate(new_strategy, extra, card_dicts, result)
    flash("Strategy switched. New plan activated.", "success")
    return redirect(url_for("paydown.monitor"))


@paydown_bp.route("/history")
def history():
    """Story 7.3 — view archived plans."""
    archived = (
        PaydownPlan.query
        .filter_by(status="archived")
        .order_by(PaydownPlan.archived_at.desc())
        .all()
    )
    return render_template(
        "paydown/history.html",
        plans=archived,
        active_page="paydown",
    )


# ── validation ────────────────────────────────────────────────────────────────

def _validate_card_form(form):
    errors = []
    for field_name, label in [
        ("current_balance", "Current balance"),
        ("min_payment",     "Minimum payment"),
        ("apr",             "APR"),
    ]:
        raw = (getattr(form, field_name).data or "").strip()
        try:
            val = Decimal(raw)
            if val < 0:
                errors.append(f"{label} must be zero or greater.")
        except InvalidOperation:
            errors.append(f"{label}: enter a valid numeric value.")

    raw_limit = str(form.credit_limit.data or "").strip()
    if raw_limit:
        try:
            val = Decimal(raw_limit)
            if val < 0:
                errors.append("Credit limit must be zero or greater.")
        except InvalidOperation:
            errors.append("Credit limit: enter a valid numeric value.")

    return errors
