from decimal import Decimal, InvalidOperation
from datetime import date

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

from app.blueprints.budgets import budgets_bp
from app.blueprints.budgets.forms import BudgetEntryForm, BudgetRecommendForm
from app.extensions import db
from app.models.budget import Budget
from app.models.category import Category
from app.models.settings import Settings
from app.models.transaction import Transaction
from app.services.insights.budget_recommender import fifty_thirty_twenty


def _build_actuals(year, month, existing, active_categories):
    """Return list of per-category display dicts with actuals and progress state."""
    month_prefix = f'{year}-{month:02d}-%'
    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 = {row.category_id: row.total for row in spent_rows}

    rows = []
    for cat in active_categories:
        budget = existing.get(cat.id)
        budgeted = budget.amount if budget else Decimal('0')
        spent = actuals.get(cat.id, Decimal('0'))
        remaining = budgeted - spent
        if budgeted > 0:
            pct = int(spent / budgeted * 100)
        else:
            pct = 0
        state = 'danger' if pct >= 100 else ('warning' if pct >= 80 else 'safe')
        rows.append({
            'category': cat,
            'budgeted': budgeted,
            'has_budget': budget is not None,
            'spent': spent,
            'remaining': remaining,
            'pct': pct,
            'state': state,
        })
    return rows


def _available_months():
    """Return sorted list of (year, month) tuples that have budget or transaction data."""
    budget_months = (
        db.session.query(Budget.year, Budget.month)
        .distinct()
        .all()
    )
    txn_months = (
        db.session.query(
            func.cast(func.substr(Transaction.date, 1, 4), db.Integer).label('year'),
            func.cast(func.substr(Transaction.date, 6, 2), db.Integer).label('month'),
        )
        .distinct()
        .all()
    )
    months = set(budget_months) | set(txn_months)
    today = date.today()
    months.add((today.year, today.month))
    return sorted(months, reverse=True)


def _parse_month_param(param):
    """Parse '?month=YYYY-MM' into (year, month). Returns today's year/month on failure."""
    today = date.today()
    if param:
        try:
            parts = param.split('-')
            return int(parts[0]), int(parts[1])
        except (AttributeError, IndexError, ValueError):
            pass
    return today.year, today.month


@budgets_bp.route("/", methods=["GET", "POST"])
def index():
    today = date.today()

    selected_year, selected_month = _parse_month_param(request.args.get('month'))
    selected_key = f'{selected_year}-{selected_month:02d}'
    is_current_month = (selected_year == today.year and selected_month == today.month)

    available_months = _available_months()

    active_categories = (
        Category.query.filter_by(is_active=True).order_by(Category.name).all()
    )
    existing = {
        b.category_id: b
        for b in Budget.query.filter_by(month=selected_month, year=selected_year).all()
    }

    display_rows = _build_actuals(selected_year, selected_month, existing, active_categories)
    form = BudgetEntryForm()
    error_cat_id = None

    def _render_index():
        return render_template(
            "budgets/index.html",
            categories=active_categories,
            existing=existing,
            display_rows=display_rows,
            form=form,
            error_cat_id=error_cat_id,
            selected_key=selected_key,
            selected_year=selected_year,
            selected_month=selected_month,
            is_current_month=is_current_month,
            available_months=available_months,
            active_page="budgets",
        )

    if request.method == "POST":
        if not is_current_month:
            flash("Budget amounts can only be edited for the current month.", "error")
            return redirect(url_for("budgets.index"))

        if form.validate_on_submit():
            try:
                # Read directly from request.form — form.category_id.data is unreliable
                # when hidden_tag() renders a competing empty hidden field first.
                cat_id = int(request.form.get('category_id', ''))
            except (TypeError, ValueError):
                abort(404)

            cat = Category.query.get_or_404(cat_id)

            raw = (form.amount.data or "").strip()
            try:
                amount = Decimal(raw)
            except InvalidOperation:
                form.amount.errors.append("Enter a valid numeric amount.")
                error_cat_id = cat.id
                return _render_index()

            if amount < 0:
                form.amount.errors.append("Amount must be zero or greater.")
                error_cat_id = cat.id
                return _render_index()

            budget = existing.get(cat.id)
            if budget is None:
                budget = Budget(
                    category_id=cat.id,
                    month=today.month,
                    year=today.year,
                    amount=amount,
                )
                db.session.add(budget)
            else:
                budget.amount = amount
            db.session.commit()
            flash("Budget saved.", "success")
            return redirect(url_for("budgets.index"))

        # form.validate_on_submit() failed (empty amount or CSRF)
        try:
            error_cat_id = int(request.form.get('category_id', 0) or 0) or None
        except (TypeError, ValueError):
            error_cat_id = None

    return _render_index()


def _render_recommend(form, items, monthly_income, errors, submitted):
    return render_template(
        "budgets/recommend.html",
        has_income=True,
        monthly_income=monthly_income,
        items=items,
        form=form,
        errors=errors,
        submitted=submitted,
        active_page="budgets",
    )


@budgets_bp.route("/recommend", methods=["GET", "POST"])
def recommend():
    settings = Settings.query.first()
    monthly_income = settings.monthly_income if settings else None

    if monthly_income is None:
        return render_template(
            "budgets/recommend.html",
            has_income=False,
            active_page="budgets",
        )

    cat_map = {
        c.name: c
        for c in Category.query.filter_by(is_active=True).all()
    }
    recommendations = fifty_thirty_twenty(monthly_income)
    # Pair each recommendation with its DB Category (None when not found)
    items = [(rec, cat_map.get(rec.category_name)) for rec in recommendations]

    form = BudgetRecommendForm()

    if form.validate_on_submit():
        errors = {}
        amounts = {}

        for rec, cat in items:
            if cat is None or rec.bucket == "savings":
                continue
            raw = request.form.get(f"amount_{cat.id}", "").strip()
            if not raw:
                continue
            try:
                amount = Decimal(raw)
            except InvalidOperation:
                errors[cat.id] = "Enter a valid numeric amount."
                continue
            if amount < 0:
                errors[cat.id] = "Amount must be zero or greater."
                continue
            if amount > 0:
                amounts[cat.id] = amount

        if errors:
            submitted = {f"amount_{cat.id}": request.form.get(f"amount_{cat.id}", "") for _, cat in items if cat}
            return _render_recommend(form, items, monthly_income, errors, submitted)

        total = sum(amounts.values(), Decimal("0"))
        if total > monthly_income:
            flash(
                f"Total (${total:.2f}) exceeds monthly income (${monthly_income:.2f}). "
                "Please reduce some amounts.",
                "error",
            )
            submitted = {f"amount_{cat.id}": request.form.get(f"amount_{cat.id}", "") for _, cat in items if cat}
            return _render_recommend(form, items, monthly_income, {}, submitted)

        today = date.today()
        for cat_id, amount in amounts.items():
            existing = Budget.query.filter_by(
                category_id=cat_id, month=today.month, year=today.year
            ).first()
            if existing:
                existing.amount = amount
            else:
                db.session.add(
                    Budget(category_id=cat_id, month=today.month, year=today.year, amount=amount)
                )
        db.session.commit()
        flash("Budgets applied.", "success")
        return redirect(url_for("budgets.index"))

    return _render_recommend(form, items, monthly_income, {}, {})
