"""The PlusROI monthly settlement engine — the Monthly Split (08 §4).

THE formula, calibrated to the cent against the sheet's own Monthly
Split report AND the real BW→PlusROI Wave invoices (2026-07-10; three
target months matched exactly from the raw ledgers):

    gross       = Σ invoices.manual_total   where settlement_month = M
    revenue     = gross − Σ fee − Σ commission_amount
    expenses    = Σ expenses.amount         where settlement_month = M
    bookkeeping = revenue × bookkeeping_pct/100     (tenant setting; 3%)
    net         = revenue − expenses − bookkeeping
    split       = net × partner_split               (tenant setting; 0.5)

Inclusion rule: the ``settlement_month`` tag IS the grouping rule — no
status filter, matching the sheet's own formulas (calibration showed
every tagged row is Paid in practice). Untagged rows are outside every
settlement and surface as the "unsettled" bucket.

Rounding policy (ADR #017): ledger amounts are already 2dp, so gross /
fees / commissions / revenue / expenses are exact. Bookkeeping, net and
split are derived on the FULL-PRECISION chain (bookkeeping unrounded
inside net; net unrounded inside split) and each reported line
quantizes HALF_UP to the cent — the sheet's own math, which is what the
real Wave invoices were cut from (invoice 2026050's split $13,546.48
reproduces exactly under this policy and is a cent off under
round-each-stage). Knife-edge months can therefore show a 1¢
sub-cent artifact between displayed lines; the diff-gate classifies
those honestly rather than the engine hiding them.

The monthly BW invoice (ground truth: Wave invoices 2026035/50/59) =
the split + reimbursement of every expense fronted by a NON-tenant
party that month (Team Cost / PPC Spend / Licences), + GST on top.
``suggested_invoice`` computes exactly those lines.
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass
from datetime import date
from decimal import ROUND_HALF_UP, Decimal

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.models import Expense, Invoice, Party, Tenant
from app.services.errors import ServiceError

CENT = Decimal("0.01")

#: Expense-category → Wave line label (ground truth: the real invoices).
#: Anything not mapped rolls into "Licences / Fees".
_REIMBURSE_LABELS = {
    "Labour": "BowdenWorks Team Cost",
    "Client Advertising Spend": "PPC Spend",
}
_REIMBURSE_FALLBACK = "Licences / Fees"


def _q(value: Decimal | None) -> Decimal:
    return (value or Decimal(0)).quantize(CENT, rounding=ROUND_HALF_UP)


def _require_income(actor) -> None:
    if not actor.capabilities.get("can_see_income"):
        raise ServiceError(
            403,
            "INCOME_FORBIDDEN",
            "Only an owner can view the settlement.",
            "The monthly split is owner-level money data.",
        )


def _tenant_settings(session: Session, tenant_id: uuid.UUID) -> dict:
    tenant = session.get(Tenant, tenant_id)
    return dict(tenant.settings or {}) if tenant else {}


def _month_str(value: date) -> str:
    return f"{value.year:04d}-{value.month:02d}"


def parse_month(raw: str) -> date:
    """'YYYY-MM' → the first-of-month Date the ledgers are tagged with."""
    try:
        year_s, month_s = raw.strip().split("-", 1)
        parsed = date(int(year_s), int(month_s), 1)
    except (ValueError, AttributeError) as exc:
        raise ServiceError(
            422,
            "BAD_MONTH",
            "month must look like YYYY-MM",
            f"Got {raw!r}.",
        ) from exc
    return parsed


@dataclass
class MonthRow:
    month: date
    invoice_count: int
    gross: Decimal
    fees: Decimal
    commissions: Decimal
    expense_count: int
    expenses: Decimal

    def compute(self, bookkeeping_pct: Decimal, partner_split: Decimal) -> dict:
        gross = _q(self.gross)
        fees = _q(self.fees)
        commissions = _q(self.commissions)
        revenue = gross - fees - commissions
        expenses = _q(self.expenses)
        # Full-precision chain (see the module docstring's rounding
        # policy): bookkeeping stays unrounded inside net, net stays
        # unrounded inside split; only the REPORTED lines quantize.
        bookkeeping_full = revenue * bookkeeping_pct / Decimal(100)
        net_full = revenue - expenses - bookkeeping_full
        bookkeeping = _q(bookkeeping_full)
        net = _q(net_full)
        split = _q(net_full * partner_split)
        return {
            "month": _month_str(self.month),
            "invoice_count": self.invoice_count,
            "expense_count": self.expense_count,
            "gross": str(gross),
            "fees": str(fees),
            "commissions": str(commissions),
            "revenue": str(revenue),
            "expenses": str(expenses),
            "bookkeeping": str(bookkeeping),
            "net": str(net),
            "split": str(split),
        }


def _month_rows(session: Session, tenant_id: uuid.UUID) -> list[MonthRow]:
    invoice_rows = session.execute(
        select(
            Invoice.settlement_month,
            func.count(Invoice.id),
            func.coalesce(func.sum(Invoice.manual_total), 0),
            func.coalesce(func.sum(Invoice.fee), 0),
            func.coalesce(func.sum(Invoice.commission_amount), 0),
        )
        .where(Invoice.tenant_id == tenant_id, Invoice.settlement_month.is_not(None))
        .group_by(Invoice.settlement_month)
    ).all()
    expense_rows = session.execute(
        select(
            Expense.settlement_month,
            func.count(Expense.id),
            func.coalesce(func.sum(Expense.amount), 0),
        )
        .where(Expense.tenant_id == tenant_id, Expense.settlement_month.is_not(None))
        .group_by(Expense.settlement_month)
    ).all()

    by_month: dict[date, MonthRow] = {}
    for month, count, gross, fees, commissions in invoice_rows:
        by_month[month] = MonthRow(
            month=month,
            invoice_count=count,
            gross=Decimal(gross),
            fees=Decimal(fees),
            commissions=Decimal(commissions),
            expense_count=0,
            expenses=Decimal(0),
        )
    for month, count, amount in expense_rows:
        row = by_month.get(month)
        if row is None:
            row = MonthRow(
                month=month,
                invoice_count=0,
                gross=Decimal(0),
                fees=Decimal(0),
                commissions=Decimal(0),
                expense_count=0,
                expenses=Decimal(0),
            )
            by_month[month] = row
        row.expense_count = count
        row.expenses = Decimal(amount)
    return [by_month[key] for key in sorted(by_month, reverse=True)]


def settlement_summary(session: Session, actor) -> dict:
    """Every settlement month, newest first, plus the unsettled bucket."""
    _require_income(actor)
    settings = _tenant_settings(session, actor.tenant_id)
    bookkeeping_pct = Decimal(str(settings.get("bookkeeping_pct", 3.0)))
    partner_split = Decimal(str(settings.get("partner_split", 0.5)))

    months = [
        row.compute(bookkeeping_pct, partner_split)
        for row in _month_rows(session, actor.tenant_id)
    ]

    unsettled_invoices = session.scalar(
        select(func.count(Invoice.id)).where(
            Invoice.tenant_id == actor.tenant_id,
            Invoice.settlement_month.is_(None),
        )
    )
    unsettled_expenses = session.scalar(
        select(func.count(Expense.id)).where(
            Expense.tenant_id == actor.tenant_id,
            Expense.settlement_month.is_(None),
        )
    )
    return {
        "months": months,
        "unsettled": {
            "invoice_count": unsettled_invoices or 0,
            "expense_count": unsettled_expenses or 0,
        },
        "settings": {
            "bookkeeping_pct": str(bookkeeping_pct),
            "partner_split": str(partner_split),
        },
    }


def settlement_detail(session: Session, actor, month_raw: str) -> dict:
    """One month's full waterfall + the suggested BW invoice lines."""
    _require_income(actor)
    month = parse_month(month_raw)
    settings = _tenant_settings(session, actor.tenant_id)
    bookkeeping_pct = Decimal(str(settings.get("bookkeeping_pct", 3.0)))
    partner_split = Decimal(str(settings.get("partner_split", 0.5)))
    gst_pct = Decimal(str(settings.get("gst_pct", 5.0)))

    rows = [r for r in _month_rows(session, actor.tenant_id) if r.month == month]
    if not rows:
        raise ServiceError(
            404,
            "SETTLEMENT_MONTH_NOT_FOUND",
            f"No settled rows tagged {month_raw}.",
            "Tag invoices/expenses with this settlement month first.",
        )
    computed = rows[0].compute(bookkeeping_pct, partner_split)

    # Expense breakdown by category (whole month).
    categories = [
        {
            "category": category or "(uncategorized)",
            "count": count,
            "amount": str(_q(Decimal(amount))),
        }
        for category, count, amount in session.execute(
            select(
                Expense.category,
                func.count(Expense.id),
                func.coalesce(func.sum(Expense.amount), 0),
            )
            .where(
                Expense.tenant_id == actor.tenant_id,
                Expense.settlement_month == month,
            )
            .group_by(Expense.category)
            .order_by(func.sum(Expense.amount).desc())
        ).all()
    ]

    # Reimbursements: expenses fronted by any party OTHER than the tenant
    # itself (the tenant paying its own bills needs no reimbursement).
    tenant = session.get(Tenant, actor.tenant_id)
    payer = Party.__table__.alias("payer")
    fronted_rows = session.execute(
        select(
            payer.c.name,
            Expense.category,
            func.count(Expense.id),
            func.coalesce(func.sum(Expense.amount), 0),
        )
        .join(payer, payer.c.id == Expense.paid_by_party_id)
        .where(
            Expense.tenant_id == actor.tenant_id,
            Expense.settlement_month == month,
            Expense.paid_by_party_id.is_not(None),
            Expense.paid_by_party_id != tenant.party_id,
        )
        .group_by(payer.c.name, Expense.category)
        .order_by(payer.c.name, func.sum(Expense.amount).desc())
    ).all()

    reimbursements = [
        {
            "paid_by": payer_name,
            "category": category or "(uncategorized)",
            "count": count,
            "amount": str(_q(Decimal(amount))),
        }
        for payer_name, category, count, amount in fronted_rows
    ]

    # Suggested BW invoice: split + fronted expenses grouped into the
    # Wave line vocabulary (ground truth: invoices 2026035/2026050/2026059).
    line_totals: dict[str, Decimal] = {}
    for _payer_name, category, _count, amount in fronted_rows:
        label = _REIMBURSE_LABELS.get(category or "", _REIMBURSE_FALLBACK)
        line_totals[label] = line_totals.get(label, Decimal(0)) + Decimal(amount)

    lines = [
        {
            "label": "PlusROI Net Profit Split",
            "description": "subcontract client work",
            "amount": computed["split"],
        }
    ]
    for label in ("BowdenWorks Team Cost", "PPC Spend", _REIMBURSE_FALLBACK):
        if label in line_totals:
            lines.append(
                {"label": label, "description": "", "amount": str(_q(line_totals[label]))}
            )
    subtotal = sum(Decimal(line["amount"]) for line in lines)
    gst = _q(subtotal * gst_pct / Decimal(100))
    suggested_invoice = {
        "lines": lines,
        "subtotal": str(_q(subtotal)),
        "gst_pct": str(gst_pct),
        "gst": str(gst),
        "total": str(_q(subtotal + gst)),
    }

    return {
        **computed,
        "expense_categories": categories,
        "reimbursements": reimbursements,
        "suggested_invoice": suggested_invoice,
    }
