/*******************************************************************************
***  This file is part of Dux Rei Nummariae.                                 ***
***                                                                          ***
***  Copyright (C) 2017, 2018, 2019, 2020, 2021                              ***
***  John Schneiderman <Licensing _AT_ Schneiderman _DOT_ me>                ***
***                                                                          ***
***  This program is free software: you can redistribute it and/or modify it ***
***  under the terms of the GNU Affero General Public License as published   ***
***  by the Free Software Foundation, either version 3 of the License, or    ***
***  (at your option) any later version.                                     ***
***                                                                          ***
***  This program is distributed in the hope that it will be useful, but     ***
***  WITHOUT ANY WARRANTY; without even the implied warranty of              ***
***  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    ***
***  See the GNU Affero General Public License for more details.             ***
***                                                                          ***
***  You should have received a copy of the GNU Affero General Public License***
***  along with this program. If not, see <http://www.gnu.org/licenses/>.    ***
*******************************************************************************/
#include "LedgerBooks.h"
using std::map;
using std::set;
using std::size_t;
using ::quint32;
using std::vector;
using drn::accounting::Account;
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
using drn::accounting::AccountTypes;
using drn::accounting::GeneralLedger;
using drn::accounting::LedgerAccount;
using drn::accounting::presentationText;
using drn::accounting::Transaction;
using drn::accounting::TransactionNumber;
using drn::accounting::TransactionStatuses;
using drn::foundation::Optional;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
using pecunia::currency::toStdString;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <pecunia/MoneyTypes.h>
using pecunia::converter;
#include <pecunia/Math.h>
using pecunia::math::absoluteValue;
#include <QDate>
using ::QDate;
#include <QObject>
using ::QObject;
#include <QString>
using ::QString;

#include <algorithm>
using std::any_of;
using std::find_if;
using std::swap;
using std::transform;
#include <cassert>
// using assert
#include <chrono>
using std::chrono::system_clock;
#include <exception>
using std::exception;
using std::out_of_range;
#include <iterator>
using std::inserter;
#include <ostream>
using std::ostream;
using std::string;
#include <utility>
using std::make_pair;
using std::move;

#include "AccountingErrors.h"
using drn::accounting::AccountError;
using drn::accounting::TransactionError;
#include "AccountNumberGenerator.h"
using drn::accounting::AccountNumberGenerator;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/Optional.hpp>
using drn::foundation::Optional;
#include <internal/TransactionPoster.h>
using drn::accounting::internal::EntryTypes;
using drn::accounting::internal::selectTAccount;
using drn::accounting::internal::TransactionPoster;


namespace
{

void validateOpeningAccountCode(
	const Optional<AccountCode>& openingAccountCode,
	const map<AccountCode, LedgerAccount>& ledgers
)
try
{
	if ( ! openingAccountCode.hasValue())
		throw AccountError{{}, QObject::tr("The opening account must be set.")};

	if (ledgers.at(*openingAccountCode).account_.type() != AccountTypes::Capital)
		throw AccountError{
			*openingAccountCode,
			QObject::tr(
				"The opening account type must be '%1'."
			).arg(presentationText(AccountTypes::Capital))
		};

	if (ledgers.count(*openingAccountCode) == 0)
		throw AccountError{
			*openingAccountCode,
			QObject::tr(
				"Cannot use an opening balance account code when the account does not exist."
			)
		};
}
catch (const out_of_range& error)
{
	throw AccountError{
		openingAccountCode.valueOrDefault(),
		QObject::tr("The opening account does not exist in the ledger."),
		error
	};
}

bool equalButDescriptionAndAmount(const Transaction& lhs, const Transaction& rhs)
{
	return lhs.occurredOn() == rhs.occurredOn()
		&& lhs.number() == rhs.number()
		&& lhs.debiting() == rhs.debiting()
		&& lhs.crediting() == rhs.crediting()
		&& lhs.status() == rhs.status();
}

}

//{ LedgerAccount

bool LedgerAccount::operator==(const LedgerAccount& other) const
{
	return this->account_ == other.account_
		&& this->transactions_ == other.transactions_;
}

//}

//{ GeneralLedger

void GeneralLedger::validateTransaction(const Transaction& txn) const
{
	if (txn.number().hasValue())
		throw TransactionError{txn.number(), txn, QObject::tr("Cannot reuse transactions.")};

	if ( ! this->hasLedger(txn.debiting()))
		throw TransactionError{
			txn.number(),
			txn,
			QObject::tr("The debiting account does not exist.")
		};

	if (this->ledgers_.at(txn.debiting()).account_.isClosed())
		throw TransactionError{txn.number(), txn, QObject::tr("The debiting account is closed.")};

	if ( ! this->hasLedger(txn.crediting()))
		throw TransactionError{
			txn.number(),
			txn,
			QObject::tr("The crediting account does not exist.")
		};

	if (this->ledgers_.at(txn.crediting()).account_.isClosed())
		throw TransactionError{txn.number(), txn, QObject::tr("The crediting account is closed.")};

	if (txn.status() != TransactionStatuses::Unreconciled)
		throw TransactionError{
			txn.number(),
			txn,
			QObject::tr("Only an unreconciled transaction may be (re)posted.")
		};

	if (txn.amount().code() == Iso4217Codes::XXX)
		throw TransactionError{
			txn.number(),
			txn,
			QObject::tr("The transaction amount cannot be for an unknown currency.")
		};
}

void GeneralLedger::performTransaction(const Transaction& txn, const TransactionNumber& number)
{
	auto debitCreditTxn{
		TransactionPoster::perform(
			this->ledgers_.at(txn.debiting()).account_,
			this->ledgers_.at(txn.crediting()).account_,
			txn,
			number
		)
	};
	this->accountTransactions_[number] = make_pair(txn.debiting(), txn.crediting());
	this->ledgers_.at(txn.debiting()).transactions_.emplace(
		make_pair(number, move(debitCreditTxn.first))
	);
	this->ledgers_.at(txn.crediting()).transactions_.emplace(
		make_pair(move(number), move(debitCreditTxn.second))
	);
}

void GeneralLedger::validateAddAccount(const Account& account) const
{
	if (account.isClosed())
		throw AccountError{
			account.code(),
			QObject::tr("Cannot add a new account that is already closed.")
		};

	if (account.type() == AccountTypes::Unknown)
		throw AccountError{
			account.code(),
			QObject::tr("The account ledger book must an account type.")
		};

	if (AccountNumber::invalid_ == account.code().number())
		throw AccountError{
			account.code(),
			QObject::tr("The account ledger book must have a valid code.")
		};

	if (this->hasLedger(account.code()))
		throw AccountError{
			account.code(),
			QObject::tr("The account ledger book already has the account.")
		};

	if (account.code().parent().hasValue())
	{
		const auto& parentNumber{account.code().parent()};
		const AccountCode parent{*parentNumber};

		if ( ! this->hasLedger(parent))
			throw AccountError{
				account.code(),
				QObject::tr("The parent of the account does not exist.")
			};

		if (account.type() != this->ledger(parent).account_.type())
			throw AccountError{
				account.code(),
				QObject::tr(
					"The parent of the account does not match the account type of the account "
						"to add."
				)
			};
	}
}

void GeneralLedger::createOpeningAccount(
	AccountNumberGenerator& generator,
	const Iso4217Codes& code
)
try
{
	const auto openingNumber{++generator};
	AccountCode defaultOpeningCode{openingNumber, "Opening Balances", {}};
	Account defaultOpeningBalance{AccountTypes::Capital, defaultOpeningCode, code};
	this->openingAccountCode_ = defaultOpeningCode;
	this->ledgers_.emplace(
		move(defaultOpeningCode), LedgerAccount{move(defaultOpeningBalance), {}}
	);
}
catch (const exception&)
{
	--generator;
	throw;
}

GeneralLedger::GeneralLedger() :
	GeneralLedger{{}, {}}
{}

GeneralLedger::GeneralLedger(
	map<AccountCode, LedgerAccount> ledgers,
	Optional<AccountCode> openingAccountCode
) :
	ledgers_{move(ledgers)},
	accountTransactions_{},
	nextNumber_{},
	openingAccountCode_{move(openingAccountCode)}
{
	TransactionNumber lastUsed{};

	if (this->openingAccountCode_.hasValue())
		validateOpeningAccountCode(this->openingAccountCode_, this->ledgers_);

	for (const auto& accountCodeLedgerPair : this->ledgers_)
	{
		const auto& examineAccountCode{accountCodeLedgerPair.first};
		const auto& examineLedger{accountCodeLedgerPair.second};

		if (examineAccountCode != examineLedger.account_.code())
			throw AccountError{
				examineAccountCode,
				QObject::tr(
					"The mapped account code does not match the ledger's account code '%1'."
				).arg(presentationText(examineLedger.account_.code()))
			};

		if (examineAccountCode.parent().hasValue())
		{
			const auto& examineParent{*examineAccountCode.parent()};

			if ( ! this->hasLedger(examineParent))
				throw AccountError{
					examineAccountCode,
					QObject::tr(
						"The parent account number, %1, does not exist."
					).arg(presentationText(examineParent))
				};

			for (const auto& checkAccountLedgerPair : this->ledgers_)
			{
				const auto& checkAccountCode{checkAccountLedgerPair.first};

				if ( ! checkAccountCode.parent().hasValue())
					continue;
				const auto& childParent{*checkAccountCode.parent()};

				if (
					examineAccountCode.number() == childParent
					&& examineParent == checkAccountCode.number()
				)
					throw AccountError{
						examineAccountCode,
						QObject::tr(
							"The account number, %1, cannot be a a parent as it's a child of the "
								"parent account."
						).arg(presentationText(checkAccountCode))
					};
			}
		}

		for (const auto& numberTransactionPair : examineLedger.transactions_)
		{
			const auto& txnNum{numberTransactionPair.first};
			const auto& txn{numberTransactionPair.second};

			if (this->accountTransactions_.count(txnNum) == 1)
			{
				const auto& debitedCreditedAccount{this->accountTransactions_.at(txnNum)};
				const auto& otherAccountCode{
					examineAccountCode == debitedCreditedAccount.first
						? debitedCreditedAccount.second
						: debitedCreditedAccount.first
				};
				const auto& otherTransaction{
					this->ledgers_.at(otherAccountCode).transactions_.at(txnNum)
				};

				// N.B. not checking the description and amounts as that is specific to each
				// account ledger, e.g. may have a converted balance.
				if ( ! equalButDescriptionAndAmount(txn,  otherTransaction))
					throw TransactionError{
						txn.number(),
						txn,
						QObject::tr(
							"The supplied ledger account, %1, contains a mismatched "
								"transaction, %2."
						)
							.arg(presentationText(examineAccountCode))
							.arg(presentationText(otherTransaction))
					};
				continue;
			}

			if (txnNum.integer() == TransactionNumber::invalid_)
				throw TransactionError{
					{},
					txn,
					QObject::tr(
						"The supplied ledger account, %1, contains an invalid transaction number."
					).arg(presentationText(examineAccountCode))
				};

			if (txnNum != txn.number())
				throw TransactionError{
					txn.number(),
					txn,
					QObject::tr(
						"The supplied ledger account, %1, contains a transaction number that is "
							"mapped to a different number, %2."
					)
						.arg(presentationText(examineAccountCode))
						.arg(presentationText(txnNum))
				};

			if ( ! this->hasLedger(txn.debiting()))
				throw TransactionError{
					txn.number(),
					txn,
					QObject::tr(
						"The supplied ledger account, %1, contains a transaction for a debiting "
							"account that does not exist."
					).arg(presentationText(examineAccountCode))
				};

			if ( ! this->hasLedger(txn.crediting()))
				throw TransactionError{
					txn.number(),
					txn,
					QObject::tr(
						"The supplied ledger account, %1, contains a transaction for a crediting "
							"account that does not exist."
					).arg(presentationText(examineAccountCode))
				};

			if (this->ledgers_.at(txn.debiting()).transactions_.count(txnNum) != 1)
				throw AccountError{
					txn.debiting(),
					QObject::tr(
						"The debiting account does not contain the transaction number '%1'."
					).arg(presentationText(txnNum))
				};

			if (this->ledgers_.at(txn.crediting()).transactions_.count(txnNum) != 1)
				throw AccountError{
					txn.crediting(),
					QObject::tr(
						"The crediting account does not contain the transaction number '%1'."
					).arg(presentationText(txnNum))
				};

			this->accountTransactions_[txnNum] = make_pair(txn.debiting(), txn.crediting());

			if (txnNum > lastUsed)
				lastUsed = txnNum;
		}
	}
	this->nextNumber_ = ++lastUsed;
}

bool GeneralLedger::operator==(const GeneralLedger& other) const
{
	return this->openingAccountCode_ == other.openingAccountCode_
		&& this->ledgers_ == other.ledgers_;
}

bool GeneralLedger::operator!=(const GeneralLedger& other) const
{
	return ! (*this == other);
}

const map<AccountCode, LedgerAccount>& GeneralLedger::ledgers() const
{
	return this->ledgers_;
}

bool GeneralLedger::hasLedger(const AccountCode& code) const
{
	return this->ledgers_.count(code) > 0;
}

bool GeneralLedger::hasLedger(const AccountNumber& number) const
{
	return this->ledgers_.count(AccountCode{number}) > 0;
}


const LedgerAccount& GeneralLedger::ledger(const AccountCode& code) const
{
	if ( ! this->hasLedger(code))
		throw AccountError{code, QObject::tr("The account ledger book does not exist.")};
	return this->ledgers_.at(code);
}

const LedgerAccount& GeneralLedger::ledger(const AccountNumber& number) const
{
	return this->ledger(AccountCode{number});
}

TransactionNumber GeneralLedger::post(const Transaction& txn)
{
	this->validateTransaction(txn);
	this->performTransaction(txn, this->nextNumber_);
	return this->nextNumber_++;
}

void GeneralLedger::repost(const TransactionNumber& number, const Transaction& txn)
{
	if (this->accountTransactions_.count(number) == 0)
		throw TransactionError{
			number,
			txn,
			QObject::tr("The original transaction number, %1 does not exist.")
				.arg(presentationText(number))
		};
	this->validateTransaction(txn);
	const auto originalDebitCreditAccounts{this->accountTransactions_.at(number)};
	auto original{
		this->ledgers_.at(originalDebitCreditAccounts.first).transactions_.at(number)
	};
	assert(
		number == original.number()
		&& "The original transaction number supplied should match the transaction number in the "
			"original debit transaction object."
	);
	assert(
		original == this->ledgers_.at(originalDebitCreditAccounts.second).transactions_.at(number)
		&& "The original transaction number supplied should match the transaction in the "
			"original credit transaction object."
	);

	if (original.status() == TransactionStatuses::Reconciled)
		throw TransactionError{
			number,
			original,
			QObject::tr("Cannot change a transaction that is already reconciled.")
		};
	this->remove(number);

	try
	{
		this->performTransaction(txn, number);
	}
	catch (const exception&)
	{
		// When performing the new transaction fails we need to undo the earlier reverse
		// transaction. This should never generate an exception since we're using the original
		// transaction that was already in the ledger.
		this->performTransaction(original, number);
		throw;
	}
}

Optional<TransactionNumber> GeneralLedger::add(
	const Account& toAdd,
	AccountNumberGenerator& generator
)
{
	this->validateAddAccount(toAdd);
	auto code{toAdd.code()};
	Account noBalance{toAdd.type(), code, toAdd.balance().code()};
	this->ledgers_.emplace(move(code), LedgerAccount{move(noBalance), {}});
	generator.update(toAdd.code().number());

	if (toAdd.balance() == 0)
		return {};

	if ( ! this->openingAccountCode_.hasValue())
		this->createOpeningAccount(generator, toAdd.balance().code());
	const auto tAccount{selectTAccount(toAdd.type(), toAdd.balance() > 0)};
	const Transaction opening{
		QDate::currentDate(),
		tAccount == EntryTypes::Debit ? toAdd.code() : *this->openingAccountCode_,
		tAccount == EntryTypes::Credit ? toAdd.code() : *this->openingAccountCode_,
		absoluteValue(toAdd.balance()),
		QObject::tr("Starting Balance.")
	};
	return this->post(opening);
}

void GeneralLedger::remove(const AccountCode& code)
{
	// N.B. the logic should match GeneralLedger::canRemove.
	static constexpr size_t sizeCheckForRemoval{1};

	if ( ! this->hasLedger(code))
		throw AccountError{
			code,
			QObject::tr("Cannot remove an account that is not in the general ledger book.")
		};
	auto& accountLedger{this->ledgers_.at(code)};

	if (accountLedger.account_.isClosed())
		throw AccountError{code, QObject::tr("Cannot remove an account that is already closed.")};

	if (this->openingAccountCode_ == code && ! accountLedger.transactions_.empty())
		throw AccountError{
			code,
			QObject::tr(
				"Cannot remove the opening account when it has transactions in the general "
					"ledger book."
			)
		};

	if (
		 ! this->ledgers_.at(code).account_.code().parent().hasValue()
		&& any_of(
			this->ledgers_.cbegin(),
			this->ledgers_.cend(),
			[&code] (const auto& codeLedger)
			{
				return codeLedger.second.account_.code().parent() == code.number();
			}
		)
	)
		throw AccountError{
			code,
			QObject::tr("Cannot remove an account that is the parent account of other accounts.")
		};

	// N.B. This should be the last error check.
	if (accountLedger.transactions_.size() >= sizeCheckForRemoval)
	{
		const auto& firstTransaction{accountLedger.transactions_.begin()->second};

		if (
			accountLedger.transactions_.size() > sizeCheckForRemoval
			|| (
				firstTransaction.crediting() != this->openingAccountCode_
				&& firstTransaction.debiting() != this->openingAccountCode_
			)
		)
			throw AccountError{
				code,
				QObject::tr("Cannot remove an account that has transactions.")
			};
		// It's okay when this has a transaction removal error since that indicates that this was
		// an account in use, and should therefore be closed and not removed, even when the only
		// transaction is the opening balance it was reconciled.
		this->remove(*firstTransaction.number());
	}
	this->ledgers_.erase(code);

	if (this->openingAccountCode_ == code)
		this->openingAccountCode_.reset();
}

void GeneralLedger::close(const AccountCode& code)
{
	if ( ! this->hasLedger(code))
		throw AccountError{
			code,
			QObject::tr("Cannot close an account that is not in the general ledger book.")
		};

	if (
		 ! this->ledgers_.at(code).account_.code().parent().hasValue()
		&& any_of(
			this->ledgers_.cbegin(),
			this->ledgers_.cend(),
			[&code] (const auto& codeLedger)
			{
				return codeLedger.second.account_.code().parent() == code.number()
						&& ! codeLedger.second.account_.isClosed();
			}
		)
	)
		throw AccountError{
			code,
			QObject::tr(
				"Cannot close an account that is the parent account of other non-closed accounts."
			)
		};
	this->ledgers_.at(code).account_.close();

	if (this->openingAccountCode_ == code)
		this->openingAccountCode_.reset();
}

void GeneralLedger::open(const AccountCode& code)
{
	if ( ! this->hasLedger(code))
		throw AccountError{
			code,
			QObject::tr("Cannot open an account that is not in the general ledger book.")
		};

	if (
		this->ledgers_.at(code).account_.code().parent().hasValue()
		&& this->ledgers_.at(
			AccountCode{AccountNumber{*this->ledgers_.at(code).account_.code().parent()}}
		).account_.isClosed()
	)
		throw AccountError{
			code,
			QObject::tr("Cannot open an account whose parent account is closed.")
		};
	this->ledgers_.at(code).account_.open();
}

void GeneralLedger::recode(const AccountCode& toRecode, const AccountCode& recodeAs)
{
	if ( ! this->hasLedger(toRecode))
		throw AccountError{
			toRecode,
			QObject::tr("Cannot recode an account that is not in the general ledger book.")
		};

	if (this->hasLedger(recodeAs) && toRecode != recodeAs)
		throw AccountError{
			toRecode,
			"Cannot recode the account in the general ledger book under account "
				"code " + presentationText(recodeAs) + " as that code already exists."
		};

	if (recodeAs.parent().hasValue())
	{
		const auto& recodeParent{*recodeAs.parent()};

		if ( ! this->hasLedger(recodeParent))
			throw AccountError{
				toRecode,
				QObject::tr(
					"Cannot recode an account with a parent, %1, that does not exist."
				).arg(presentationText(recodeParent))
			};

		for (const auto& checkAccountLedgerPair : this->ledgers_)
		{
			const auto& checkAccountCode{checkAccountLedgerPair.first};

			if ( ! checkAccountCode.parent().hasValue())
				continue;
			const auto& checkParent{*checkAccountCode.parent()};

			if (recodeAs.number() == checkParent && recodeParent == checkAccountCode.number())
				throw AccountError{
					toRecode,
					QObject::tr(
						"Cannot recode the account as the new parent account number, %1, is a "
							"child of that account."
					).arg(presentationText(checkAccountCode))
				};
		}
	}
	auto currentLedger{this->ledgers_.find(toRecode)};

	if (currentLedger->second.account_.isClosed())
		throw AccountError{
			toRecode,
			QObject::tr("Cannot recode an account that is already closed.")
		};
	Account toAdd{
		currentLedger->second.account_.type(),
		recodeAs,
		currentLedger->second.account_.balance()
	};

	if (toRecode != recodeAs)
	{
		this->validateAddAccount(toAdd);
		auto code{toAdd.code()};
		this->ledgers_.emplace(make_pair(move(code), LedgerAccount{move(toAdd), {}}));
		swap(this->ledgers_.at(recodeAs), currentLedger->second);
		this->ledgers_.erase(currentLedger);
	}
	this->ledgers_.at(recodeAs).account_.setCode(recodeAs);

	for (auto& codeLedger : this->ledgers_)
		if (codeLedger.second.account_.code().parent() == toRecode.number())
			codeLedger.second.account_.setParent(recodeAs.number());

	for (auto& txnNumDebitCreditCode : this->accountTransactions_)
	{
		const auto& txnNumber{txnNumDebitCreditCode.first};
		auto& debitAccountCode{txnNumDebitCreditCode.second.first};
		auto& creditAccountCode{txnNumDebitCreditCode.second.second};

		if (toRecode == debitAccountCode)
		{
			TransactionPoster::recodeAccount(
				toRecode,
				this->ledgers_.at(recodeAs).transactions_.at(txnNumber),
				recodeAs
			);
			TransactionPoster::recodeAccount(
				toRecode,
				this->ledgers_.at(creditAccountCode).transactions_.at(txnNumber),
				recodeAs
			);
			debitAccountCode = recodeAs;
		}

		if (toRecode == creditAccountCode)
		{
			TransactionPoster::recodeAccount(
				toRecode,
				this->ledgers_.at(recodeAs).transactions_.at(txnNumber),
				recodeAs
			);
			TransactionPoster::recodeAccount(
				toRecode,
				this->ledgers_.at(debitAccountCode).transactions_.at(txnNumber),
				recodeAs
			);
			creditAccountCode = recodeAs;
		}
	}

	if (toRecode == this->openingAccountCode_)
		this->openingAccountCode_ = recodeAs;
}

void GeneralLedger::clear(const TransactionNumber& number)
{
	this->clear(vector<TransactionNumber>{number});
}

void GeneralLedger::clear(const vector<TransactionNumber>& transactionNumbers)
{
	for (const auto& transactionNumber : transactionNumbers)
	{
		if (this->accountTransactions_.count(transactionNumber) == 0)
			throw Error{
				QObject::tr(
					"The transaction number, %1, does not exist and cannot be cleared."
				).arg(presentationText(transactionNumber))
			};
		const auto debitedCreditedAccount{this->accountTransactions_.at(transactionNumber)};
		auto& debitTransaction{
			this->ledgers_.at(debitedCreditedAccount.first).transactions_.at(transactionNumber)
		};
		assert(
			transactionNumber == debitTransaction.number()
			&& "The transaction number supplied should match the transaction number in the "
				"debit transaction object."
		);
		debitTransaction.markCleared();
		auto& creditTransaction{
			this->ledgers_.at(debitedCreditedAccount.second).transactions_.at(transactionNumber)
		};
		assert(
			transactionNumber == creditTransaction.number()
			&& "The transaction number supplied should match the transaction number in the "
				"credit transaction object."
		);
		creditTransaction.markCleared();
	}
}

void GeneralLedger::reconcile(const TransactionNumber& number)
{
	this->reconcile(vector<TransactionNumber>{number});
}

void GeneralLedger::reconcile(const vector<TransactionNumber>& transactionNumbers)
{
	for (const auto& transactionNumber : transactionNumbers)
	{
		if (this->accountTransactions_.count(transactionNumber) == 0)
			throw Error{
				QObject::tr(
					"The transaction number, %1, does not exist and cannot be reconciled."
				).arg(presentationText(transactionNumber))
			};
		const auto debitedCreditedAccount{this->accountTransactions_.at(transactionNumber)};
		auto& debitTransaction{
			this->ledgers_.at(debitedCreditedAccount.first).transactions_.at(transactionNumber)
		};
		assert(
			transactionNumber == debitTransaction.number()
			&& "The transaction number supplied should match the transaction number in the "
				"debit transaction object."
		);
		debitTransaction.markReconciled();
		auto& creditTransaction{
			this->ledgers_.at(debitedCreditedAccount.second).transactions_.at(transactionNumber)
		};
		assert(
			transactionNumber == creditTransaction.number()
			&& "The transaction number supplied should match the transaction number in the "
				"credit transaction object."
		);
		creditTransaction.markReconciled();
	}
}

void GeneralLedger::remove(const TransactionNumber& number)
{
	this->remove(vector<TransactionNumber>{number});
}

void GeneralLedger::remove(const vector<TransactionNumber>& transactionNumbers)
{
	for (const auto& transactionNumber : transactionNumbers)
	{
		if (this->accountTransactions_.count(transactionNumber) == 0)
			throw Error{
				QObject::tr(
					"The transaction number, %1, does not exist and cannot be removed."
				).arg(presentationText(transactionNumber))
			};
		const auto& debitedCreditedAccount{this->accountTransactions_.at(transactionNumber)};
		auto& debitTransaction{
			this->ledgers_.at(debitedCreditedAccount.first).transactions_.at(transactionNumber)
		};
		assert(
			transactionNumber == debitTransaction.number()
			&& "The transaction number supplied should match the transaction number in the "
				"debit transaction object."
		);
		auto& creditTransaction{
			this->ledgers_.at(debitedCreditedAccount.second).transactions_.at(transactionNumber)
		};
		assert(
			transactionNumber == creditTransaction.number()
			&& "The transaction number supplied should match the transaction number in the "
				"credit transaction object."
		);

		if (debitTransaction.status() == TransactionStatuses::Reconciled)
			throw TransactionError{
				transactionNumber,
				debitTransaction,
				QObject::tr("Cannot remove a transaction that is reconciled.")
			};
		assert(
			creditTransaction.status() != TransactionStatuses::Reconciled
			&& "The credit transaction object should not be reconciled if the debit transaction "
				"object is not."
		);
		TransactionPoster::reverse(
			this->ledgers_.at(debitTransaction.debiting()).account_,
			this->ledgers_.at(creditTransaction.crediting()).account_,
			creditTransaction
		);
		#pragma GCC diagnostic push
		#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
		// Justification: This is a sanity check.
		size_t removed;
		removed =
			this->ledgers_.at(debitTransaction.debiting()).transactions_.erase(transactionNumber);
		assert(removed == 1 && "Should have had the debit account transaction.");
		removed =
			this->ledgers_.at(creditTransaction.crediting()).transactions_.erase(transactionNumber);
		assert(removed == 1 && "Should have had the crediting account transaction.");
		removed = this->accountTransactions_.erase(transactionNumber);
		assert(removed == 1 && "Should have had the account transaction map entry.");
		#pragma GCC diagnostic pop
	}
}

set<Account> GeneralLedger::accounts() const
{
	set<Account> ledgerAccouts;
	transform(
		this->ledgers_.cbegin(),
		this->ledgers_.cend(),
		inserter(ledgerAccouts, ledgerAccouts.begin()),
		[] (const auto& ledger)
		{
			return ledger.second.account_;
		}
	);
	return ledgerAccouts;
}

set<AccountCode> GeneralLedger::accountCodes() const
{
	set<AccountCode> accountCodes;

	for (const auto& ledger : this->ledgers_)
		accountCodes.emplace(ledger.second.account_.code());
	return accountCodes;
}

set<AccountCode> GeneralLedger::accountCodes(const AccountTypes& type) const
{
	set<AccountCode> accountCodes;

	for (const auto& ledger : this->ledgers_)
		if (type == ledger.second.account_.type())
			accountCodes.emplace(ledger.second.account_.code());
	return accountCodes;
}

const Optional<AccountCode>& GeneralLedger::openingAccountCode() const
{
	return this->openingAccountCode_;
}

bool GeneralLedger::isEmpty() const
{
	return this->ledgers_.empty();
}

void GeneralLedger::convertTo(
	const AccountCode& convertAccount,
	const Iso4217Codes& code,
	AccountNumberGenerator& generator
)
{
	if ( ! this->hasLedger(convertAccount))
		throw AccountError{
			convertAccount,
			QObject::tr("Cannot convert the balance currency when the account does not exist.")
		};

	if (this->ledger(convertAccount).account_.balance().code() == code)
		throw AccountError{
			convertAccount,
			QObject::tr("Cannot convert the balance currency that is already in the same currency.")
		};

	if ( ! this->openingAccountCode_.hasValue())
		this->createOpeningAccount(generator, code);
	auto& account{this->ledgers_.at(convertAccount).account_};
	const Transaction conversion{
		QDate::currentDate(),
		convertAccount,
		*this->openingAccountCode_,
		Money{code},
		QObject::tr("Account balance currency change.")
	};
	const auto txn{this->post(conversion)};

	try
	{
		account.convertTo(code);
	}
	catch (const exception&)
	{
		this->remove(txn);
		throw;
	}
}

bool GeneralLedger::canRemove(const AccountCode& code) const
{
	// N.B. the logic should match GeneralLedger::remove.
	static constexpr size_t sizeCheckForRemoval{1};

	if ( ! this->hasLedger(code))
		return false;
	auto& accountLedger{this->ledgers_.at(code)};

	if (accountLedger.account_.isClosed())
		return false;

	if (this->openingAccountCode_ == code && ! accountLedger.transactions_.empty())
		return false;

	if (
		 ! this->ledgers_.at(code).account_.code().parent().hasValue()
		&& any_of(
			this->ledgers_.cbegin(),
			this->ledgers_.cend(),
			[&code] (const auto& codeLedger)
			{
				return codeLedger.second.account_.code().parent() == code.number();
			}
		)
	)
		return false;

	// N.B. This must be the last error check.
	if (accountLedger.transactions_.size() >= sizeCheckForRemoval)
	{
		const auto& firstTransaction{accountLedger.transactions_.begin()->second};

		if (
			accountLedger.transactions_.size() > sizeCheckForRemoval
			|| (
				firstTransaction.crediting() != this->openingAccountCode_
				&& firstTransaction.debiting() != this->openingAccountCode_
			)
		)
			return false;
	}
	return true;
}

void GeneralLedger::setOpeningAccountCode(foundation::Optional<AccountCode> code)
{
	if (code.hasValue())
		validateOpeningAccountCode(code, this->ledgers_);
	this->openingAccountCode_ = move(code);
}

set<AccountCode> GeneralLedger::accountCodes(const set<AccountNumber>& accountNumbers) const
{
	set<AccountCode> codes;

	for (const auto& accountNumber : accountNumbers)
		codes.emplace(this->ledger(AccountCode{accountNumber}).account_.code());
	return codes;
}

//}

ostream& drn::accounting::operator<<(ostream& o, const LedgerAccount& la)
{
	o << "ACT (" << la.account_ << ") TXN (";

	bool isFirst{true};
	for (const auto& numTxn : la.transactions_)
		o << (isFirst ? isFirst = false, "" : ", ") << numTxn.first << "=>" << numTxn.second << ')';
	return o;
}

ostream& drn::accounting::operator<<(ostream& o, const GeneralLedger& gl)
{
	o << "Next TXN Number (" << gl.nextNumber_ << "), Opening Account Code ("
			<< gl.openingAccountCode_ << "), Ledgers (" ;

	bool isFirst{true};
	for (const auto& codeLedger : gl.ledgers_)
		o << (isFirst ? isFirst = false, "" : ", ") << codeLedger.first << "=>"
				<< codeLedger.second << ')';
	return o;
}

set<Iso4217Codes> drn::accounting::containedCurrencies(const GeneralLedger& gl)
{
	set<Iso4217Codes> codes;

	for (const auto& ledger : gl.ledgers())
		codes.emplace(ledger.second.account_.balance().code());
	return codes;
}
