/*******************************************************************************
***  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 "BanksWidget.h"
using ::QWidget;
using std::function;
using std::map;
using std::move;
using std::set;
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
using drn::desktop_ui::internal::BanksWidget;
using drn::foundation::ObserverPtr;
using drn::foundation::Optional;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <QAbstractButton>
using ::QAbstractButton;
#include <QDialogButtonBox>
using ::QDialogButtonBox;
#include <QPushButton>
using ::QPushButton;
#include <QString>
using ::QString;
#include <QtDebug>
// using qDebug;
#include <QTimer>
using ::QTimer;
#include <QTreeWidgetItem>
using ::QTreeWidgetItem;
#include <QTreeWidgetItemIterator>
using ::QTreeWidgetItemIterator;

#include <algorithm>
using std::all_of;
using std::find_if;
using std::transform;
#include <chrono>
using namespace std::chrono_literals;
#include <exception>
using std::exception;
#include <iterator>
using std::inserter;
#include <vector>
using std::vector;

#include "AccountEntryWidget.h"
using drn::desktop_ui::internal::AccountEntryWidget;
#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::AccountTypes;
#include "BankEntryWidget.h"
using drn::desktop_ui::internal::BankEntryWidget;
#include <banking/BankName.h>
using drn::banking::Bank;
using drn::banking::BankName;
#include <banking/BankAccountType.h>
using drn::banking::BankAccountType;
using drn::banking::SupportedAccountTypes;
#include <banking/BankMap.h>
using drn::banking::BankMap;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/QtMemory.hpp>
using drn::foundation::makeQtPtr;


namespace
{

constexpr const auto bankAccountColumn{0};

}

Optional<ObserverPtr<QTreeWidgetItem>> BanksWidget::findTreeItem(
	const function<bool(const QString&)>& isItem
)
{
	QTreeWidgetItemIterator item{this->bankAndAccountsTrwdgt};

	while (*item != nullptr)
	{
		if (isItem((*item)->text(bankAccountColumn)))
			return {*item};
		++item;
	}
	return {};
}

void BanksWidget::add(const Bank& b)
{
	qDebug() << "Adding bank" << b.name_.institution();
	auto item{makeQtPtr<QTreeWidgetItem>(this->bankAndAccountsTrwdgt)};
	item->setText(
		bankAccountColumn,
		b.name_.isEmpty()
			? BankEntryWidget::newBankNameText()
			: b.name_.institution()
	);
	auto widget{
		makeQtPtr<BankEntryWidget>(b.name_.isEmpty() ? Optional<Bank>{} : b, this)
	};
	this->entriesVbxlyt->insertWidget(0, widget.observed());
	widget->hide();
	this->banks_[item] = widget;
	this->bankAndAccountsTrwdgt->setCurrentItem(item.observed());
}

void BanksWidget::add(BankName bn, Account a, const SupportedAccountTypes& sat)
{
	qDebug() << "Adding bank account" << bn.institution() << a.code().name();
	auto bankItem{
		this->findTreeItem(
			[institution{bn.institution()}] (const auto& text)
			{
				return institution == text;
			}
		)
	};

	if ( ! bankItem.hasValue())
	{
		qFatal("Failed to locate the bank widget item that is to hold the account.");
		return;
	}
	// Confirm that the found item is a bank.
	Q_ASSERT(this->banks_.has(*bankItem));
	const bool hasAccount{a.code().number().integer() != AccountNumber::invalid_};
	this->banks_[*bankItem]->markDirty();
	auto item{makeQtPtr<QTreeWidgetItem>((*bankItem).observed())};
	item->setText(
		bankAccountColumn,
		hasAccount
			? presentationText(a.code())
			: AccountEntryWidget::newAccountCodeText()
	);
	auto widget{
		makeQtPtr<AccountEntryWidget>(
			hasAccount
				? a.code()
				: Optional<AccountCode>{},
			a.isClosed(),
			Optional<AccountTypes>{},
			a.balance(),
			this->accountNumbers_,
			this->usableCurrencies_,
			sat,
			this
		)
	};
	this->entriesVbxlyt->insertWidget(0, widget.observed());
	widget->hide();
	this->accounts_[item] = widget;
	this->bankAndAccountsTrwdgt->setCurrentItem(item.observed());
}

void BanksWidget::add(const AccountCode& parent, Account a)
{
	auto accountItem{
		this->findTreeItem(
			[parentText{presentationText(parent)}] (const auto& text)
			{
				return parentText == text;
			}
		)
	};

	if ( ! accountItem.hasValue())
	{
		qFatal("Failed to locate the account widget item that is to hold the account.");
		return;
	}
	// Confirm that the found item is an account.
	Q_ASSERT(this->accounts_.has(*accountItem));
	const bool hasAccount{a.code().number().integer() != AccountNumber::invalid_};
	auto item{makeQtPtr<QTreeWidgetItem>((*accountItem).observed())};
	item->setText(
		bankAccountColumn,
		hasAccount
			? presentationText(a.code())
			: AccountEntryWidget::newAccountCodeText()
	);
	auto widget{
		makeQtPtr<AccountEntryWidget>(
			hasAccount ? a.code() : Optional<AccountCode>{},
			a.isClosed(),
			a.type(),
			a.balance(),
			this->accountNumbers_,
			this->usableCurrencies_,
			this
		)
	};
	this->entriesVbxlyt->insertWidget(0, widget.observed());
	widget->setEditable(false);
	widget->hide();
	this->accounts_[item] = widget;
	this->bankAndAccountsTrwdgt->setCurrentItem(item.observed());
}

BankName BanksWidget::currentSelectedBankName() const
{
	const auto item{this->bankAndAccountsTrwdgt->currentItem()};

	// Test if the current item is a bank.
	if (this->banks_.has(item))
		return BankName{item->text(bankAccountColumn)};
	const auto parent{item->parent()};

	// Test if the current item is a bank account.
	if (this->banks_.has(parent))
		return BankName{parent->text(bankAccountColumn)};
	// The current item must be a general account.
	Q_ASSERT(parent->parent() != nullptr);
	return BankName{parent->parent()->text(bankAccountColumn)};
}

bool BanksWidget::isAnyItemDirty() const
{
	for (const auto& nameBank : this->banks_)
		if (nameBank.second->isDirty())
			return true;

	for (const auto& codeAccount : this->accounts_)
		if (codeAccount.second->isDirty())
			return true;
	return false;
}

void BanksWidget::saveAccounts()
{
	this->bankAndAccountsTrwdgt->setCurrentIndex({});
	qDebug() << "Starting processing bank account widgets.";
	// Use to remove widgets that are new while inside the loop.
	vector<ObserverPtr<QTreeWidgetItem>> toRemove;

	for (auto& codeAccount : this->accounts_)
	{
		auto& item{codeAccount.first};
		const auto& code{item->text(bankAccountColumn)};
		auto& entry{codeAccount.second};

		try
		{
			if ( ! entry->isDirty())
			{
				qDebug() << "No Changes to Account:" << code;
				continue;
			}
			const auto accountType{entry->bankAccountType()};
			const auto originalCode{entry->original()};
			const bool isNew{ ! entry->original().hasValue()};

			if (entry->isRemoved())
			{
				qDebug() << "Removing Account:" << code;

				if (isNew)
					toRemove.emplace_back(item);
				else
					emit removedAccount(*originalCode);
				continue;
			}

			if (isNew)
			{
				qDebug() << "Adding New Account" << code;
				emit addedAccount(
					entry->name(),
					entry->bankAccountType().valueOr(SupportedAccountTypes::Unknown),
					entry->balance()
				);
			}
			else
			{
				// Only the bank accounts should be editable.
				Q_ASSERT(accountType.hasValue());

				if (entry->name() != originalCode->name())
				{
					qDebug() << "Renaming Account" << originalCode->name() << "to" << entry->name();
					emit renamedAccount(
						*originalCode,
						AccountCode{
							originalCode->number(),
							entry->name(),
							originalCode->parent().hasValue()
								? *originalCode->parent()
								: Optional<AccountNumber>{}
						}
					);
				}
				else // User must have undone what made the item dirty.
					this->onAccountSaveSucceeded(
						AccountCode{
							originalCode->number(),
							entry->name(),
							originalCode->parent().hasValue()
								? *originalCode->parent()
								: Optional<AccountNumber>{}
						}
					);
			}
		}
		catch (const exception& error)
		{
			qWarning() << "Account entry save error for" << code << error.what();
			this->displayError(item, error.what());
		}
	}

	for (const auto& remove : toRemove)
		this->remove(remove);
	qDebug() << "Processed all bank accounts.";

	if (this->areAllAccountsSaved())
		QTimer::singleShot(
			100ms,
			this,
			[this] ()
			{
				this->saveBanks();
			}
		);
}

void BanksWidget::saveBanks()
{
	this->bankAndAccountsTrwdgt->setCurrentIndex({});
	qDebug() << "Starting processing bank widgets.";
	// Use to remove widgets that are new while inside the loop.
	vector<ObserverPtr<QTreeWidgetItem>> toRemove;

	for (auto& nameBank : this->banks_)
	{
		auto& item{nameBank.first};
		const auto& nameText{item->text(bankAccountColumn)};
		auto& entry{nameBank.second};

		try
		{
			if ( ! entry->isDirty())
			{
				qDebug() << "No Changes to Bank:" << nameText;
				continue;
			}
			const bool isNew{ ! entry->original().hasValue()};

			if (entry->isRemoved())
			{
				qDebug() << "Removing Account:" << nameText;

				if (isNew)
					toRemove.emplace_back(item);
				else
					emit removed(BankName{entry->name()});
				continue;
			}
			const BankName name{entry->name()};
			const auto accountNumbersBankTypes{
				[this, &item] ()
				{
					map<AccountNumber, SupportedAccountTypes> accountTypes;

					for (const auto& codeAccount : this->accounts_)
						if (codeAccount.first->parent() == item)
							accountTypes.emplace(
								codeAccount.second->number(),
								codeAccount.second->bankAccountType().valueOr(
									SupportedAccountTypes::Unknown
								)
							);
					return accountTypes;
				}()
			};

			if (isNew)
			{
				qDebug() << "Adding Bank:" << nameText;
				emit added(
					Bank{name, this->bankAccountNumbers(item), entry->isClosed()},
					accountNumbersBankTypes
				);
			}
			else
			{
				qDebug() << "Modifying Bank:" << nameText;

				if ( ! isNew && entry->original()->name_ != name)
				{
					qDebug() << "Renaming Bank from" << entry->original()->name_.institution()
							<< "to" << nameText;
					emit rename(entry->original()->name_, name);
				}
				const bool isNowClosed{entry->isClosed()};
				const bool wasOriginallyClosed{entry->original()->isClosed()};

				if ( ! wasOriginallyClosed && isNowClosed)
				{
					qDebug() << "Closing Bank:" << nameText;
					emit closed(name);
					continue;
				}

				if (wasOriginallyClosed && ! isNowClosed)
				{
					qDebug() << "Reopening Bank:" << nameText;
					emit opened(name);
				}
				emit updated(
					Bank{name, this->bankAccountNumbers(item)},
					accountNumbersBankTypes
				);
			}
		}
		catch (const exception& error)
		{
			qWarning() << "Bank entry save error for" << nameText << error.what();
			this->displayError(item, error.what());
		}
	}
	qDebug() << "Processing accounts.";

	for (const auto& remove : toRemove)
		this->remove(remove);

	if ( ! this->isAnyItemDirty())
		this->close();
}

void BanksWidget::displayError(
	const ObserverPtr<QTreeWidgetItem>& item,
	const QString& errorMessage
)
{
	item->setBackground(bankAccountColumn, Qt::red);
	this->bankAndAccountsTrwdgt->setCurrentItem(item.observed());

	if (this->accounts_.has(item))
		this->accounts_[item]->setErrorMessage(errorMessage);
	else if (this->banks_.has(item))
		this->banks_[item]->setErrorMessage(errorMessage);
	else
		throw Error{
			tr(
				"Failed to locate neither the account nor the bank widget entry to display "
					"the error."
			)
		};
}

void BanksWidget::remove(const ObserverPtr<QTreeWidgetItem>& item)
{
	if (this->accounts_.has(item))
	{
		auto& entry{this->accounts_[item]};
		// N.B. resetting dirty flag now as it's being removed  so "handled".
		entry->resetDirtyFlag();
		entry->deleteLater();
		this->accounts_.erase(item);
	}
	else if (this->banks_.has(item))
	{
		auto& entry{this->banks_[item]};
		// N.B. resetting dirty flag now as it's being removed  so "handled".
		entry->resetDirtyFlag();
		entry->deleteLater();
		this->banks_.erase(item);
	}
	else
		throw Error{
			tr("Failed to locate neither the account nor the bank widget entry to remove.")
		};
	this->bankAndAccountsTrwdgt->removeItemWidget(item.observed(), bankAccountColumn);
}

bool BanksWidget::areAllAccountsSaved() const
{
	return all_of(
		this->accounts_.begin(),
		this->accounts_.end(),
		[] (const auto& kv)
		{
			return ! kv.second->isDirty();
		}
	);
}

set<AccountNumber> BanksWidget::bankAccountNumbers(const ObserverPtr<QTreeWidgetItem>& item) const
{
	set<AccountNumber> accountNumbers;

	for (const auto& codeAccount : this->accounts_)
		if (codeAccount.first->parent() == item)
			accountNumbers.emplace(codeAccount.second->number());
	return accountNumbers;
}

void BanksWidget::onClicked(const ObserverPtr<QAbstractButton>& button)
{
	qDebug() << "Handling bank entry widget clicked button" << button->text();

	if (this->saveDiscardBttnbx->button(QDialogButtonBox::Discard) == button)
		this->close();
	else if (this->saveDiscardBttnbx->button(QDialogButtonBox::Save) == button)
		this->saveAccounts();
	else if (this->addBankPshbttn_.observed() == button)
		this->add(Bank{});
	else if (this->addAccountPshbttn_.observed() == button)
		this->add(this->currentSelectedBankName(), Account{}, SupportedAccountTypes::Unknown);
	else
		Q_ASSERT(false && "Unknown button pressed.");
}

void BanksWidget::onCurrentItemChanged(
	ObserverPtr<QTreeWidgetItem> current,
	ObserverPtr<QTreeWidgetItem> previous
)
{
	if ((nullptr == current) && (nullptr == previous))
		qDebug() << "Changing displayed bank from nothing to nothing.";
	else if ((current != nullptr) && (nullptr == previous))
		qDebug() << "Changing displayed bank from nothing to" << current->text(bankAccountColumn);
	else if ((nullptr == current) && (previous != nullptr))
		qDebug() << "Changing displayed bank from" << previous->text(bankAccountColumn)
				<< "to nothing.";
	else
		qDebug() << "Changing displayed bank from" << previous->text(bankAccountColumn) << "to"
			<< current->text(bankAccountColumn);

	if (previous != nullptr)
	{
		if (this->banks_.has(previous))
		{
			auto bank{this->banks_.at(previous)};
			bank->hide();

			if (bank->name().isEmpty())
				previous->setText(bankAccountColumn, BankEntryWidget::newBankNameText());
			else
				previous->setText(bankAccountColumn, bank->name());
		}
		else if (this->accounts_.has(previous))
		{
			auto account{this->accounts_.at(previous)};
			account->hide();

			if (account->accountCodeText().isEmpty())
				previous->setText(bankAccountColumn, AccountEntryWidget::newAccountCodeText());
			else
				previous->setText(bankAccountColumn, account->accountCodeText());
		}
		else
		{
			qFatal("Failed to locate the widget item that was previously selected.");
			return;
		}
	}

	if (current != nullptr)
	{
		if (this->banks_.has(current))
		{
			this->banks_.at(current)->show();
			this->selectedType_ = SelectedItemTypes::Bank;
		}
		else if (this->accounts_.has(current))
		{
			auto account{this->accounts_.at(current)};
			account->show();
			account->bankAccountType().hasValue()
				? this->selectedType_ = SelectedItemTypes::BankAccount
				: this->selectedType_ = SelectedItemTypes::Account;
		}
		else
			this->selectedType_ = SelectedItemTypes::Unknown;
	}

	switch (this->selectedType_)
	{
	case SelectedItemTypes::Account:
	case SelectedItemTypes::Unknown:
		this->addBankPshbttn_->setEnabled(false);
		this->addAccountPshbttn_->setEnabled(false);
	break;
	case SelectedItemTypes::Bank:
		this->addBankPshbttn_->setEnabled(true);
		this->addAccountPshbttn_->setEnabled(true);
	break;
	case SelectedItemTypes::BankAccount:
		this->addBankPshbttn_->setEnabled(true);
		this->addAccountPshbttn_->setEnabled(false);
	break;
	default:
		Q_ASSERT(false && "Should never be able to have an unknown selection.");
	};
}

void BanksWidget::onSaveSucceeded(const BankName& bn)
{
	qDebug() << "Successfully Saved Bank" << bn.institution();
	auto item{
		this->findTreeItem(
			[&bn] (const auto& text)
			{
				return text == bn.institution();
			}
		)
	};

	if ( ! item.hasValue())
	{
		qFatal("Failed to locate the bank that saved successfully.");
		return;
	}
	auto entry{this->banks_[*item]};
	(*item)->setBackground(bankAccountColumn, Qt::green);

	if (entry->isRemoved())
		this->remove(*item);
	else
		entry->saved(this->bankAccountNumbers(*item));

	if ( ! this->isAnyItemDirty())
		this->close();
}

void BanksWidget::onSaveFailed(
	const Optional<BankName>& original,
	const QString& errorMessage,
	const Optional<BankName>& displayed
)
{
	qCritical() << "Bank entry save error for" << original.valueOrDefault().institution()
			<< displayed.valueOrDefault().institution() << errorMessage;
	auto item{
		this->findTreeItem(
			[&displayed, &original] (const auto& text)
			{
				return text == displayed.valueOr(original.valueOrDefault()).institution();
			}
		)
	};

	if ( ! item.hasValue())
	{
		qFatal("Failed to locate the bank whose save failed.");
		return;
	}
	this->displayError(*item, errorMessage);
}

bool BanksWidget::close()
{
	emit closing();
	return QWidget::close();
}

void BanksWidget::onAccountSaveSucceeded(AccountCode ac)
{
	qDebug() << "Successfully saved account to ledger." << ac.name();
	auto item{
		this->findTreeItem(
			[&ac] (const auto& text)
			{
				// New bank accounts will have a new code number and the code name is the only thing
				// that can be changed in an existing bank account.
				return text == presentationText(ac)
						|| text.endsWith(AccountCode::separator_ + ac.name());
			}
		)
	};

	if ( ! item.hasValue())
	{
		qFatal("Failed to locate the account that saved successfully.");
		return;
	}
	auto entry{this->accounts_[*item]};
	(*item)->setBackground(bankAccountColumn, Qt::green);

	if (entry->isRemoved())
		this->remove(*item);
	else
		entry->saved(std::move(ac));

	if (this->areAllAccountsSaved())
		QTimer::singleShot(
			100ms,
			this,
			[this] ()
			{
				this->saveBanks();
			}
		);
}

void BanksWidget::onAccountSaveFailed(
	const Optional<AccountCode>& original,
	const QString& errorMessage,
	const Optional<AccountCode>& displayed
)
{
	qWarning() << "Account entry failed to save,"
			<< "Original Code"<< original.valueOrDefault().name()
			<< "Displayed Code" << displayed.valueOrDefault().name()
			<< errorMessage;
	auto item{
		this->findTreeItem(
			[&displayed, &original] (const auto& text)
			{
				// New bank accounts will have a new code number and the code name is the only thing
				// that can be changed in an existing bank account.
				if (original.hasValue())
					return text == presentationText(*original);
				return text.endsWith(AccountCode::separator_ + displayed.valueOrDefault().name());
			}
		)
	};

	if ( ! item.hasValue())
	{
		qFatal("Failed to locate the account whose save failed.");
		return;
	}
	this->displayError(*item, errorMessage);
}

BanksWidget::BanksWidget(
	const BankMap& banks,
	const set<Account>& allAccounts,
	const set<BankAccountType>& accountTypes,
	set<Iso4217Codes> usableCurrencies,
	const ObserverPtr<QWidget> parent
) :
	QWidget{parent.observed()},
	accountNumbers_{},
	usableCurrencies_{std::move(usableCurrencies)},
	addBankPshbttn_{nullptr},
	addAccountPshbttn_{nullptr},
	selectedType_{SelectedItemTypes::Unknown},
	banks_{},
	accounts_{}
{
	this->setAttribute(Qt::WA_DeleteOnClose);
	this->setupUi(this);
	transform(
		allAccounts.begin(),
		allAccounts.end(),
		inserter(this->accountNumbers_, this->accountNumbers_.begin()),
		[] (const auto& account)
		{
			return account.code().number();
		}
	);

	for (const auto& b : banks)
	{
		const auto& bank{b.second};
		this->add(bank);

		for (const auto& accountNumber : bank.accounts())
		{
			const auto& account{
				[&allAccounts, &accountNumber] ()
				{
					for (const auto& act : allAccounts)
						if (act.code().number() == accountNumber)
							return act;
					throw Error{
						tr("Failed to locate bank account '%1'.").arg(accountNumber.integer())
					};
				}()
			};
			const auto accountType{
				find_if(
					accountTypes.cbegin(),
					accountTypes.cend(),
					[ac{account.code()}] (const auto& at)
					{
						return ac == at.code_;
					}
				)
			};

			if (accountType == accountTypes.cend())
				throw Error{
					tr(
						"Failed to locate the bank account type of '%1'."
					).arg(accountNumber.integer())
				};

			// Only adding non-goal account types since they will be displayed below their
			// parent account.
			if (accountType->type_ != SupportedAccountTypes::Goal)
				this->add(bank.name_, account, accountType->type_);

			for (const auto& possibleChild : allAccounts)
				if (possibleChild.code().parent() == account.code().number())
					this->add(account.code(), possibleChild);
		}
	}

	if (banks.empty())
		this->add(Bank{});
	this->bankAndAccountsTrwdgt->sortItems(bankAccountColumn, Qt::SortOrder::AscendingOrder);

	//{ Set-up Add (New Bank) Button

	this->addBankPshbttn_ = this->saveDiscardBttnbx->addButton(
		tr("&Add Bank"),
		QDialogButtonBox::ButtonRole::ActionRole
	);

	if (this->addBankPshbttn_ == nullptr)
		throw Error{tr("Failed to create the add bank push button.")};

	//}

	//{ Set-up Add (New Account) Button

	this->addAccountPshbttn_ = this->saveDiscardBttnbx->addButton(
		tr("A&dd Account"),
		QDialogButtonBox::ButtonRole::ActionRole
	);

	if (this->addAccountPshbttn_ == nullptr)
		throw Error{tr("Failed to create the add bank account push button.")};

	//}

	connect(
		this->bankAndAccountsTrwdgt,
		&QTreeWidget::currentItemChanged,
		this,
		&BanksWidget::onCurrentItemChanged
	);
	connect(
		this->saveDiscardBttnbx,
		&QDialogButtonBox::clicked,
		this,
		&BanksWidget::onClicked
	);
	ObserverPtr<QTreeWidgetItem> top{this->bankAndAccountsTrwdgt->topLevelItem(0)};
	this->bankAndAccountsTrwdgt->setCurrentItem(top.observed());
	this->banks_.at(top)->show();
}
