"""
Ledger service layer — the only sanctioned way to move money in the books.

Invariants enforced here:
  * Every transaction balances to zero (sum of its entries == 0).
  * Posting is idempotent when an `idempotency_key` is supplied — a retry
    returns the existing transaction instead of double-posting.
  * Balances are always derived from entries, never stored mutable numbers.
"""
from __future__ import annotations

from decimal import Decimal
from typing import Iterable

from django.db import transaction as db_transaction
from django.db.models import Sum

from .models import Account, Entry, Transaction

ZERO = Decimal('0.00')
_Q = Decimal('0.01')


def get_system_account(kind: str) -> Account:
    # Deterministic + duplicate-safe: MySQL can't enforce the partial unique
    # constraint, so never rely on get_or_create (which would raise on dupes).
    # Always return the canonical (lowest-id) system account, seeded at deploy.
    account = (Account.objects
               .filter(kind=kind, user__isnull=True)
               .order_by('id').first())
    if account is None:
        account = Account.objects.create(
            kind=kind, user=None,
            name=dict(Account.Kind.choices).get(kind, kind))
    return account


def get_user_account(user, kind: str = Account.Kind.USER_WALLET) -> Account:
    account, _ = Account.objects.get_or_create(
        user=user, kind=kind,
        defaults={'name': f'{kind} for {getattr(user, "phone", user.pk)}'},
    )
    return account


def account_balance(account: Account) -> Decimal:
    return account.entries.aggregate(s=Sum('amount'))['s'] or ZERO


def user_wallet_balance(user) -> Decimal:
    """Available balance a seller can withdraw (wallet credits minus reserves)."""
    return account_balance(get_user_account(user))


@db_transaction.atomic
def post_transaction(
    *,
    kind: str,
    lines: Iterable[tuple[Account, Decimal]],
    idempotency_key: str | None = None,
    reference_type: str = '',
    reference_id: int | None = None,
    memo: str = '',
    created_by=None,
) -> Transaction:
    """
    Atomically record a balanced transaction.

    `lines` is an iterable of (account, signed_amount). Positive credits the
    account; negative debits it. The amounts must sum to zero.
    """
    lines = [(acc, Decimal(amt).quantize(_Q)) for acc, amt in lines]

    total = sum((amt for _, amt in lines), ZERO)
    if total != ZERO:
        raise ValueError(f'Unbalanced transaction: entries sum to {total}, expected 0')
    if len(lines) < 2:
        raise ValueError('A transaction needs at least two entries')

    if idempotency_key:
        existing = Transaction.objects.filter(idempotency_key=idempotency_key).first()
        if existing:
            return existing

    txn = Transaction.objects.create(
        kind=kind,
        idempotency_key=idempotency_key,
        reference_type=reference_type,
        reference_id=reference_id,
        memo=memo,
        created_by=created_by,
    )
    Entry.objects.bulk_create(
        [Entry(transaction=txn, account=acc, amount=amt) for acc, amt in lines]
    )
    return txn


# ── Domain postings ──────────────────────────────────────────────────────────
def post_commission(*, user, amount: Decimal, order_id: int,
                    kind: str = Transaction.Kind.COMMISSION,
                    memo: str = '') -> Transaction | None:
    """Credit a seller's wallet with a sales commission (idempotent per order)."""
    amount = Decimal(amount).quantize(_Q)
    if amount <= ZERO:
        return None
    wallet = get_user_account(user)
    expense = get_system_account(Account.Kind.COMMISSION_EXPENSE)
    return post_transaction(
        kind=kind,
        lines=[(expense, -amount), (wallet, amount)],
        idempotency_key=f'commission:order:{order_id}',
        reference_type='order',
        reference_id=order_id,
        memo=memo or f'Commission for order #{order_id}',
    )


def reverse_commission(*, user, amount: Decimal, order_id: int,
                       memo: str = '') -> Transaction | None:
    """Reverse a previously posted commission (e.g. order cancelled/refunded)."""
    amount = Decimal(amount).quantize(_Q)
    if amount <= ZERO:
        return None
    wallet = get_user_account(user)
    expense = get_system_account(Account.Kind.COMMISSION_EXPENSE)
    return post_transaction(
        kind=Transaction.Kind.COMMISSION_REVERSAL,
        lines=[(wallet, -amount), (expense, amount)],
        idempotency_key=f'commission_reversal:order:{order_id}',
        reference_type='order',
        reference_id=order_id,
        memo=memo or f'Commission reversal for order #{order_id}',
    )


def wallet_breakdown(user) -> dict[str, Decimal]:
    """
    Net (signed) sum of all wallet entries grouped by transaction kind. Used to
    build the earnings breakdown — reversals net against their originals.
    """
    wallet = get_user_account(user)
    rows = (Entry.objects
            .filter(account=wallet)
            .values('transaction__kind')
            .annotate(total=Sum('amount')))
    return {r['transaction__kind']: r['total'] or ZERO for r in rows}
