"""services/money/settlement_apply — the settlement "apply to month"
write action (phase-3 slice 3, docs/design/plusroi-forms.md §6; ADR #024).

Rian's month-end apply: TAG selected paid invoices + still-untagged
expenses with a ``settlement_month`` (first-of-month). The FROZEN
settlement engine (``services/money/settlement.py``) already groups by
that tag with NO status filter — this module only sets the tag, so the
engine reflects the apply with zero engine change.

Money-safety rules (each a counted skip, never a silent write):

  * an invoice tags only when its status is ``paid`` — the engine has no
    status filter, so tagging an unpaid/cancelled invoice would pollute
    the split (``skipped_not_paid``);
  * a row already carrying a tag is NEVER silently moved between months
    (``skipped_already_tagged``) — re-tagging is a deliberate correction
    done on the row itself, not through a batch apply;
  * an id that doesn't resolve inside THIS tenant is a hard 404 —
    all-or-nothing, so a stale picker can't half-apply.

The apply picker needs no new reads: paid-but-untagged invoices are
``/api/books/invoices?month=unsettled&status=paid`` and untagged
expenses are ``/api/books/expenses?month=unsettled`` (the existing
ledger lists).

Owner-gated + writable-books + tenant-scoped (``services.plusroi_gates``)
+ ONE audit row with complete before/after tag images.
"""

from __future__ import annotations

import uuid

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models import Expense, Invoice
from app.services.audit import write_audit
from app.services.errors import ServiceError
from app.services.money.invoices_plus import parse_month
from app.services.plusroi_gates import (
    require_owner,
    require_tenant,
    require_writable_books,
)


def _parse_ids(raw, label: str) -> list[uuid.UUID]:
    if raw is None:
        return []
    if not isinstance(raw, list):
        raise ServiceError(422, "BAD_IDS", f"{label} must be a list of UUIDs.")
    out: list[uuid.UUID] = []
    for value in raw:
        try:
            out.append(uuid.UUID(str(value)))
        except (ValueError, TypeError) as exc:
            raise ServiceError(
                422, "BAD_IDS", f"{label} must be a list of UUIDs.", f"Got {value!r}."
            ) from exc
    return out


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


def apply_to_month(session: Session, actor, payload: dict) -> dict:
    """POST /api/settlement/apply {month, invoice_ids?, expense_ids?}."""
    require_owner(actor, "Only an owner can apply a settlement month.")
    require_writable_books(session, actor)
    tenant_id = require_tenant(actor)

    month = parse_month(payload.get("month"))
    month_key = _month_key(month)
    invoice_ids = _parse_ids(payload.get("invoice_ids"), "invoice_ids")
    expense_ids = _parse_ids(payload.get("expense_ids"), "expense_ids")
    if not invoice_ids and not expense_ids:
        raise ServiceError(
            422, "NO_SELECTION", "Pick at least one invoice or expense to apply."
        )

    invoices = list(
        session.scalars(
            select(Invoice).where(
                Invoice.id.in_(invoice_ids), Invoice.tenant_id == tenant_id
            )
        ).all()
    ) if invoice_ids else []
    if len(invoices) != len(set(invoice_ids)):
        raise ServiceError(
            404,
            "INVOICES_NOT_FOUND",
            "Some selected invoices weren't found in these books.",
            f"{len(set(invoice_ids)) - len(invoices)} of {len(set(invoice_ids))}"
            " ids didn't resolve. Refresh the picker and retry.",
        )
    expenses = list(
        session.scalars(
            select(Expense).where(
                Expense.id.in_(expense_ids), Expense.tenant_id == tenant_id
            )
        ).all()
    ) if expense_ids else []
    if len(expenses) != len(set(expense_ids)):
        raise ServiceError(
            404,
            "EXPENSES_NOT_FOUND",
            "Some selected expenses weren't found in these books.",
            f"{len(set(expense_ids)) - len(expenses)} of {len(set(expense_ids))}"
            " ids didn't resolve. Refresh the picker and retry.",
        )

    before_invoices = []
    before_expenses = []
    tagged_invoices: list[Invoice] = []
    tagged_expenses: list[Expense] = []
    skipped_not_paid = 0
    skipped_tagged_invoices = 0
    skipped_tagged_expenses = 0

    for invoice in invoices:
        if invoice.status != "paid":
            skipped_not_paid += 1
            continue
        if invoice.settlement_month is not None:
            skipped_tagged_invoices += 1
            continue
        before_invoices.append({"id": str(invoice.id), "settlement_month": None})
        invoice.settlement_month = month
        tagged_invoices.append(invoice)

    for expense in expenses:
        if expense.settlement_month is not None:
            skipped_tagged_expenses += 1
            continue
        before_expenses.append({"id": str(expense.id), "settlement_month": None})
        expense.settlement_month = month
        tagged_expenses.append(expense)

    if not tagged_invoices and not tagged_expenses:
        raise ServiceError(
            409,
            "NOTHING_TO_APPLY",
            "Nothing left to apply — every selected row is already tagged or"
            " not paid yet.",
            f"Skipped: {skipped_not_paid} not paid,"
            f" {skipped_tagged_invoices + skipped_tagged_expenses} already"
            " tagged with a settlement month.",
        )

    write_audit(
        session,
        actor,
        action="settlement.apply",
        entity_type="tenant",
        entity_id=tenant_id,
        before={"invoices": before_invoices, "expenses": before_expenses},
        after={
            "month": month_key,
            "invoice_ids": [str(i.id) for i in tagged_invoices],
            "expense_ids": [str(e.id) for e in tagged_expenses],
            "skipped_not_paid": skipped_not_paid,
            "skipped_already_tagged_invoices": skipped_tagged_invoices,
            "skipped_already_tagged_expenses": skipped_tagged_expenses,
        },
    )
    session.commit()
    return {
        "ok": True,
        "month": month_key,
        "invoices_tagged": len(tagged_invoices),
        "expenses_tagged": len(tagged_expenses),
        "skipped_not_paid": skipped_not_paid,
        "skipped_already_tagged": skipped_tagged_invoices + skipped_tagged_expenses,
    }


__all__ = ["apply_to_month"]
