from __future__ import annotations

from datetime import date
from decimal import Decimal, InvalidOperation

from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, Response
from sqlalchemy.orm import Session

from quartermaster import month_service, service
from quartermaster.db import get_session
from quartermaster.models import SECTION_LABELS, Month, MonthState, Section
from quartermaster.routes import templates

router = APIRouter(prefix="/month/{year_month}", tags=["month"])


def _parse_amount(raw: str, *, allow_zero: bool = True) -> Decimal:
    try:
        amount = Decimal(raw.strip())
    except (InvalidOperation, AttributeError) as exc:
        raise HTTPException(status_code=400, detail="amount must be numeric") from exc
    if amount < 0:
        raise HTTPException(status_code=400, detail="amount must be non-negative")
    if amount == 0 and not allow_zero:
        raise HTTPException(status_code=400, detail="amount must be positive")
    return amount.quantize(Decimal("0.01"))


def _require_year_month(year_month: str) -> str:
    if not month_service.valid_year_month(year_month):
        raise HTTPException(
            status_code=404, detail="year_month must be formatted as YYYY-MM"
        )
    return year_month


def _require_month(db: Session, year_month: str) -> Month:
    _require_year_month(year_month)
    month = month_service.get_month(db, year_month)
    if month is None:
        raise HTTPException(status_code=404, detail="month has not been created yet")
    return month


def _require_editable_month(db: Session, year_month: str) -> Month:
    month = _require_month(db, year_month)
    try:
        month_service.ensure_editable(month)
    except month_service.MonthLifecycleError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return month


def _section_views(month: Month) -> list[month_service.MonthSectionView]:
    return [
        month_service.section_view(month, s, SECTION_LABELS[s]) for s in Section
    ]


def _render_section(
    request: Request, month: Month, section: Section
) -> HTMLResponse:
    view = month_service.section_view(month, section, SECTION_LABELS[section])
    return templates.TemplateResponse(
        request,
        "partials/month_section.html",
        {
            "month": month,
            "section": view,
            "editable": month.state != MonthState.closed,
        },
    )


def _render_target(request: Request, db: Session, month: Month) -> HTMLResponse:
    target = month_service.get_month_target(db, month)
    debt_minimums = [e for e in month.entries if e.section == Section.debt_minimum]
    debt_minimums.sort(key=lambda e: e.id)
    return templates.TemplateResponse(
        request,
        "partials/month_target.html",
        {
            "month": month,
            "target": target,
            "debt_minimums": debt_minimums,
            "editable": month.state != MonthState.closed,
        },
    )


def _render_zero(request: Request, month: Month) -> HTMLResponse:
    zero = month_service.month_zero(month)
    return templates.TemplateResponse(
        request,
        "partials/month_zero.html",
        {
            "zero": zero,
            "planned_tone": service.zero_tone(zero.planned),
            "applied_tone": service.zero_tone(zero.applied),
        },
    )


def _render_group_totals(request: Request, month: Month) -> HTMLResponse:
    return templates.TemplateResponse(
        request,
        "partials/month_group_totals.html",
        {"groups": month_service.month_group_views(month)},
    )


def _append_oob(base: HTMLResponse, *extras: HTMLResponse) -> HTMLResponse:
    body = base.body.decode() + "".join(e.body.decode() for e in extras)
    return HTMLResponse(body)


@router.get("", response_class=HTMLResponse)
def view_month(
    year_month: str,
    request: Request,
    db: Session = Depends(get_session),
) -> HTMLResponse:
    _require_year_month(year_month)
    month = month_service.get_month(db, year_month)
    prev_ym = month_service.shift_year_month(year_month, -1)
    next_ym = month_service.shift_year_month(year_month, 1)
    all_months = month_service.list_months(db)
    if month is None:
        return templates.TemplateResponse(
            request,
            "month_create.html",
            {
                "year_month": year_month,
                "pretty_year_month": month_service.pretty_year_month(year_month),
                "pretty_prev": month_service.pretty_year_month(prev_ym),
                "pretty_next": month_service.pretty_year_month(next_ym),
                "prev_year_month": prev_ym,
                "next_year_month": next_ym,
                "all_months": all_months,
            },
        )
    zero = month_service.month_zero(month)
    editable = month.state != MonthState.closed
    can_close = (
        month.state == MonthState.active
        and zero.applied == Decimal("0.00")
    )
    return templates.TemplateResponse(
        request,
        "month.html",
        {
            "month": month,
            "year_month": year_month,
            "pretty_year_month": month_service.pretty_year_month(year_month),
            "pretty_prev": month_service.pretty_year_month(prev_ym),
            "pretty_next": month_service.pretty_year_month(next_ym),
            "prev_year_month": prev_ym,
            "next_year_month": next_ym,
            "all_months": all_months,
            "groups": month_service.month_group_views(month),
            "target": month_service.get_month_target(db, month),
            "debt_minimums": sorted(
                (e for e in month.entries if e.section == Section.debt_minimum),
                key=lambda e: e.id,
            ),
            "zero": zero,
            "planned_tone": service.zero_tone(zero.planned),
            "applied_tone": service.zero_tone(zero.applied),
            "state": month.state.value,
            "editable": editable,
            "can_close": can_close,
        },
    )


@router.post("/create")
def create_month(
    year_month: str,
    db: Session = Depends(get_session),
) -> Response:
    _require_year_month(year_month)
    try:
        month_service.create_month(db, year_month)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return Response(
        status_code=204,
        headers={"HX-Redirect": f"/month/{year_month}"},
    )


@router.post("/sections/{section}/entries", response_class=HTMLResponse)
def add_month_entry(
    year_month: str,
    section: Section,
    request: Request,
    name: str = Form(...),
    planned: str = Form(...),
    notes: str | None = Form(None),
    db: Session = Depends(get_session),
) -> HTMLResponse:
    month = _require_editable_month(db, year_month)
    clean_name = name.strip()
    if not clean_name:
        raise HTTPException(status_code=400, detail="name is required")
    month_service.add_month_entry(
        db, month, section, clean_name, _parse_amount(planned), notes=notes
    )
    db.refresh(month)
    return _append_oob(
        _render_section(request, month, section),
        _render_zero(request, month),
        _render_group_totals(request, month),
    )


@router.delete("/entries/{entry_id}", response_class=HTMLResponse)
def delete_month_entry(
    year_month: str,
    entry_id: int,
    request: Request,
    db: Session = Depends(get_session),
) -> HTMLResponse:
    month = _require_editable_month(db, year_month)
    section = month_service.delete_month_entry(db, month, entry_id)
    if section is None:
        raise HTTPException(status_code=404, detail="entry not found")
    db.refresh(month)
    extras: list[HTMLResponse] = [
        _render_zero(request, month),
        _render_group_totals(request, month),
    ]
    if section == Section.debt_minimum:
        extras.append(_render_target(request, db, month))
    return _append_oob(_render_section(request, month, section), *extras)


@router.post("/entries/{entry_id}", response_class=HTMLResponse)
def update_month_entry(
    year_month: str,
    entry_id: int,
    request: Request,
    name: str | None = Form(None),
    planned: str | None = Form(None),
    notes: str | None = Form(None),
    db: Session = Depends(get_session),
) -> HTMLResponse:
    month = _require_editable_month(db, year_month)
    clean_name: str | None = None
    if name is not None:
        clean_name = name.strip()
        if not clean_name:
            raise HTTPException(status_code=400, detail="name must not be empty")
    parsed_planned = _parse_amount(planned) if planned is not None else None
    kwargs: dict = dict(
        name=clean_name,
        planned=parsed_planned,
    )
    if notes is not None:
        kwargs["notes"] = notes
    updated = month_service.update_month_entry(db, month, entry_id, **kwargs)
    if updated is None:
        raise HTTPException(status_code=404, detail="entry not found")
    db.refresh(month)
    return _append_oob(
        _render_section(request, month, updated.section),
        _render_zero(request, month),
        _render_group_totals(request, month),
    )


@router.post("/activate")
def activate_month(
    year_month: str,
    db: Session = Depends(get_session),
) -> Response:
    month = _require_month(db, year_month)
    try:
        month_service.activate_month(db, month)
    except month_service.MonthLifecycleError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return Response(
        status_code=204,
        headers={"HX-Redirect": f"/month/{year_month}"},
    )


@router.post("/close")
def close_month(
    year_month: str,
    db: Session = Depends(get_session),
) -> Response:
    month = _require_month(db, year_month)
    try:
        month_service.close_month(db, month)
    except month_service.MonthLifecycleError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return Response(
        status_code=204,
        headers={"HX-Redirect": f"/month/{year_month}"},
    )


@router.post("/reopen")
def reopen_month(
    year_month: str,
    db: Session = Depends(get_session),
) -> Response:
    month = _require_month(db, year_month)
    try:
        month_service.reopen_month(db, month)
    except month_service.MonthLifecycleError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return Response(
        status_code=204,
        headers={"HX-Redirect": f"/month/{year_month}"},
    )


def _parse_date(raw: str) -> date:
    try:
        return date.fromisoformat(raw.strip())
    except (ValueError, AttributeError) as exc:
        raise HTTPException(
            status_code=400, detail="date must be YYYY-MM-DD"
        ) from exc


def _parse_signed_amount(raw: str) -> Decimal:
    try:
        amount = Decimal(raw.strip())
    except (InvalidOperation, AttributeError) as exc:
        raise HTTPException(
            status_code=400, detail="amount must be numeric"
        ) from exc
    return amount.quantize(Decimal("0.01"))


@router.post("/entries/{entry_id}/postings", response_class=HTMLResponse)
def create_posting(
    year_month: str,
    entry_id: int,
    request: Request,
    occurred_on: str = Form(...),
    amount: str = Form(...),
    description: str | None = Form(None),
    payee: str | None = Form(None),
    db: Session = Depends(get_session),
) -> HTMLResponse:
    month = _require_editable_month(db, year_month)
    posting = month_service.add_posting(
        db,
        month,
        entry_id,
        _parse_date(occurred_on),
        _parse_signed_amount(amount),
        description=description,
        payee=payee,
    )
    if posting is None:
        raise HTTPException(status_code=404, detail="entry not found")
    db.refresh(month)
    entry = month_service.get_month_entry(db, month, entry_id)
    return _append_oob(
        _render_section(request, month, entry.section),
        _render_zero(request, month),
        _render_group_totals(request, month),
    )


@router.post("/postings/{posting_id}", response_class=HTMLResponse)
def update_posting(
    year_month: str,
    posting_id: int,
    request: Request,
    occurred_on: str | None = Form(None),
    amount: str | None = Form(None),
    description: str | None = Form(None),
    payee: str | None = Form(None),
    db: Session = Depends(get_session),
) -> HTMLResponse:
    month = _require_editable_month(db, year_month)
    kwargs: dict = {}
    if occurred_on is not None:
        kwargs["occurred_on"] = _parse_date(occurred_on)
    if amount is not None:
        kwargs["amount"] = _parse_signed_amount(amount)
    if description is not None:
        kwargs["description"] = description
    if payee is not None:
        kwargs["payee"] = payee
    updated = month_service.update_posting(db, month, posting_id, **kwargs)
    if updated is None:
        raise HTTPException(status_code=404, detail="posting not found")
    db.refresh(month)
    entry = updated.entry
    return _append_oob(
        _render_section(request, month, entry.section),
        _render_zero(request, month),
        _render_group_totals(request, month),
    )


@router.delete("/postings/{posting_id}", response_class=HTMLResponse)
def delete_posting(
    year_month: str,
    posting_id: int,
    request: Request,
    db: Session = Depends(get_session),
) -> HTMLResponse:
    month = _require_editable_month(db, year_month)
    entry = month_service.delete_posting(db, month, posting_id)
    if entry is None:
        raise HTTPException(status_code=404, detail="posting not found")
    db.refresh(month)
    return _append_oob(
        _render_section(request, month, entry.section),
        _render_zero(request, month),
        _render_group_totals(request, month),
    )


@router.post("/target", response_class=HTMLResponse)
def update_month_target(
    year_month: str,
    request: Request,
    month_entry_id: str = Form(""),
    db: Session = Depends(get_session),
) -> HTMLResponse:
    month = _require_editable_month(db, year_month)
    raw = month_entry_id.strip()
    target_id: int | None
    if raw == "":
        target_id = None
    else:
        try:
            target_id = int(raw)
        except ValueError as exc:
            raise HTTPException(
                status_code=400, detail="month_entry_id must be an integer"
            ) from exc
    try:
        month_service.set_month_target(db, month, target_id)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return _render_target(request, db, month)
