package com.leisingha.ledger.transaction;

import com.leisingha.ledger.account.Account;
import com.leisingha.ledger.common.CurrencyMismatchException;
import com.leisingha.ledger.common.InvalidEntryException;
import com.leisingha.ledger.common.UnbalancedTransactionException;

import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * The business rules a transaction must satisfy before anything is written.
 *
 * <p>Deliberately free of Spring and of the database: everything it needs is
 * passed in, so the rules can be unit-tested directly without a context or a
 * container. The ordering of the checks is part of the contract — a request that
 * breaks several rules at once gets the error for the first one, which keeps
 * responses predictable.
 */
final class TransactionValidator {

    private static final int MINIMUM_ENTRIES = 2;

    private TransactionValidator() {
    }

    /**
     * The rules that need nothing but the request itself.
     *
     * <p>Runs before the accounts are fetched, for two reasons. The spec fixes the
     * order — entry count and amounts are checked ahead of whether an account
     * exists — so a one-sided posting naming a bad account id must answer 422 for
     * the shape, not 404 for the id. It is also simply cheaper to reject a
     * malformed request without touching the database at all.
     */
    static void validateShape(List<CreateEntryRequest> entries) {
        requireAtLeastTwoEntries(entries);
        requirePositiveAmounts(entries);
    }

    /**
     * The rules that need the referenced accounts loaded.
     *
     * @param entries  the requested entries, in submission order
     * @param accounts the accounts those entries reference, keyed by id
     */
    static void validateAgainstAccounts(List<CreateEntryRequest> entries, Map<UUID, Account> accounts) {
        requireSingleCurrency(entries, accounts);
        requireBalanced(entries);
    }

    /** A single-sided posting is not a double entry. */
    private static void requireAtLeastTwoEntries(List<CreateEntryRequest> entries) {
        if (entries.size() < MINIMUM_ENTRIES) {
            throw new InvalidEntryException(
                "Transaction needs at least " + MINIMUM_ENTRIES + " entries, got " + entries.size());
        }
    }

    /** Amounts carry magnitude only; {@link Direction} carries the sign. */
    private static void requirePositiveAmounts(List<CreateEntryRequest> entries) {
        for (int i = 0; i < entries.size(); i++) {
            long amount = entries.get(i).amountMinor();
            if (amount <= 0) {
                throw new InvalidEntryException(
                    "Entry " + i + " has amountMinor " + amount + "; amounts must be greater than 0");
            }
        }
    }

    /**
     * This service stores currency but never converts it, so a transaction that
     * straddles two currencies has no meaningful total and is refused.
     */
    private static void requireSingleCurrency(List<CreateEntryRequest> entries, Map<UUID, Account> accounts) {
        Account first = accounts.get(entries.get(0).accountId());
        String expected = first.getCurrency();

        for (CreateEntryRequest entry : entries) {
            Account account = accounts.get(entry.accountId());
            if (!expected.equals(account.getCurrency())) {
                throw new CurrencyMismatchException(
                    "Entries span currencies: account " + first.getId() + " is " + expected
                        + " but account " + account.getId() + " is " + account.getCurrency());
            }
        }
    }

    /** The invariant the whole service exists to protect. */
    private static void requireBalanced(List<CreateEntryRequest> entries) {
        long debits = sumOf(entries, Direction.DEBIT);
        long credits = sumOf(entries, Direction.CREDIT);
        if (debits != credits) {
            throw new UnbalancedTransactionException(debits, credits);
        }
    }

    private static long sumOf(List<CreateEntryRequest> entries, Direction side) {
        return entries.stream()
            .filter(entry -> entry.direction() == side)
            .mapToLong(CreateEntryRequest::amountMinor)
            .sum();
    }
}
