/*******************************************************************************
***  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/>.    ***
*******************************************************************************/
#ifndef DRN_NAVIGATION_INTERNAL_BUDGETBANKLEDGERS_H_
#define DRN_NAVIGATION_INTERNAL_BUDGETBANKLEDGERS_H_

#include <cassert>
#include <chrono>
#include <iosfwd>
#include <map>
#include <memory>
#include <set>
#include <type_traits>
#include <utility>

#include <accounting/AccountCode.h>
#include <accounting/LedgerBooks.h>
#include <banking/BankMap.h>
#include <banking/BankAccount.h>
#include <banking/Reconciliation.h>
#include "BankLedgers.h"
#include <budgeting/Budget.h>
#include "BudgetLedgers.h"
#include <foundation/CalendarTime.hpp>
#include <foundation/Optional.hpp>
#include <foundation/TypeIndexMap.hpp>


class QDate;
class QString;

namespace pecunia
{
namespace currency
{

enum class Iso4217Codes : quint16;

}}

namespace drn
{
namespace accounting
{

class Account;
class AccountNumberGenerator;
class Transaction;
class TransactionNumber;

}
namespace banking
{

class Bank;
class BankName;

}
namespace budgeting
{

struct BudgetItemDue;
struct BudgetItemIdentifier;
enum class BudgetItemTypes : quint8;
class BudgetSource;

}
namespace navigation
{

class BankAccountSurvey;
struct BudgetBankAccount;

namespace internal
{

/**
 * @brief Manages the relationship between the budget, the general ledger, and bank accounts.
 */
class BudgetBankLedgers
{
	void verifyBudgetSourceName(const budgeting::BudgetSource& bs);
	void verifyAccountCodeName(const accounting::AccountCode& ac);
	void addBudgetBankAssociation(
		const budgeting::Bill& b,
		accounting::AccountCode& createdBudgetItem,
		const banking::BankAccount& associate
	);
	void addBudgetBankAssociation(
		const budgeting::Debt& d,
		accounting::AccountCode& createdBudgetItem,
		const banking::BankAccount& associate
	);
	void addBudgetBankAssociation(
		const budgeting::Goal& g,
		accounting::AccountCode& createdBudgetItem,
		const banking::BankAccount& associate
	);
	void addBudgetBankAssociation(
		const budgeting::Nontrack& n,
		accounting::AccountCode& createdBudgetItem,
		const banking::BankAccount& associate
	);
	void addBudgetBankAssociation(
		const budgeting::Wage& w,
		accounting::AccountCode& createdBudgetItem,
		const banking::BankAccount& associate
	);
	void updateBudgetBankAssociation(
		const budgeting::Bill& preupdate,
		const accounting::AccountCode& createdBudgetItem,
		const drn::foundation::Optional<banking::BankAccount>& associate
	);
	void updateBudgetBankAssociation(
		const budgeting::Debt& preupdate,
		const accounting::AccountCode& createdBudgetItem,
		const drn::foundation::Optional<banking::BankAccount>& associate
	);
	void updateBudgetBankAssociation(
		const budgeting::Goal& preupdate,
		const accounting::AccountCode& createdBudgetItem,
		const drn::foundation::Optional<banking::BankAccount>& associate
	);
	void updateBudgetBankAssociation(
		const budgeting::Nontrack& preupdate,
		const accounting::AccountCode& createdBudgetItem,
		const drn::foundation::Optional<banking::BankAccount>& associate
	);
	void updateBudgetBankAssociation(
		const budgeting::Wage& preupdate,
		const accounting::AccountCode& createdBudgetItem,
		const drn::foundation::Optional<banking::BankAccount>& associate
	);

	accounting::GeneralLedger ledgers_;
	budgeting::Budget budget_;
	banking::BankMap banks_;
	std::unique_ptr<accounting::AccountNumberGenerator> accountCodeNumberGenerator_;
	BudgetLedgers budgetLedgers_;
	BankLedgers bankLedgers_;
	banking::ReconciledBankAccounts reconciliations_;

public:
	BudgetBankLedgers();
	/**
	 * @brief Fully constructs the object.
	 *
	 * @pre The accountCodeNumberGenerator cannot be null.
	 *
	 * @exception AccountError When the ledger contains an account code name that is same as a
	 * budget item source.
	 *
	 * @param ledgers The values for the General Ledger.
	 * @param budget The values for the budget.
	 * @param budgetCodes The account ledger codes connecting the budgets.
	 * @param banks The values for the banks.
	 * @param bankAccountTypes The account ledger codes connecting the banks.
	 * @param accountCodeNumberGenerator The generator for creating new account codes.
	 */
	BudgetBankLedgers(
		accounting::GeneralLedger ledgers,
		budgeting::Budget budget,
		foundation::TypeIndexMap<budgeting::BudgetSource, accounting::AccountCode> budgetCodes,
		banking::BankMap banks,
		std::map<
			banking::BankName,
			std::map<accounting::AccountNumber, banking::SupportedAccountTypes>
		> bankAccountTypes,
		std::unique_ptr<accounting::AccountNumberGenerator> accountCodeNumberGenerator
	);
	BudgetBankLedgers(const BudgetBankLedgers& other);
	BudgetBankLedgers(BudgetBankLedgers&& other) noexcept;
	~BudgetBankLedgers();
	BudgetBankLedgers& operator=(const BudgetBankLedgers& other) = delete;
	BudgetBankLedgers& operator=(BudgetBankLedgers&& other) noexcept;
	const BankLedgers& bankLedgers() const;
	const BudgetLedgers& budgetLedgers() const;
	/**
	 * @brief Changes the budgeting system objects being managed.
	 *
	 * @exception AccountError When there are issues with an account.
	 * @exception BankError When there are issues with a bank.
	 * @exception BudgetItemError When there are issues with a budget.
	 *
	 * @param ledgers The new general ledger books to start managing.
	 * @param budget The new budget to start managing.
	 * @param budgetCodes The associations between a budget item and the general ledger account for
	 * that item.
	 * @param banks The new financial institutions to start managing.
	 * @param bankAccountTypes The associations between financial institutions and the general
	 * ledger to start managing.
	 */
	void setManaged(
		accounting::GeneralLedger ledgers,
		budgeting::Budget budget,
		foundation::TypeIndexMap<budgeting::BudgetSource, accounting::AccountCode> budgetCodes,
		banking::BankMap banks,
		std::map<
			banking::BankName,
			std::map<accounting::AccountNumber, banking::SupportedAccountTypes>
		> bankAccountTypes
	);
	/**
	 * @brief Determines if the object is in an empty state.
	 *
	 * @return Gives true when the banks, budget, and ledgers are all empty, other wise false.
	 */
	bool isEmpty() const;
	/**
	 * @brief Clears all the user data.
	 */
	void reset();
	/**
	 * @brief The current budget.
	 */
	const budgeting::Budget& budget() const;
	/**
	 * @brief The ledgers in the general ledger book.
	 */
	const accounting::GeneralLedger& ledgers() const;
	/**
	 * @brief The current banks.
	 */
	const banking::BankMap& banks() const;
	/**
	 * @brief Generates an association between account codes, which are associated with a bank, and
	 * their user understandable bank account type.
	 *
	 * @return The association of account codes to bank account types, or empty when no banks have
	 * any account associations.
	 */
	std::set<banking::BankAccountType> bankAccountTypes() const;
	/**
	 * @brief Changes the automated account code number generator.
	 *
	 * @pre The supplied generator cannot be null.
	 *
	 * @param accountCodeNumberGenerator The generator to change to.
	 */
	void setAccountNumberGenerator(
		std::unique_ptr<accounting::AccountNumberGenerator> accountCodeNumberGenerator
	);
	/**
	 * @brief An accessor to the automated account code number generator;
	 */
	const accounting::AccountNumberGenerator& accountCodeNumberGenerator() const;
	/**
	 * @brief Collects all the known bank names.
	 */
	std::set<banking::BankName> bankNames() const;

	//{ Budgeting Member Functions

	/**
	 * @brief Adds a new budget item to the budget and the associated ledger account.
	 *
	 * @exception BudgetItemError When the budget item source is the same as an existing account
	 * code name.
	 *
	 * @tparam BudgetItemType The type of the budget item to add.
	 *
	 * @param item The budget item that is to be added.
	 *
	 * @return The account code created for storing the budget item's transactions.
	 */
	template<typename BudgetItemType>
	accounting::AccountCode add(
		BudgetItemType item,
		const foundation::Optional<banking::BankAccount>& ba = {}
	);
	template<typename BudgetItemType>
	void update(
		BudgetItemType item,
		const foundation::Optional<banking::BankAccount>& ba = {}
	);
	void removeWage(const budgeting::BudgetSource& source);
	void removeBill(const budgeting::BudgetSource& source);
	void removeDebt(const budgeting::BudgetSource& source);
	void removeGoal(const budgeting::BudgetSource& source);
	void removeNontrack(const budgeting::BudgetSource& source);
	/**
	 * @brief Changes the name of a wage's source.
	 *
	 * @exception BudgetItemError When the budget item source is the same as an existing account
	 * code name.
	 *
	 * @param original The current budget item source.
	 * @param changed The new budget item source.
	 */
	void renameWage(
		const budgeting::BudgetSource& original,
		const budgeting::BudgetSource& changed
	);
	/**
	 * @brief Changes the name of a bill's source.
	 *
	 * @exception BudgetItemError When the budget item source is the same as an existing account
	 * code name.
	 *
	 * @param original The current budget item source.
	 * @param changed The new budget item source.
	 */
	void renameBill(
		const budgeting::BudgetSource& original,
		const budgeting::BudgetSource& changed
	);
	/**
	 * @brief Changes the name of a debt's source.
	 *
	 * @exception BudgetItemError When the budget item source is the same as an existing account
	 * code name.
	 *
	 * @param original The current budget item source.
	 * @param changed The new budget item source.
	 */
	void renameDebt(
		const budgeting::BudgetSource& original,
		const budgeting::BudgetSource& changed
	);
	/**
	 * @brief Changes the name of a goal's source.
	 *
	 * @exception BudgetItemError When the budget item source is the same as an existing account
	 * code name.
	 *
	 * @param original The current budget item source.
	 * @param changed The new budget item source.
	 */
	void renameGoal(
		const budgeting::BudgetSource& original,
		const budgeting::BudgetSource& changed
	);
	/**
	 * @brief Changes the name of a nontrack's source.
	 *
	 * @exception BudgetItemError When the budget item source is the same as an existing account
	 * code name.
	 *
	 * @param original The current budget item source.
	 * @param changed The new budget item source.
	 */
	void renameNontrack(
		const budgeting::BudgetSource& original,
		const budgeting::BudgetSource& changed
	);

	//}

	//{ Accounting Member Functions

	/**
	 * @brief Adds an account to the general ledger.
	 *
	 * @exception AccountError When the ledger contains an account code name that is same as a
	 * budget item source.
	 *
	 * @param account The account to add.
	 */
	void add(accounting::Account account);
	void close(const accounting::AccountCode& code);
	void open(const accounting::AccountCode& code);
	void remove(const accounting::AccountCode& code);
	/**
	 * @brief Recodes an account in the general ledger.
	 *
	 * @exception AccountError When the ledger contains an account code name that is same as a
	 * budget item source.
	 *
	 * @param original The current general ledger code.
	 * @param changed The new general ledger code.
	 */
	void recode(const accounting::AccountCode& original, const accounting::AccountCode& changed);
	void clear(const accounting::TransactionNumber& number);
	void reconcile(const accounting::TransactionNumber& number);
	accounting::TransactionNumber post(const accounting::Transaction& txn);
	void repost(const accounting::TransactionNumber& number, const accounting::Transaction& txn);
	void remove(const accounting::TransactionNumber& number);

	//}

	//{ Bank Member Functions

	/**
	 * @brief Adds a new bank to the system.
	 *
	 * @exception BankError When the financial institution is already closed.
	 * @exception BankError When the financial institution is already present in the container.
	 * @exception BankError When the ledger account type cannot be associated with the
	 * financial institution type.
	 *
	 * @param b The bank to add. This is expected not to have any accounts associated with it.
	 * @param accountNumbersBankTypes The general ledger account numbers and bank type associated
	 * with the bank.
	 */
	void add(
		banking::Bank b,
		std::map<
			accounting::AccountNumber,
			banking::SupportedAccountTypes
		> accountNumbersBankTypes = {}
	);
	/**
	 * @brief Adds an association with an account code to a bank.
	 *
	 * @exception BankError When the bank does not exist.
	 * @exception BankError When another bank is already associated with that code.
	 * @exception BankError When the account code does not exist.
	 *
	 * @param name The name of the bank to add the account code to.
	 * @param number The number to add to the bank.
	 * @param type The type of bank account the general ledger account is managing.
	 */
	void addAssociation(
		const banking::BankName& name,
		const accounting::AccountNumber& number,
		const banking::SupportedAccountTypes& type
	);
	/**
	 * @brief Creates an account in the accounting system.
	 *
	 * @exception AccountError When the bank account type is not known.
	 * @exception AccountError When the account name is not valid.
	 * @exception AccountError When the account currency is not valid.
	 *
	 * @param name The name of the account to add.
	 * @param type The purpose type of the account.
	 * @param openingBalance The opening balance for the account.
	 *
	 * @return The code for the account created.
	 */
	accounting::AccountCode addAccount(
		::QString name,
		const banking::SupportedAccountTypes& type,
		const pecunia::currency::Money& openingBalance
	);
	/**
	 * @brief Updates the information for a financial institution and the associated accounts.
	 *
	 * @exception BankError When the financial institution is already closed.
	 * @exception BankError When the financial institution is not present in the container.
	 * @exception BankError When the ledger account is already associated with another financial
	 * institution.
	 * @exception BankError When the ledger account type cannot be associated with the
	 * financial institution type.
	 *
	 * @param b The financial institution whose information is to be updated.
	 * @param accountNumbersBankTypes The new account ledger associations for the financial
	 * institution.
	 */
	void update(
		banking::Bank b,
		std::map<
			accounting::AccountNumber,
			banking::SupportedAccountTypes
		> accountNumbersBankTypes = {}
	);
	/**
	 * @brief Closes a bank and all associated financial institutions.
	 *
	 * @exception BankError When the financial institution is already closed.
	 * @exception BankError When the financial institution does not exist.
	 *
	 * @param name The name of the bank to close.
	 */
	void close(const banking::BankName& name);
	/**
	 * @brief Opens a bank and all associated financial institutions.
	 *
	 * @exception BankError When the financial institution is already opened.
	 * @exception BankError When the financial institution does not exist.
	 *
	 * @param name The name of the bank to open.
	 */
	void open(const banking::BankName& name);
	/**
	 * @brief Renames a bank and all associations under that name.
	 *
	 * @exception BankError When the financial institution name to be changed does not exist.
	 * @exception BankError When the financial institution name to be changed and to be changed
	 * into are the same.
	 * @exception BankError When the financial institution name to change to already exists.
	 * @exception BankError When the financial institution is already closed.
	 *
	 * @param original The current name of the bank.
	 * @param changed The new name for the bank.
	 */
	void rename(const banking::BankName& original, const banking::BankName& changed);
	/**
	 * @brief Removes a financial institution and all general ledgers under the financial
	 * institution's association.
	 *
	 * @exception BankError When the financial institution to remove does not exist.
	 * @exception BankError When the financial institution cannot be removed.
	 *
	 * @param bank The financial institution to remove.
	 */
	void remove(const banking::BankName& bank);

	//}

	accounting::TransactionNumber post(
		const budgeting::BudgetItemDue& bid,
		const accounting::AccountCode& ac,
		const ::QDate& postOn
	);
	accounting::TransactionNumber post(
		const budgeting::BudgetItemIdentifier& bii,
		const pecunia::currency::Money& amount,
		const accounting::AccountCode& ac,
		const ::QDate& postOn
	);
	void skip(const budgeting::BudgetItemIdentifier& bii);
	std::set<BudgetBankAccount> budgetBankAccounts(const budgeting::BudgetItemTypes& type) const;
	BankAccountSurvey projectedSurvey(
		const banking::BankName& bn,
		const accounting::AccountNumber& an,
		const ::QDate& surveyedOn,
		const std::chrono::hours& hrInWeek,
		const foundation::Days& dayInWeek
	);
	void updateSurvey(
		const banking::BankName& bn,
		const accounting::AccountNumber& an,
		const std::vector<accounting::TransactionNumber> reconciled,
		std::vector<accounting::Transaction> added,
		// TODO: rename unusedBudget
		const std::map<budgeting::BudgetItemIdentifier, pecunia::currency::Money>& unspentBudget, // sum(unspentBudget) == balance
		pecunia::currency::Money balance // == prior_balance + reconciled + added
	);
};

bool operator==(const BudgetBankLedgers& lhs, const BudgetBankLedgers& rhs);
bool operator!=(const BudgetBankLedgers& lhs, const BudgetBankLedgers& rhs);
std::ostream& operator<<(std::ostream& o, const BudgetBankLedgers& bba);

}}}

template<typename BudgetItemType>
drn::accounting::AccountCode drn::navigation::internal::BudgetBankLedgers::add(
	BudgetItemType item,
	const drn::foundation::Optional<banking::BankAccount>& ba
)
{
	static_assert(
		std::is_base_of<drn::budgeting::BudgetedMoney, BudgetItemType>::value,
		"The budget item type must be derived from a drn::budgeting::BudgetedMoney."
	);
	assert(
		this->accountCodeNumberGenerator_ != nullptr
		&& "Account code number generator must be set."
	);
	const auto source{item.source()};
	this->verifyBudgetSourceName(source);
	auto accountCode{
		this->budgetLedgers_.add(std::move(item), *this->accountCodeNumberGenerator_)
	};

	if (ba.hasValue())
		this->addBudgetBankAssociation(
			BudgetItemType{source},
			accountCode,
			*ba
		);
	return accountCode;
}

template<typename BudgetItemType>
void drn::navigation::internal::BudgetBankLedgers::update(
	BudgetItemType item,
	const drn::foundation::Optional<banking::BankAccount>& ba
)
{
	static_assert(
		std::is_base_of<drn::budgeting::BudgetedMoney, BudgetItemType>::value,
		"The budget item type must be derived from a drn::budgeting::BudgetedMoney."
	);
	assert(
		this->accountCodeNumberGenerator_ != nullptr
		&& "Account code number generator must be set."
	);
	const auto preUpdate{
		drn::budgeting::findBudgetItem<BudgetItemType>(item.source(), this->budget_)
	};
	this->budgetLedgers_.update(std::move(item), *this->accountCodeNumberGenerator_);
	const auto accountCode{
		this->budgetLedgers_.budgetAccountCode<BudgetItemType>(preUpdate->source())
	};
	this->updateBudgetBankAssociation(*preUpdate, accountCode, ba);
}

#endif
