/*******************************************************************************
***  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 "BankLedgers.h"
using ::QString;
using std::array;
using std::map;
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
using drn::accounting::presentationText;
using drn::banking::Bank;
using drn::banking::BankAccountType;
using drn::banking::SupportedAccountTypes;
using drn::banking::BankName;
using drn::banking::numberOfSupportedAccountTypes;
using drn::foundation::ObserverPtr;
using drn::navigation::internal::BankLedgers;

#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <QObject>
using ::QObject;

#include <algorithm>
using std::find_if;
#include <cassert>
// using assert
#include <exception>
using std::exception;
#include <ostream>
using std::ostream;
#include <set>
using std::set;
#include <utility>
using std::move;

#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::AccountTypes;
#include <accounting/AccountingErrors.h>
using drn::accounting::AccountError;
#include <accounting/LedgerBooks.h>
using drn::accounting::GeneralLedger;
#include <accounting/AccountNumberGenerator.h>
using drn::accounting::AccountNumberGenerator;
#include <banking/BankMap.h>
using drn::banking::BankMap;
#include <banking/BankingErrors.h>
using drn::banking::BankError;
#include <foundation/EnumMacros.hpp>
// using *_ENUMERATION_*
#include <foundation/Optional.hpp>
using drn::foundation::Optional;


BankLedgers::BankLedgers(
	ObserverPtr<BankMap> banks,
	ObserverPtr<accounting::GeneralLedger> ledgers,
	map<BankName, map<AccountNumber, SupportedAccountTypes>> bankAccountTypes
) :
	banks_{move(banks)},
	ledgers_{move(ledgers)},
	bankAccountTypes_{move(bankAccountTypes)}
{
	assert(banks != nullptr && "The banks cannot be null.");
	assert(ledgers != nullptr && "The general ledger cannot be null");

	for (const auto& bankNameAccountNumberType : this->bankAccountTypes_)
	{
		if ( ! this->banks_->hasBank(bankNameAccountNumberType.first))
			throw BankError{
				bankNameAccountNumberType.first,
				QObject::tr("The bank name is not present in the list of known banks.")
			};

		if (bankNameAccountNumberType.second.empty())
			throw BankError{
				bankNameAccountNumberType.first,
				QObject::tr("The bank account type must have an association.")
			};

		for (const auto& accountNumberType : bankNameAccountNumberType.second)
		{
			if (AccountNumber::invalid_ == accountNumberType.first)
				throw BankError{
					bankNameAccountNumberType.first,
					QObject::tr("The account ledger number must be set.")
				};

			if ( ! this->ledgers_->hasLedger(accountNumberType.first))
				throw BankError{
					bankNameAccountNumberType.first,
					QObject::tr("The account ledger number is not present in the general ledger.")
				};

			if (accountNumberType.second == SupportedAccountTypes::Unknown)
				throw BankError{
					bankNameAccountNumberType.first,
					QObject::tr("The bank account type cannot be unknown.")
				};
		}

		for (const auto& bankNameAccountNumberTypes : this->bankAccountTypes_)
			for (const auto& otherBankNameAccountNumberTypes :  this->bankAccountTypes_)
			{
				if (bankNameAccountNumberTypes.first == otherBankNameAccountNumberTypes.first)
					continue;

				for (const auto& accountNumberType : bankNameAccountNumberTypes.second)
					if (otherBankNameAccountNumberTypes.second.count(accountNumberType.first) != 0)
						throw BankError{
							bankNameAccountNumberTypes.first,
							QObject::tr(
									"The bank account '%1' has its type associated with the bank "
										"'%2' and cannot be used again."
							)
								.arg(presentationText(accountNumberType.first))
								.arg(otherBankNameAccountNumberTypes.first.institution())
						};
			}
	}
}

const BankMap& BankLedgers::banks() const
{
	return *this->banks_;
}

const GeneralLedger& BankLedgers::ledgers() const
{
	return *this->ledgers_;
}

const map<BankName, map<AccountNumber, SupportedAccountTypes>>& BankLedgers::bankAccountTypes(
) const
{
	return this->bankAccountTypes_;
}

Optional<BankName> BankLedgers::findBankName(const AccountNumber& number)
{
	const auto bankWithAccount{
		find_if(
			this->banks_->cbegin(),
			this->banks_->cend(),
			[&number] (const auto& nameBank)
			{
				return nameBank.second.accounts().count(number) == 1;
			}
		)
	};
	return bankWithAccount == this->banks_->cend()
		? Optional<BankName>{}
		: Optional<BankName>{bankWithAccount->first};
}

void BankLedgers::add(Bank bank, map<AccountNumber, SupportedAccountTypes> accountNumbersBankTypes)
{
	if (bank.accounts().size() != accountNumbersBankTypes.size())
		throw BankError{
			bank.name_,
			QObject::tr(
				"The number of accounts associated with the new bank is not the same as the "
					"number of bank account types."
			)
		};

	for (const auto& accountNumber : bank.accounts())
		if ( ! this->ledgers_->hasLedger(AccountCode{accountNumber}))
			throw BankError{
				bank.name_,
				QObject::tr(
					"Adding the account '%1' is not in the general ledger."
				).arg(presentationText(accountNumber))
			};

	for (const auto& accountNumberBankType : accountNumbersBankTypes)
		if (
			this->ledgers_->ledger(AccountCode{accountNumberBankType.first}).account_.type()
				!= toAccountTypes(accountNumberBankType.second)
		)
			throw BankError{
				bank.name_,
				QObject::tr(
					"Invalid mapping between the account ledger '%1' and the bank account type "
						"'%2' during bank creation."
				)
					.arg(presentationText(accountNumberBankType.first))
					.arg(presentationText(accountNumberBankType.second))
			};
	const auto name{bank.name_};
	this->banks_->add(move(bank));

	for (const auto& accountCodesBankType : accountNumbersBankTypes)
		this->bankAccountTypes_[name][accountCodesBankType.first] = accountCodesBankType.second;
}

void BankLedgers::addAccount(
	const BankName& bn,
	const QString& accountName,
	const SupportedAccountTypes& type,
	const Money& balance,
	AccountNumberGenerator& generator
)
{
	if ( ! this->banks_->hasBank(bn))
		throw BankError{bn, QObject::tr("The bank does not exist.")};
	const auto accounts{this->ledgers_->accounts()};
	const auto accountType{toAccountTypes(type)};
	const auto accountWithSameName{
		find_if(
			accounts.cbegin(),
			accounts.cend(),
			[&accountName] (const auto& account)
			{
				return account.code().name() == accountName;
			}
		)
	};

	if (accountWithSameName != accounts.cend())
	{
		const auto bankWithSameName{this->banks_->find(accountWithSameName->code().number())};
		assert(bankWithSameName.hasValue() && "A bank must be associated with an account by here.");
		throw BankError{
			bn,
			QObject::tr(
				"The account name '%1' is already associated with the bank '%2'."
			)
				.arg(accountName)
				.arg(bankWithSameName->name_.institution())
		};
	}

	try
	{
		const AccountCode ac{++generator, accountName};
		this->ledgers_->add(Account{accountType, ac, balance}, generator);
		this->banks_->add(bn, ac.number());
		this->bankAccountTypes_[bn][ac.number()] = type;
	}
	catch (const exception&)
	{
		--generator;
		throw;
	}
}

void BankLedgers::addAssociation(
	const BankName& name,
	const AccountNumber& number,
	const SupportedAccountTypes& type
)
{
	if ( ! this->banks_->hasBank(name))
		throw BankError{name, QObject::tr("The bank does not exist.")};

	if ( ! this->ledgers_->hasLedger(AccountCode{number}))
		throw BankError{
			name,
			QObject::tr(
				"The general ledger does not have the account number '%1'."
			).arg(presentationText(number))
		};

	if (type == SupportedAccountTypes::Unknown)
		throw BankError{name, QObject::tr("The bank account type cannot be unknown.")};
	this->banks_->add(name, number);
	this->bankAccountTypes_[name][number] = type;
}

void BankLedgers::rename(const BankName& original, const BankName& changed)
{
	this->banks_->rename(original, changed);

	if (this->bankAccountTypes_.count(original) > 0)
	{
		const auto originalSupportedAccountTypes{this->bankAccountTypes_.find(original)};
		assert(originalSupportedAccountTypes != this->bankAccountTypes_.cend());
		this->bankAccountTypes_[changed] = originalSupportedAccountTypes->second;
		this->bankAccountTypes_.erase(originalSupportedAccountTypes);
	}
}

void BankLedgers::close(const BankName& bn)
{
	this->banks_->close(bn);
	set<AccountCode> closed{};

	try
	{
		for (const auto& accountNumber : this->banks_->bank(bn).accounts())
		{
			this->ledgers_->close(AccountCode{accountNumber});
			closed.emplace(accountNumber);
		}
	}
	catch (const exception&)
	{
		for (const auto& accountCode : closed)
			this->ledgers_->open(accountCode);
		this->banks_->open(bn);
		throw;
	}
}

void BankLedgers::open(const BankName& name)
{
	this->banks_->open(name);

	for (const auto& accountNumber : this->banks_->bank(name).accounts())
		this->ledgers_->open(AccountCode{accountNumber});
}

bool BankLedgers::isEmpty() const
{
	return this->banks_->empty();
}

void BankLedgers::clearBankAccountType()
{
	this->bankAccountTypes_.clear();
}

void BankLedgers::remove(const BankName& bn)
{
	if ( ! this->banks_->hasBank(bn))
		throw BankError{
			bn,
			QObject::tr("Cannot remove when a bank by that name does not exist.")
		};
	const auto& bank{this->banks_->bank(bn)};

	if (bank.isClosed())
		throw BankError{bn, QObject::tr("Cannot remove when the bank is closed.")};

	for (const auto& number : bank.accounts())
		if ( ! this->ledgers_->canRemove(AccountCode{number}))
			throw BankError{
				bn,
				QObject::tr(
					"Cannot remove the bank as the account number '%1' cannot be removed."
				).arg(presentationText(number))
			};

	for (const auto& number : bank.accounts())
		this->ledgers_->remove(AccountCode{number});
	this->banks_->remove(bn);
	this->bankAccountTypes_.erase(bn);
}

void BankLedgers::update(
	Bank bank,
	const map<AccountNumber, SupportedAccountTypes>& accountNumbersBankTypes
)
{
	if ( ! this->banks_->hasBank(bank.name_))
		throw BankError{
			bank.name_,
			QObject::tr("Cannot update when a bank by that name does not exist.")
		};

	if (bank.isClosed())
		throw BankError{bank.name_, QObject::tr("Cannot close a bank via an update.")};

	if (bank.accounts().size() != accountNumbersBankTypes.size())
		throw BankError{
			bank.name_,
			QObject::tr(
				"The number of accounts associated with the bank is not the same as the number of "
					"bank account types."
			)
		};

	for (const auto& accountNumber : bank.accounts())
		if ( ! this->ledgers_->hasLedger(AccountCode{accountNumber}))
			throw BankError{
				bank.name_,
				QObject::tr(
					"Updating the account '%1' is not in the general ledger."
				).arg(presentationText(accountNumber))
			};

	for (const auto& accountNumbersBankType : accountNumbersBankTypes)
		if (
			this->ledgers_->ledger(AccountCode{accountNumbersBankType.first}).account_.type()
				!= toAccountTypes(accountNumbersBankType.second)
		)
			throw BankError{
				bank.name_,
				QObject::tr(
					"Invalid mapping between the account ledger '%1' and the bank account type "
						"'%2'."
				)
					.arg(presentationText(accountNumbersBankType.first))
					.arg(presentationText(accountNumbersBankType.second))
			};

	for (const auto& accountNumbersBankType : accountNumbersBankTypes)
		if (
			this->banks_->bank(bank.name_).accounts().count(accountNumbersBankType.first) == 1
			&& this->bankAccountTypes_.at(bank.name_).at(accountNumbersBankType.first)
				!= accountNumbersBankType.second
		)
			throw BankError{
				bank.name_,
				QObject::tr("Cannot change the bank account type once set.")
			};
	const auto accountNumbers{bank.accounts()};
	const auto name{bank.name_};
	auto removedNumbers{this->banks_->bank(bank.name_).accounts()};
	this->banks_->update(move(bank));

	for (const auto& accountNumber : accountNumbers)
	{
		const auto current{this->bankAccountTypes_[name].find(accountNumber)};

		if (current != this->bankAccountTypes_[name].cend())
			this->bankAccountTypes_[name].erase(current);
		this->bankAccountTypes_[name][accountNumber] = accountNumbersBankTypes.at(accountNumber);
		removedNumbers.erase(accountNumber);
	}

	for (const auto& removedNumber : removedNumbers)
		this->bankAccountTypes_[name].erase(removedNumber);

	if (this->bankAccountTypes_[name].empty())
		this->bankAccountTypes_.erase(name);
}

void BankLedgers::recode(const AccountNumber& original, const AccountNumber& changed)
{
	const auto accountBankName{this->findBankName(original)};

	if ( ! accountBankName.hasValue())
		throw AccountError{
			AccountCode{original},
			QObject::tr("Cannot recode a bank account when no association exists.")
		};
	const auto changedBankName{this->findBankName(changed)};

	if (changedBankName.hasValue() && *accountBankName != *changedBankName)
		throw BankError{
			*changedBankName,
			QObject::tr(
				"Cannot recode the account '%1' to the account '%2' as it is already associated."
			)
				.arg(presentationText(original))
				.arg(presentationText(changed))
		};
	const auto bankType{this->bankAccountTypes_.at(*accountBankName).at(original)};
	this->removeAssociation(*accountBankName, original);
	this->addAssociation(*accountBankName, changed, bankType);
}

bool BankLedgers::hasAssociation(const BankName& name, const AccountNumber& number)
{
	if ( ! this->banks_->hasBank(name))
		return false;
	return this->findBankName(number).hasValue();
}

void BankLedgers::removeAssociation(const BankName& name, const AccountNumber& number)
{
	if ( ! this->banks_->hasBank(name))
		throw BankError{
			name,
			QObject::tr("Cannot remove a bank account when the bank does not exist.")
		};

	if ( ! this->findBankName(number).hasValue())
		throw BankError{
			name,
			QObject::tr("Cannot remove a bank account when no association exists.")
		};
	this->banks_->remove(name, number);

	if (this->bankAccountTypes_.at(name).erase(number) != 1)
		throw BankError{
			name,
			QObject::tr(
				"Failed to remove account association '%1'."
			).arg(presentationText(number))
		};
}

set<BankAccountType> BankLedgers::bankAccountCodes(const BankName& name) const
{
	set<BankAccountType> accountTypes;

	if (this->bankAccountTypes_.count(name) == 0)
		return {};

	for (const auto& accountNumberType : this->bankAccountTypes_.at(name))
		accountTypes.emplace(
			BankAccountType{
				this->ledgers_->ledger(accountNumberType.first).account_.code(),
				accountNumberType.second
			}
		);
	return accountTypes;
}

bool drn::navigation::internal::operator==(const BankLedgers& lhs, const BankLedgers& rhs)
{
	return lhs.banks() == rhs.banks() && lhs.ledgers() == rhs.ledgers();
}

ostream& drn::navigation::internal::operator<<(ostream& o, const BankLedgers& ba)
{
	return o << "Banks (" << ba.banks() << "), General Ledger (" << ba.ledgers() << ')';
}
