from decimal import Decimal, InvalidOperation
from datetime import date

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

from app.blueprints.bills import bills_bp
from app.blueprints.bills.forms import BillForm, DebtForm, ActionForm
from app.extensions import db
from app.models.bill import Bill
from app.models.debt import Debt
from app.models.category import Category
from app.models.transaction import Transaction
from app.models.account import Account
from app.utils.date_utils import add_months


# ── helpers ───────────────────────────────────────────────────────────────────

def _next_due_date(due_day: int, after: date | None = None) -> date:
    """Return the next calendar date on which `due_day` falls, >= today (or after)."""
    today = after or date.today()
    year, month = today.year, today.month
    # Clamp day to last day of month
    try:
        candidate = date(year, month, min(due_day, _days_in_month(year, month)))
    except ValueError:
        candidate = date(year, month, _days_in_month(year, month))
    if candidate < today:
        # Advance to next month
        next_month = add_months(date(year, month, 1), 1)
        candidate = date(
            next_month.year,
            next_month.month,
            min(due_day, _days_in_month(next_month.year, next_month.month)),
        )
    return candidate


def _days_in_month(year: int, month: int) -> int:
    import calendar
    return calendar.monthrange(year, month)[1]


def _bill_status(due_date_str: str) -> tuple[str, int]:
    """Return (status_label, days_until_due) for a bill due_date string."""
    today = date.today()
    due = date.fromisoformat(due_date_str)
    delta = (due - today).days
    if delta < 0:
        return "Overdue", delta
    elif delta == 0:
        return "Due Today", 0
    else:
        return "Upcoming", delta


def _category_choices():
    cats = Category.query.filter_by(is_active=True).order_by(Category.name).all()
    choices = [(0, "— None —")] + [(c.id, c.name) for c in cats]
    return choices


# ── bills list ────────────────────────────────────────────────────────────────

@bills_bp.route("/")
def index():
    show_all = request.args.get("all") == "1"
    today = date.today()
    cutoff = date(today.year, today.month + 1 if today.month < 12 else 1,
                  today.day) if not show_all else None

    bills_query = Bill.query.filter_by(is_active=True)
    debts_query = Debt.query.filter_by(is_active=True)

    bills = bills_query.all()
    debts = debts_query.all()

    # Ensure due_date is populated
    for bill in bills:
        if not bill.due_date:
            bill.due_date = _next_due_date(bill.due_day).isoformat()
    db.session.commit()

    # Build unified display list
    items = []
    for bill in bills:
        status, days = _bill_status(bill.due_date)
        if not show_all and days > 30:
            continue
        items.append({
            'type': 'bill',
            'obj': bill,
            'status': status,
            'days': days,
            'due_date': date.fromisoformat(bill.due_date),
            'name': bill.name,
            'amount': bill.amount,
        })

    for debt in debts:
        if debt.due_date:
            status, days = _bill_status(debt.due_date)
            if not show_all and days > 30:
                continue
            items.append({
                'type': 'debt',
                'obj': debt,
                'status': status,
                'days': days,
                'due_date': date.fromisoformat(debt.due_date),
                'name': debt.name,
                'amount': debt.min_payment,
            })

    items.sort(key=lambda x: x['due_date'])

    action_form = ActionForm()
    return render_template(
        "bills/index.html",
        items=items,
        show_all=show_all,
        action_form=action_form,
        active_page="bills",
    )


# ── bill CRUD ─────────────────────────────────────────────────────────────────

@bills_bp.route("/create", methods=["GET", "POST"])
def create():
    form = BillForm()
    form.category_id.choices = _category_choices()

    if form.validate_on_submit():
        errors = _validate_bill_form(form)
        if errors:
            for msg in errors:
                flash(msg, "error")
            return render_template("bills/bill_form.html", form=form,
                                   action_url=url_for("bills.create"),
                                   title="Add Bill", active_page="bills")

        due_day = int(form.due_day.data)
        bill = Bill(
            name=form.name.data.strip(),
            amount=Decimal(form.amount.data.strip()),
            due_day=due_day,
            payee=(form.payee.data or "").strip() or None,
            category_id=form.category_id.data or None,
            is_active=True,
            due_date=_next_due_date(due_day).isoformat(),
        )
        db.session.add(bill)
        db.session.commit()
        flash("Bill added.", "success")
        return redirect(url_for("bills.index"))

    return render_template("bills/bill_form.html", form=form,
                           action_url=url_for("bills.create"),
                           title="Add Bill", active_page="bills")


@bills_bp.route("/<int:bill_id>/edit", methods=["GET", "POST"])
def edit(bill_id):
    bill = Bill.query.get_or_404(bill_id)
    form = BillForm(obj=bill)
    form.category_id.choices = _category_choices()

    if request.method == "GET":
        form.due_day.data = str(bill.due_day)
        form.amount.data = str(bill.amount)
        form.category_id.data = bill.category_id or 0
        return render_template("bills/bill_form.html", form=form,
                               action_url=url_for("bills.edit", bill_id=bill_id),
                               title="Edit Bill", active_page="bills")

    if form.validate_on_submit():
        errors = _validate_bill_form(form)
        if errors:
            for msg in errors:
                flash(msg, "error")
            return render_template("bills/bill_form.html", form=form,
                                   action_url=url_for("bills.edit", bill_id=bill_id),
                                   title="Edit Bill", active_page="bills")

        due_day = int(form.due_day.data)
        bill.name = form.name.data.strip()
        bill.amount = Decimal(form.amount.data.strip())
        bill.due_day = due_day
        bill.payee = (form.payee.data or "").strip() or None
        bill.category_id = form.category_id.data or None
        bill.is_active = form.is_active.data
        bill.due_date = _next_due_date(due_day).isoformat()
        db.session.commit()
        flash("Bill updated.", "success")
        return redirect(url_for("bills.index"))

    return render_template("bills/bill_form.html", form=form,
                           action_url=url_for("bills.edit", bill_id=bill_id),
                           title="Edit Bill", active_page="bills")


@bills_bp.route("/<int:bill_id>/delete", methods=["POST"])
def delete(bill_id):
    bill = Bill.query.get_or_404(bill_id)
    db.session.delete(bill)
    db.session.commit()
    flash("Bill deleted.", "success")
    return redirect(url_for("bills.index"))


@bills_bp.route("/<int:bill_id>/toggle-active", methods=["POST"])
def toggle_active(bill_id):
    bill = Bill.query.get_or_404(bill_id)
    bill.is_active = not bill.is_active
    db.session.commit()
    status = "activated" if bill.is_active else "deactivated"
    flash(f"Bill {status}.", "success")
    return redirect(url_for("bills.index",
                            **{"all": "1"} if not bill.is_active else {}))


# ── mark as paid ──────────────────────────────────────────────────────────────

@bills_bp.route("/<int:bill_id>/pay", methods=["POST"])
def mark_paid(bill_id):
    bill = Bill.query.get_or_404(bill_id)
    today = date.today()

    # Resolve account — use Cash/first available
    account = Account.query.filter_by(is_active=True).first()
    if account is None:
        flash("No accounts exist. Please add an account first.", "error")
        return redirect(url_for("bills.index"))

    # Resolve category — use bill's category or fallback to Uncategorized
    category = None
    if bill.category_id:
        category = Category.query.get(bill.category_id)
    if category is None:
        category = Category.query.filter_by(name="Uncategorized").first()

    txn = Transaction(
        date=today.isoformat(),
        merchant_normalized=bill.name,
        amount=bill.amount,
        is_credit=False,
        is_manual=True,
        account_id=account.id,
        category_id=category.id if category else None,
    )
    db.session.add(txn)

    bill.last_paid_date = today.isoformat()
    # Advance next due date by one month from current due date
    current_due = date.fromisoformat(bill.due_date) if bill.due_date else today
    next_due = add_months(date(current_due.year, current_due.month, 1), 1)
    bill.due_date = _next_due_date(bill.due_day, next_due).isoformat()

    db.session.commit()
    flash("Bill marked as paid. Transaction logged.", "success")
    return redirect(url_for("bills.index"))


# ── debt CRUD ─────────────────────────────────────────────────────────────────

@bills_bp.route("/debts/create", methods=["GET", "POST"])
def create_debt():
    form = DebtForm()

    if form.validate_on_submit():
        errors = _validate_debt_form(form)
        if errors:
            for msg in errors:
                flash(msg, "error")
            return render_template("bills/debt_form.html", form=form,
                                   action_url=url_for("bills.create_debt"),
                                   title="Add Debt", active_page="bills")

        debt = Debt(
            name=form.name.data.strip(),
            current_balance=Decimal(form.current_balance.data.strip()),
            interest_rate=Decimal(form.interest_rate.data.strip()),
            min_payment=Decimal(form.min_payment.data.strip()),
            due_date=(form.due_date.data or "").strip() or None,
            is_active=True,
        )
        db.session.add(debt)
        db.session.commit()
        flash("Debt added.", "success")
        return redirect(url_for("bills.index"))

    return render_template("bills/debt_form.html", form=form,
                           action_url=url_for("bills.create_debt"),
                           title="Add Debt", active_page="bills")


@bills_bp.route("/debts/<int:debt_id>/edit", methods=["GET", "POST"])
def edit_debt(debt_id):
    debt = Debt.query.get_or_404(debt_id)
    form = DebtForm(obj=debt)

    if request.method == "GET":
        form.current_balance.data = str(debt.current_balance)
        form.interest_rate.data = str(debt.interest_rate)
        form.min_payment.data = str(debt.min_payment)
        return render_template("bills/debt_form.html", form=form,
                               action_url=url_for("bills.edit_debt", debt_id=debt_id),
                               title="Edit Debt", active_page="bills")

    if form.validate_on_submit():
        errors = _validate_debt_form(form)
        if errors:
            for msg in errors:
                flash(msg, "error")
            return render_template("bills/debt_form.html", form=form,
                                   action_url=url_for("bills.edit_debt", debt_id=debt_id),
                                   title="Edit Debt", active_page="bills")

        debt.name = form.name.data.strip()
        debt.current_balance = Decimal(form.current_balance.data.strip())
        debt.interest_rate = Decimal(form.interest_rate.data.strip())
        debt.min_payment = Decimal(form.min_payment.data.strip())
        debt.due_date = (form.due_date.data or "").strip() or None
        db.session.commit()
        flash("Debt updated.", "success")
        return redirect(url_for("bills.index"))

    return render_template("bills/debt_form.html", form=form,
                           action_url=url_for("bills.edit_debt", debt_id=debt_id),
                           title="Edit Debt", active_page="bills")


@bills_bp.route("/debts/<int:debt_id>/delete", methods=["POST"])
def delete_debt(debt_id):
    debt = Debt.query.get_or_404(debt_id)
    db.session.delete(debt)
    db.session.commit()
    flash("Debt deleted.", "success")
    return redirect(url_for("bills.index"))


# ── validation helpers ────────────────────────────────────────────────────────

def _validate_bill_form(form):
    errors = []
    raw_amount = (form.amount.data or "").strip()
    try:
        amount = Decimal(raw_amount)
        if amount <= 0:
            errors.append("Amount must be greater than zero.")
    except InvalidOperation:
        errors.append("Enter a valid numeric amount.")

    try:
        day = int(form.due_day.data or "")
        if not (1 <= day <= 31):
            errors.append("Due day must be between 1 and 31.")
    except (TypeError, ValueError):
        errors.append("Due day must be a number between 1 and 31.")

    return errors


def _validate_debt_form(form):
    errors = []
    for field_name, label in [
        ("current_balance", "Current balance"),
        ("min_payment", "Minimum payment"),
    ]:
        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 amount.")

    raw_rate = (form.interest_rate.data or "").strip()
    try:
        rate = Decimal(raw_rate)
        if rate < 0:
            errors.append("Interest rate must be zero or greater.")
    except InvalidOperation:
        errors.append("Interest rate: enter a valid numeric value.")

    raw_due = (form.due_date.data or "").strip()
    if raw_due:
        try:
            date.fromisoformat(raw_due)
        except ValueError:
            errors.append("Due date must be in YYYY-MM-DD format.")

    return errors
