/*******************************************************************************
***  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 "Navigator.h"
using pecunia::currency::Money;
using ::QObject;
using ::QString;
using std::chrono::hours;
using std::map;
using std::make_unique;
using std::set;
using drn::accounting::Account;
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
using drn::accounting::presentationText;
using drn::accounting::Transaction;
using drn::accounting::TransactionNumber;
using drn::adviser::BudgetHealth;
using drn::banking::Bank;
using drn::banking::SupportedAccountTypes;
using drn::banking::BankName;
using drn::budgeting::BudgetedMoney;
using drn::budgeting::BudgetItemDue;
using drn::budgeting::BudgetItemIdentifier;
using drn::budgeting::budgetItemsDue;
using drn::budgeting::BudgetItemTypes;
using drn::budgeting::BudgetSource;
using drn::budgeting::BudgetTotals;
using drn::conversion::ConversionMap;
using drn::foundation::Days;
using drn::foundation::Optional;
using drn::navigation::Navigator;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <pecunia/SetUp.h>
using pecunia::currency::setUpCurrency;
#include <QLocale>
using ::QLocale;

#include <tuple>
using std::get;
#include <stdexcept>
using std::exception;
using std::logic_error;
#include <utility>
using std::move;

#include <banking/BankAccount.h>
using drn::banking::BankAccount;
#include "BudgetBankAccount.h"
using drn::navigation::BudgetBankAccount;
#include <budgeting/Bill.h>
using drn::budgeting::Bill;
#include <budgeting/Constants.hpp>
using drn::budgeting::maximumDaysInAWeek;
using drn::budgeting::maximumDaysNotifyWithin;
using drn::budgeting::maximumHoursInAWeek;
using drn::budgeting::minimumDaysInAWeek;
using drn::budgeting::minimumDaysNotifyWithin;
using drn::budgeting::minimumHoursInAWeek;
#include <budgeting/Debt.h>
using drn::budgeting::Debt;
#include <budgeting/EventFrequency.h>
using drn::budgeting::EventFrequency;
#include <budgeting/Goal.h>
using drn::budgeting::Goal;
#include <budgeting/Nontrack.h>
using drn::budgeting::Nontrack;
#include <budgeting/Wage.h>
using drn::budgeting::Wage;
#include <conversion/CurrencyConverter.h>
using drn::conversion::CurrencyConverter;
#include <conversion/CurrencySource.hpp>
using drn::conversion::CurrencySource;
#include <conversion/OpenExchangeRatesSource.h>
using drn::conversion::OpenExchangeRatesSource;
#include <drn/config.h>
// using DuxReiNummariae_OPEN_EXCHANGE_RATES_KEY
#include <file-storage/BudgetFile.h>
using drn::file_storage::BudgetFile;
#include <file-storage/Constants.hpp>
using drn::file_storage::budgetFileExtention_;
#include <file-storage/ExchangeRatesFile.h>
using drn::file_storage::ExchangeRatesFile;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/QtMemory.hpp>
using drn::foundation::makeQtPtr;
#include "internal/BudgetBankLedgers.h"
using drn::navigation::internal::BudgetBankLedgers;


void Navigator::onPrepareShowBills()
{
	emit showBills(
		this->budgetBankLedgers_->budget().bills_,
		this->preferences_.hoursInWeek(),
		this->preferences_.daysInWeek(),
		this->preferences_.preferredCurrency(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onPrepareShowDebts()
{
	emit showDebts(
		this->budgetBankLedgers_->budget().debts_,
		this->budgetBankLedgers_->budgetBankAccounts(BudgetItemTypes::Debt),
		this->budgetBankLedgers_->bankNames(),
		this->preferences_.hoursInWeek(),
		this->preferences_.daysInWeek(),
		this->preferences_.preferredCurrency(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onPrepareShowGoals()
{
	emit showGoals(
		this->budgetBankLedgers_->budget().goals_,
		this->budgetBankLedgers_->budgetBankAccounts(BudgetItemTypes::Goal),
		this->budgetBankLedgers_->bankNames(),
		this->preferences_.hoursInWeek(),
		this->preferences_.daysInWeek(),
		this->preferences_.preferredCurrency(),
		this->preferences_.usableCurrencies(),
		this->budgetBankLedgers_->budget().wages_.total(
			this->preferences_.preferredCurrency(),
			EventFrequency::Monthly,
			this->preferences_.hoursInWeek(),
			this->preferences_.daysInWeek()
		)
	);
}

void Navigator::onPrepareShowNontracks()
{
	emit showNontracks(
		this->budgetBankLedgers_->budget().nontracks_,
		this->preferences_.hoursInWeek(),
		this->preferences_.daysInWeek(),
		this->preferences_.preferredCurrency(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onPrepareShowWages()
{
	emit showWages(
		this->budgetBankLedgers_->budget().wages_,
		this->preferences_.hoursInWeek(),
		this->preferences_.daysInWeek(),
		this->preferences_.preferredCurrency(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onAdded(
	const BudgetItemTypes& type,
	const BudgetedMoney& item,
	const Optional<BankAccount>& ba
)
try
{
	switch (type)
	{
	case BudgetItemTypes::Bill:
	{
		const auto& bill{dynamic_cast<const Bill&>(item)};
		this->budgetBankLedgers_->add(bill, ba);
		break;
	}
	case BudgetItemTypes::Debt:
	{
		const auto& debt{dynamic_cast<const Debt&>(item)};
		this->budgetBankLedgers_->add(debt, ba);
		break;
	}
	case BudgetItemTypes::Goal:
	{
		const auto& goal{dynamic_cast<const Goal&>(item)};
		this->budgetBankLedgers_->add(goal, ba);
		break;
	}
	case BudgetItemTypes::Nontrack:
	{
		const auto& nontrack{dynamic_cast<const Nontrack&>(item)};
		this->budgetBankLedgers_->add(nontrack, ba);
		break;
	}
	case BudgetItemTypes::Wage:
	{
		const auto& wage{dynamic_cast<const Wage&>(item)};
		this->budgetBankLedgers_->add(wage, ba);
		break;
	}
	default:
		throw logic_error{"An unknown budget item type was supplied when adding a new entry."};
	};
	this->isDirty_.flipOn();
	emit saveSucceeded(item.source());
}
catch (const Error& e)
{
	qWarning() << "Failed to add budget item." << e.what();
	// N.B. When adding a new entry it cannot have an original name.
	emit saveFailed({}, e.message(), item.source());
}

void Navigator::onRenamed(
	const BudgetItemTypes& type,
	const BudgetSource& original,
	const BudgetSource& changed
)
try
{
	switch (type)
	{
		case BudgetItemTypes::Bill:
			this->budgetBankLedgers_->renameBill(original, changed);
			break;
		case BudgetItemTypes::Debt:
			this->budgetBankLedgers_->renameDebt(original, changed);
			break;
		case BudgetItemTypes::Goal:
			this->budgetBankLedgers_->renameGoal(original, changed);
			break;
		case BudgetItemTypes::Nontrack:
			this->budgetBankLedgers_->renameNontrack(original, changed);
			break;
		case BudgetItemTypes::Wage:
			this->budgetBankLedgers_->renameWage(original, changed);
			break;
		default:
			throw logic_error{"An unknown budget item type was supplied when renaming an entry."};
	};
	this->isDirty_.flipOn();
	emit saveSucceeded(original);
}
catch (const Error& e)
{
	qWarning() << "Failed to rename budget item." << e.what();
	emit saveFailed(original, e.message(), changed);
}

void Navigator::onUpdated(
	const BudgetItemTypes& type,
	const BudgetedMoney& item,
	const Optional<BankAccount>& ba
)
try
{
	switch (type)
	{
	case BudgetItemTypes::Bill:
	{
		const auto& bill{dynamic_cast<const Bill&>(item)};
		this->budgetBankLedgers_->update(bill, ba);
		break;
	}
	case BudgetItemTypes::Debt:
	{
		const auto& debt{dynamic_cast<const Debt&>(item)};
		this->budgetBankLedgers_->update(debt, ba);
		break;
	}
	case BudgetItemTypes::Goal:
	{
		const auto& goal{dynamic_cast<const Goal&>(item)};
		this->budgetBankLedgers_->update(goal, ba);
		break;
	}
	case BudgetItemTypes::Nontrack:
	{
		const auto& nontrack{dynamic_cast<const Nontrack&>(item)};
		this->budgetBankLedgers_->update(nontrack, ba);
		break;
	}
	case BudgetItemTypes::Wage:
	{
		const auto& wage{dynamic_cast<const Wage&>(item)};
		this->budgetBankLedgers_->update(wage, ba);
		break;
	}
	default:
		throw logic_error{"An unknown budget item type was supplied when updating an entry."};
	};
	this->isDirty_.flipOn();
	emit saveSucceeded(item.source());
}
catch (const Error& e)
{
	qWarning() << "Failed to update budget item." << e.what();
	// N.B. When an item is updated its source name cannot change, that's a separate action.
	emit saveFailed(item.source(), e.message(), item.source());
}

void Navigator::onRemoved(const BudgetItemTypes& type, const BudgetSource& source)
try
{
	switch (type)
	{
		case BudgetItemTypes::Bill:
			this->budgetBankLedgers_->removeBill(source);
			break;
		case BudgetItemTypes::Debt:
			this->budgetBankLedgers_->removeDebt(source);
			break;
		case BudgetItemTypes::Goal:
			this->budgetBankLedgers_->removeGoal(source);
			break;
		case BudgetItemTypes::Nontrack:
			this->budgetBankLedgers_->removeNontrack(source);
			break;
		case BudgetItemTypes::Wage:
			this->budgetBankLedgers_->removeWage(source);
			break;
		default:
			throw logic_error{"An unknown budget item type was supplied when removing an entry."};
	};
	this->isDirty_.flipOn();
	emit saveSucceeded(source);
}
catch (const Error& e)
{
	qWarning() << "Failed to remove budget item." << e.what();
	emit saveFailed(source, e.message(), {});
}

void Navigator::onPrepareShowAccounts()
{
	emit showAccounts(
		this->budgetBankLedgers_->ledgers().accounts(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onAddedAct(const Account& a)
try
{
	this->budgetBankLedgers_->add(a);
	this->isDirty_.flipOn();
	emit accountSaveSucceeded(a.code());
}
catch (const Error& e)
{
	qWarning() << "Failed to add account entry." << e.what();
	// N.B. When adding a new entry it cannot have an original name.
	emit accountSaveFailed({}, e.message(), a.code());
}

void Navigator::onRecoded(const AccountCode& current, const AccountCode& changed)
try
{
	this->budgetBankLedgers_->recode(current, changed);
	this->isDirty_.flipOn();
	emit accountSaveSucceeded(current);
}
catch (const Error& e)
{
	qWarning() << "Failed to recode an account." << e.what();
	emit accountSaveFailed(current, e.message(), changed);
}

void Navigator::onClosed(const AccountCode& code)
try
{
	this->budgetBankLedgers_->close(code);
	this->isDirty_.flipOn();
	emit accountSaveSucceeded(code);
}
catch (const Error& e)
{
	qWarning() << "Failed to close an account." << e.what();
	emit accountSaveFailed(code, e.message(), code);
}

void Navigator::onOpened(const AccountCode& code)
try
{
	this->budgetBankLedgers_->open(code);
	this->isDirty_.flipOn();
	emit accountSaveSucceeded(code);
}
catch (const Error& e)
{
	qWarning() << "Failed to reopen an account." << e.what();
	emit accountSaveFailed(code, e.message(), code);
}

void Navigator::onRemovedAct(const AccountCode& code)
try
{
	this->budgetBankLedgers_->remove(code);
	this->isDirty_.flipOn();
	emit accountSaveSucceeded(code);
}
catch (const Error& e)
{
	qWarning() << "Failed to remove an account." << e.what();
	emit accountSaveFailed(code, e.message(), {});
}

void Navigator::onPrepareShowLedgers()
{
	emit showLedgers(
		this->budgetBankLedgers_->ledgers().accounts(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onSelectedLedger(const AccountCode& code)
{
	emit showLedger(this->budgetBankLedgers_->ledgers().ledger(code));
}

void Navigator::onAddedTransaction(const Transaction& txn)
try
{
	const auto postedNumber{this->budgetBankLedgers_->post(txn)};
	this->isDirty_.flipOn();
	emit transactionSaveSucceeded(txn.number(), postedNumber);
}
catch (const Error& e)
{
	qWarning() << "Failed to add a transaction." << e.what();
	emit transactionSaveFailed(txn.number(), e.message());
}

void Navigator::onUpdatedTransaction(const TransactionNumber& tn, const Transaction& txn)
try
{
	this->budgetBankLedgers_->repost(tn, txn);
	this->isDirty_.flipOn();
	emit transactionSaveSucceeded(tn, {});
}
catch (const Error& e)
{
	qWarning() << "Failed to update a transaction." << e.what();
	emit transactionSaveFailed(tn, e.message());
}

void Navigator::onMarkCleared(const TransactionNumber& tn)
try
{
	this->budgetBankLedgers_->clear(tn);
	this->isDirty_.flipOn();
	emit transactionSaveSucceeded(tn, {});
}
catch (const Error& e)
{
	qWarning() << "Failed to mark a transaction as cleared." << e.what();
	emit transactionSaveFailed(tn, e.message());
}

void Navigator::onMarkReconciled(const TransactionNumber& tn)
try
{
	this->budgetBankLedgers_->reconcile(tn);
	this->isDirty_.flipOn();
	emit transactionSaveSucceeded(tn, {});
}
catch (const Error& e)
{
	qWarning() << "Failed to mark a transaction as reconciled." << e.what();
	emit transactionSaveFailed(tn, e.message());
}

void Navigator::onRemovedTransaction(const TransactionNumber& tn)
try
{
	this->budgetBankLedgers_->remove(tn);
	this->isDirty_.flipOn();
	emit transactionSaveSucceeded(tn, {});
}
catch (const Error& e)
{
	qWarning() << "Failed to remove a transaction." << e.what();
	emit transactionSaveFailed(tn, e.message());
}

void Navigator::onPrepareShowBanks()
{
	emit showBanks(
		this->budgetBankLedgers_->banks(),
		this->budgetBankLedgers_->ledgers().accounts(),
		this->preferences_.usableCurrencies(),
		this->budgetBankLedgers_->bankAccountTypes()
	);
}

void Navigator::onBankAdded(
	const Bank& b,
	const map<AccountNumber, SupportedAccountTypes>& accountNumbersBankTypes
)
try
{
	this->budgetBankLedgers_->add(b, accountNumbersBankTypes);
	this->isDirty_.flipOn();
	emit bankSaveSucceeded(b.name_);
}
catch (const Error& e)
{
	qWarning() << "Failed to add a bank." << e.what();
	emit bankSaveFailed({}, e.message(), b.name_);
}

void Navigator::onBankUpdated(
	const Bank& b,
	const map<AccountNumber, SupportedAccountTypes>& accountNumbersBankTypes
)
try
{
	this->budgetBankLedgers_->update(b, accountNumbersBankTypes);
	this->isDirty_.flipOn();
	emit bankSaveSucceeded(b.name_);
}
catch (const Error& e)
{
	qWarning() << "Failed to update a bank." << e.what();
	emit bankSaveFailed(b.name_, e.message(), {});
}

void Navigator::onBankRenamed(const BankName& original, const BankName& change)
try
{
	this->budgetBankLedgers_->rename(original, change);
	this->isDirty_.flipOn();
	emit bankSaveSucceeded(change);
}
catch (const Error& e)
{
	qWarning() << "Failed to rename a bank." << e.what();
	emit bankSaveFailed(original, e.message(), change);
}

void Navigator::onBankClosed(const BankName& bn)
try
{
	this->budgetBankLedgers_->close(bn);
	this->isDirty_.flipOn();
	emit bankSaveSucceeded(bn);
}
catch (const Error& e)
{
	qWarning() << "Failed to close a bank." << e.what();
	emit bankSaveFailed(bn, e.message(), {});
}

void Navigator::onBankOpened(const BankName& bn)
try
{
	this->budgetBankLedgers_->open(bn);
	this->isDirty_.flipOn();
	emit bankSaveSucceeded(bn);
}
catch (const Error& e)
{
	qWarning() << "Failed to reopen a bank." << e.what();
	emit bankSaveFailed(bn, e.message(), {});
}

void Navigator::onBankRemoved(const BankName& bn)
try
{
	this->budgetBankLedgers_->remove(bn);
	this->isDirty_.flipOn();
	emit bankSaveSucceeded(bn);
}
catch (const Error& e)
{
	qWarning() << "Failed to remove a bank." << e.what();
	emit bankSaveFailed(bn, e.message(), {});
}

void Navigator::onAddedBankAccount(
	const QString& name,
	const SupportedAccountTypes& type,
	const Money& openingBalance
)
try
{
	const auto added{this->budgetBankLedgers_->addAccount(name, type, openingBalance)};
	this->isDirty_.flipOn();
	emit bankAccountSaveSucceeded(added);
}
catch (const Error& e)
{
	qWarning() << "Failed to add a bank account." << e.what();
	// N.B. When adding a new entry it cannot have an original code.
	emit bankAccountSaveFailed({}, e.message(), AccountCode{name});
}

void Navigator::onRenamedBankAccount(const AccountCode& original, const AccountCode& changed)
try
{
	this->budgetBankLedgers_->recode(original, changed);
	this->isDirty_.flipOn();
	emit bankAccountSaveSucceeded(changed);
}
catch (const Error& e)
{
	qWarning() << "Failed to rename a bank account." << e.what();
	emit bankAccountSaveFailed(original, e.message(), changed);
}

void Navigator::onRemovedBankAccount(const AccountCode& ac)
try
{
	this->budgetBankLedgers_->remove(ac);
	this->isDirty_.flipOn();
	emit bankAccountSaveSucceeded(ac);
}
catch (const Error& e)
{
	qWarning() << "Failed to remove a bank account." << e.what();
	emit bankAccountSaveFailed(ac, e.message(), {});
}

void Navigator::onPrepareShowPostBudgetItems()
{
	emit showPostBudgetItems(
		this->preferences_.notifyDueWithin().count(),
		budgetItemsDue(
			this->preferences_.notifyDueWithin(),
			this->budgetBankLedgers_->budget()
		),
		this->budgetBankLedgers_->bankNames(),
		this->preferences_.preferredCurrency(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onLookUpBankAccounts(const BankName& bn)
{
	emit lookedUpBankAccounts(bn, this->budgetBankLedgers_->bankLedgers().bankAccountCodes(bn));
}

void Navigator::onLookUpBudgetItemSources(
	const BudgetItemIdentifier& requestedBy,
	const BudgetItemTypes& bit
)
{
	const auto sources{
		[&bit, this] ()
		{
			switch (bit)
			{
			case BudgetItemTypes::Bill:
				return this->budgetBankLedgers_->budgetLedgers().budget().bills_.sources();
			case BudgetItemTypes::Debt:
				return this->budgetBankLedgers_->budgetLedgers().budget().debts_.sources();
			case BudgetItemTypes::Goal:
				return this->budgetBankLedgers_->budgetLedgers().budget().goals_.sources();
			case BudgetItemTypes::Nontrack:
				return this->budgetBankLedgers_->budgetLedgers().budget().nontracks_.sources();
			case BudgetItemTypes::Wage:
				return this->budgetBankLedgers_->budgetLedgers().budget().wages_.sources();
			case BudgetItemTypes::Unknown:
				return set<BudgetSource>{};
			default:
				throw logic_error{
					"An unknown budget item type was supplied when looking up all the budget item "
						"sources."
				};
			};
		}()
	};
	emit lookedUpBudgetItemSources(requestedBy, bit, sources);
}

void Navigator::onPostBudgetItemPosted(
	const BudgetItemDue& bid,
	const AccountCode& ac,
	const QDate& postOn
)
try
{
	const TransactionNumber txnNum{this->budgetBankLedgers_->post(bid, ac, postOn)};
	this->isDirty_.flipOn();
	emit postBudgetItemSaveSucceeded(bid.id_, txnNum);
}
catch (const exception& e)
{
	qWarning() << "Failed to post budget item due." << e.what();
	emit postBudgetItemSaveFailed(bid.id_, e.what());
}

void Navigator::onPostBudgetItemPostedUnexpected(
	const BudgetItemIdentifier& bii,
	const Money& amount,
	const AccountCode& ac,
	const QDate& postOn
)
try
{
	const TransactionNumber txnNum{this->budgetBankLedgers_->post(bii, amount, ac, postOn)};
	this->isDirty_.flipOn();
	emit postBudgetItemSaveSucceeded(bii, txnNum);
}
catch (const exception& e)
{
	qWarning() << "Failed to post budget item unexpected." << e.what();
	emit postBudgetItemSaveFailed(bii, e.what());
}

void Navigator::onPostBudgetItemSkipped(const BudgetItemIdentifier& bii)
try
{
	this->budgetBankLedgers_->skip(bii);
	this->isDirty_.flipOn();
	emit postBudgetItemSaveSucceeded(bii, {});
}
catch (const exception& e)
{
	qWarning() << "Failed to skip budget item due." << e.what();
	emit postBudgetItemSaveFailed(bii, e.what());
}

void Navigator::onNewBudgetFile()
{
	if ( ! this->budgetBankLedgers_->isEmpty() && this->isDirty_)
	{
		emit showConfirmNewBudgetFile();
		return;
	}
	this->onConfirmedNewBudgetFile();
}

void Navigator::onConfirmedNewBudgetFile()
{
	qDebug() << "Clearing all the budget file values.";
	this->budgetBankLedgers_->reset();
	this->filePathName_.reset();
	this->isDirty_.flipOff();
	emit clearedBudgetFile();
}

void Navigator::onLoadBudgetFile(const QString& filePathName)
{
	if (this->isDirty_)
	{
		emit showConfirmLoadBudgetFile(filePathName);
		return;
	}
	this->onConfirmedLoadBudgetFile(filePathName, false);
}

void Navigator::onConfirmedLoadBudgetFile(const QString& filePathName, const bool isReloaded)
{
	qInfo() << "Loading Budget:" << filePathName;
	BudgetFile bf{filePathName};
	auto ledgerBudgetBudgetCodesBanksBankTypes{bf.load()};
	this->budgetBankLedgers_->setManaged(
		move(get<0>(ledgerBudgetBudgetCodesBanksBankTypes)),
		move(get<1>(ledgerBudgetBudgetCodesBanksBankTypes)),
		move(get<2>(ledgerBudgetBudgetCodesBanksBankTypes)),
		move(get<3>(ledgerBudgetBudgetCodesBanksBankTypes)),
		move(get<4>(ledgerBudgetBudgetCodesBanksBankTypes))
	);
	qInfo() << "Successfully loaded budget.";
	this->filePathName_ = filePathName;
	this->isDirty_.flipOff();
	emit loadedBudgetFile(bf.name(), isReloaded);
}

void Navigator::onSaveBudgetFile()
{
	qInfo() << "Handling Save Budget to" << this->filePathName_.valueOr("Budget File Not Set");

	if (this->filePathName_.hasValue())
		this->onSaveAsBudgetFile(*this->filePathName_);
	else
		emit showSaveAsBudgetFile();
}

void Navigator::onSaveAsBudgetFile(const QString& filePathName)
{
	const QString fileName{
		filePathName.endsWith(budgetFileExtention_)
			? filePathName
			: filePathName + budgetFileExtention_
	};
	qInfo() << "User selected the file-name" << fileName << "to store the budget under.";
	BudgetFile bf{fileName};
	bf.store(
		this->budgetBankLedgers_->ledgers(),
		this->budgetBankLedgers_->budget(),
		this->budgetBankLedgers_->budgetLedgers().budgetAccountCodes(),
		this->budgetBankLedgers_->banks(),
		this->budgetBankLedgers_->bankLedgers().bankAccountTypes()
	);
	this->filePathName_ = filePathName;
	this->isDirty_.flipOff();
	emit savedBudgetFile(bf.name());
}

void Navigator::onReloadBudgetFile()
{
	qInfo() << "Reloading the current budget file.";

	if ( ! this->filePathName_.hasValue())
	{
		emit showNoBudgetFileOpened();
		return;
	}
	const auto filePathName{*this->filePathName_};
	this->onConfirmedNewBudgetFile();
	this->onConfirmedLoadBudgetFile(filePathName, true);
}

void Navigator::onCheckIsBudgetFileDirty()
{
	emit checkedIsBudgetFileDirty(this->isDirty_);
}

void Navigator::onPrepareShowDashboard()
{
	const auto preferred{this->preferences_.preferredCurrency()};
	const BudgetTotals bt{
		this->budgetBankLedgers_->budget(),
		preferred,
		EventFrequency::Monthly,
		this->preferences_.hoursInWeek(),
		this->preferences_.daysInWeek()
	};
	const auto dueWithin{this->preferences_.notifyDueWithin()};
	const auto due{budgetItemsDue(dueWithin, this->budgetBankLedgers_->budget())};
	const BudgetHealth bh{bt};
	emit showDashboard(bt, due, bh, dueWithin, preferred);
}

void Navigator::onPrepareShowExchangeRates()
{
	emit showExchangeRates(
		CurrencyConverter::conversions(),
		this->preferences_.usableCurrencies()
	);
}

void Navigator::onFetchExchangeRates(const set<Iso4217Codes>& currencies)
{
	Q_ASSERT(this->exchangeSource_ != nullptr);
	CurrencyConverter cc{*this->exchangeSource_};
	const auto conversions{cc.fetchAll(currencies)};
	emit fetchedExchangeRates(currencies, conversions);
}

void Navigator::onStoreExchangeRates(const ConversionMap& conversions)
{
	qInfo() << "Storing exchange rates.";

	for (const auto& conversion : conversions)
		for (const auto& toRatio : conversion.second)
			CurrencyConverter::setConversion(conversion.first, toRatio.convertTo_, toRatio.ratio_);
	ExchangeRatesFile erf{};
	erf.store(conversions);
}

void Navigator::onPrepareShowConfigure()
{
	const auto budgetCurrencies{containedCurrencies(this->budgetBankLedgers_->budget())};
	const auto ledgerCurrencies{containedCurrencies(this->budgetBankLedgers_->ledgers())};
	set<Iso4217Codes> usedCurrencies;
	usedCurrencies.insert(budgetCurrencies.cbegin(), budgetCurrencies.cend());
	usedCurrencies.insert(ledgerCurrencies.cbegin(), ledgerCurrencies.cend());
	emit showConfigure(
		this->preferences_.hoursInWeek(),
		this->preferences_.daysInWeek(),
		this->preferences_.notifyDueWithin(),
		this->preferences_.preferredCurrency(),
		this->preferences_.usableCurrencies(),
		usedCurrencies
	);
}

void Navigator::onConfigureUpdated(
	const hours& hrInWeek,
	const Days& dayInWeek,
	const Days& notifyDueWithin,
	const Iso4217Codes& preferred,
	const set<Iso4217Codes>& usable
)
try
{
	qInfo() << "Updating user preferences.";

	if (hrInWeek < hours{minimumHoursInAWeek} || hrInWeek >= hours{maximumHoursInAWeek})
		throw Error{
			QObject::tr(
				"The number of work hours in a week must be between %1 and %2."
			).arg(minimumHoursInAWeek).arg(maximumHoursInAWeek)
		};

	if (dayInWeek < Days{minimumDaysInAWeek} || dayInWeek > Days{maximumDaysInAWeek})
		throw Error{
			QObject::tr(
				"The number of work days in a week must be between %1 and %2."
			).arg(minimumDaysInAWeek).arg(maximumDaysInAWeek)
		};
	bool missingUsedCode{false};
	QStringList missing;
	const auto budgetCurrencies{containedCurrencies(this->budgetBankLedgers_->budget())};
	const auto ledgerCurrencies{containedCurrencies(this->budgetBankLedgers_->ledgers())};
	set<Iso4217Codes> usedCurrencies;
	usedCurrencies.insert(budgetCurrencies.cbegin(), budgetCurrencies.cend());
	usedCurrencies.insert(ledgerCurrencies.cbegin(), ledgerCurrencies.cend());

	for (const auto& usedCode : usedCurrencies)
	{
		if (usable.count(usedCode) == 0)
		{
			missingUsedCode = true;
			missing << QString::fromStdString(toStdString(usedCode));
		}
	}

	if (missingUsedCode)
	{
		missing << QString::fromStdString(toStdString(preferred));
		throw Error{
			QObject::tr(
				"The usable currencies must contain '%1', as they are currently in use in the "
					"application."
			).arg(missing.join("', '"))
		};
	}

	if (
		notifyDueWithin < Days{minimumDaysNotifyWithin}
		|| notifyDueWithin > Days{maximumDaysNotifyWithin}
	)
		throw Error{
			QObject::tr(
				"The number of days to notify within must be between %1 and %2."
			).arg(minimumDaysNotifyWithin).arg(maximumDaysNotifyWithin)
		};
	this->preferences_.setHoursInWeek(hrInWeek);
	this->preferences_.setDaysInWeek(dayInWeek);
	this->preferences_.setNotifyDueWithin(notifyDueWithin);
	this->preferences_.setPreferredCurrency(preferred);
	this->preferences_.setUsableCurrencies(usable);
	emit configureSaveSucceeded();
}
catch (const exception& error)
{
	emit configureSaveFailed(error.what());
}

Navigator::Navigator() :
	QObject{nullptr},
	filePathName_{},
	isDirty_{},
	budgetBankLedgers_{make_unique<BudgetBankLedgers>()},
	preferences_{},
	networkManager_{},
	exchangeSource_{
		makeQtPtr<OpenExchangeRatesSource>(
			this->networkManager_,
			DuxReiNummariae_OPEN_EXCHANGE_RATES_API_KEY,
			this
		).observed()
	}
{
	const auto systemLocale{QLocale::system().name()};
	qDebug() << "User system locale" << systemLocale;
	setUpCurrency(
		[this] (const Iso4217Codes& from, const Iso4217Codes& to)
		{
			Q_ASSERT(this->exchangeSource_ != nullptr);
			const CurrencyConverter cc{*this->exchangeSource_};
			return cc.lookUp(from, to);
		},
		systemLocale.toStdString()
	);

	ExchangeRatesFile erf{};
	try
	{
		if (erf.exists())
			CurrencyConverter::initialise(erf.load());
	}
	catch (const exception& error)
	{
		qWarning() << "Failed to initialise the currency conversions." << error.what();
		erf.remove();
	}
}

Navigator::~Navigator() = default;
