"""Ledger posting service — the only sanctioned way to write to the ledger.

`post_journal` validates that a journal balances and is idempotent, then commits
the journal and its immutable entries atomically. No client endpoint may create
arbitrary entries; callers pass balanced lines and this module enforces the
invariants (V2 §7.2).
"""

from decimal import Decimal

from django.db import transaction

from .models import LedgerAccount, LedgerEntry, LedgerJournal


class LedgerError(Exception):
    """Raised when a journal is unbalanced or references unknown accounts."""


def _d(value) -> Decimal:
    return value if isinstance(value, Decimal) else Decimal(str(value))


@transaction.atomic
def post_journal(reference, lines, *, description="", deal=None, created_by="system", idempotency_key=None):
    """Post a balanced journal.

    lines: iterable of dicts: {"account": "<code>", "debit"|"credit": amount, "memo"?: str}
    Returns the LedgerJournal. If a journal with the same reference or
    idempotency_key already exists, it is returned unchanged (safe replay).
    """
    if idempotency_key:
        existing = LedgerJournal.objects.filter(idempotency_key=idempotency_key).first()
        if existing:
            return existing
    existing_ref = LedgerJournal.objects.filter(reference=reference).first()
    if existing_ref:
        return existing_ref

    total_debit = Decimal("0")
    total_credit = Decimal("0")
    prepared = []
    for line in lines:
        debit = _d(line.get("debit", 0))
        credit = _d(line.get("credit", 0))
        if debit < 0 or credit < 0:
            raise LedgerError("Ledger amounts must be non-negative.")
        if debit > 0 and credit > 0:
            raise LedgerError("A ledger line cannot debit and credit at once.")
        if debit == 0 and credit == 0:
            raise LedgerError("A ledger line must move a non-zero amount.")
        account = LedgerAccount.objects.filter(code=line["account"], is_active=True).first()
        if account is None:
            raise LedgerError(f"Unknown or inactive ledger account: {line['account']}")
        total_debit += debit
        total_credit += credit
        prepared.append((account, debit, credit, line.get("memo", "")))

    if total_debit != total_credit:
        raise LedgerError(f"Journal does not balance: debits {total_debit} != credits {total_credit}.")
    if total_debit == 0:
        raise LedgerError("Journal has no movement.")

    journal = LedgerJournal.objects.create(
        reference=reference,
        description=description,
        deal=deal,
        created_by=created_by or "system",
        idempotency_key=idempotency_key,
    )
    LedgerEntry.objects.bulk_create([
        LedgerEntry(journal=journal, account=account, debit=debit, credit=credit, memo=memo)
        for (account, debit, credit, memo) in prepared
    ])
    return journal


def account_balance(code) -> Decimal:
    """Signed balance for an account: debits - credits (natural for assets/expenses)."""
    from django.db.models import Sum

    account = LedgerAccount.objects.filter(code=code).first()
    if account is None:
        return Decimal("0")
    agg = account.entries.aggregate(d=Sum("debit"), c=Sum("credit"))
    return (agg["d"] or Decimal("0")) - (agg["c"] or Decimal("0"))


def trial_balance():
    """Return per-account (debit_total, credit_total) and the overall imbalance.

    A healthy ledger always has total debits == total credits (imbalance 0).
    """
    from django.db.models import Sum

    rows = []
    total_d = Decimal("0")
    total_c = Decimal("0")
    for acct in LedgerAccount.objects.all():
        agg = acct.entries.aggregate(d=Sum("debit"), c=Sum("credit"))
        d = agg["d"] or Decimal("0")
        c = agg["c"] or Decimal("0")
        total_d += d
        total_c += c
        rows.append({"code": acct.code, "name": acct.name, "type": acct.type, "debit": d, "credit": c})
    return {"rows": rows, "total_debit": total_d, "total_credit": total_c, "imbalance": total_d - total_c}
