/*******************************************************************************
***  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 "PostBudgetItemsWidget.h"
using pecunia::currency::Iso4217Codes;
using ::QWidget;
using std::exception;
using std::function;
using std::set;
using std::vector;
using drn::banking::BankName;
using drn::budgeting::BudgetSource;
using drn::desktop_ui::internal::PostBudgetItemsWidget;
using drn::foundation::ObserverPtr;
using drn::foundation::Optional;

#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <QAbstractButton>
using ::QAbstractButton;
#include <QDate>
using ::QDate;
#include <QDialogButtonBox>
using ::QDialogButtonBox;
#include <QDebug>
// using qDebug.
#include <QPushButton>
using ::QPushButton;

#include <stdexcept>
using std::logic_error;

#include <accounting/AccountCode.h>
using drn::accounting::AccountCode;
#include <accounting/Transaction.h>
using drn::accounting::TransactionNumber;
#include <banking/BankAccountType.h>
using drn::banking::BankAccountType;
#include <budgeting/BudgetItemDue.h>
using drn::budgeting::BudgetItemDue;
#include <budgeting/BudgetItemIdentifier.h>
using drn::budgeting::BudgetItemIdentifier;
#include <budgeting/BudgetItemTypes.h>
using drn::budgeting::BudgetItemTypes;
#include "PostBudgetItemEntryWidget.h"
using drn::desktop_ui::internal::BudgetItemDueAction;
using drn::desktop_ui::internal::PostBudgetItemEntryWidget;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/QtMemory.hpp>
using drn::foundation::makeQtPtr;


Optional<ObserverPtr<PostBudgetItemEntryWidget>> PostBudgetItemsWidget::findPostBudgetItem(
	const function<bool(const PostBudgetItemEntryWidget&)>& isMatch
)
{
	for (const auto& entry : this->findChildren<PostBudgetItemEntryWidget*>())
		try
		{
			if (isMatch(*entry))
				return {entry};
		}
		catch (const exception& error)
		{
			qDebug() << "An error occurred while checking a post budget item entry widget."
					<< error.what();
			continue;
		}
	return {};
}

Optional<ObserverPtr<const PostBudgetItemEntryWidget>> PostBudgetItemsWidget::findPostBudgetItem(
	const function<bool(const PostBudgetItemEntryWidget&)>& isMatch
) const
{
	for (const auto& entry : this->findChildren<PostBudgetItemEntryWidget*>())
		try
		{
			if (isMatch(*entry))
				return {entry};
		}
		catch (const exception& error)
		{
			qDebug() << "An error occurred while checking a post budget item entry widget."
					<< error.what();
			continue;
		}
	return {};
}

void PostBudgetItemsWidget::add(const Optional<BudgetItemDue>& item)
{
	qDebug() << "Adding posting budget item entry:" << item.valueOrDefault().id_.source_.name();
	auto pbiw{
		makeQtPtr<PostBudgetItemEntryWidget>(
			item,
			this->banks_,
			this->usableCurrencies_,
			this
		)
	};
	connect(
		pbiw.observed(),
		&PostBudgetItemEntryWidget::lookUpBankAccounts,
		this,
		&PostBudgetItemsWidget::lookUpBankAccounts
	);
	connect(
		pbiw.observed(),
		&PostBudgetItemEntryWidget::lookUpBudgetItemDueSources,
		this,
		&PostBudgetItemsWidget::lookUpBudgetItemDueSources
	);
	connect(
		pbiw.observed(),
		&PostBudgetItemEntryWidget::amountChanged,
		this,
		&PostBudgetItemsWidget::updateTotals
	);
	connect(
		pbiw.observed(),
		&PostBudgetItemEntryWidget::actionChanged,
		this,
		&PostBudgetItemsWidget::updateTotals
	);
	this->postBudgetItemEntriesVbxlyt->insertWidget(
		this->postBudgetItemEntriesVbxlyt->indexOf(this->postBudgetItemEntriesBottomSpcr),
		pbiw.observed()
	);
}

void PostBudgetItemsWidget::processBudgetItemsDue()
{
	qDebug() << "Processing budget items due";

	for (const auto& entry : this->findChildren<PostBudgetItemEntryWidget*>())
		try
		{
			if ( ! entry->isDirty())
			{
				qDebug() << "Not dirty budget item due" << entry->itemDue().name();
				continue;
			}

			switch (entry->dueAction())
			{
			case BudgetItemDueAction::Post:
				qDebug() << "Posting budget item due" << entry->itemDue().name();

				if (entry->isUnexpectedItem())
					emit postedUnexpected(
						{entry->itemType(), entry->itemDue()},
						entry->amount(),
						entry->accountCode().valueOrDefault(),
						entry->postOn()
					);
				else
					emit posted(
						BudgetItemDue{
							{entry->itemType(), entry->itemDue()},
							entry->amount(),
							entry->dueOn()
						},
						entry->accountCode().valueOrDefault(),
						entry->postOn()
					);
				continue;
			case BudgetItemDueAction::Skip:
				qDebug() << "Skipping budget item due" << entry->itemDue().name();

				// Checking to see if a source is selected when skipping to give the user a chance
				// to change to post should that have been what was truly intended.
				if (entry->bankName().hasValue())
				{
					this->onSaveFailed(
						{entry->itemType(), entry->itemDue()},
						tr(
							"Notice: the budget item due is marked to skip, but it had a bank "
							"source selected, the source was cleared. Clicking 'save' again will "
							"confirm to skip this item."
						)
					);
					entry->clearBankName();
					continue;
				}
				emit skipped({entry->itemType(), entry->itemDue()});
				continue;
			case BudgetItemDueAction::Ignore:
				qDebug() << "Ignoring budget item due" << entry->itemDue().name();
				continue;
			case BudgetItemDueAction::Unknown:
				qWarning() << "Post budget item entry has an unknown action!"
						<< entry->itemDue().name();
				this->onSaveFailed(
					{entry->itemType(), entry->itemDue()},
					tr("Budget item entry has an unknown action!")
				);
				continue;
			default:
				throw logic_error{"Unhandled budget item due action while posting a budget item."};
			}
		}
		catch (const exception& error)
		{
			this->onSaveFailed({entry->itemType(), entry->itemDue()}, error.what());
		}

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

bool PostBudgetItemsWidget::hasDirtyEntry() const
{
	const auto postBudgetItem{
		this->findPostBudgetItem(
			[] (const auto& entry)
			{
				return entry.isDirty();
			}
		)
	};
	return postBudgetItem.hasValue();
}

void PostBudgetItemsWidget::onClicked(ObserverPtr<QAbstractButton> button)
{
	qDebug() << "Handling bills button" << button->text();

	if (this->saveDiscardBttnbx->button(QDialogButtonBox::Discard) == button)
		this->close();
	else if (this->saveDiscardBttnbx->button(QDialogButtonBox::Save) == button)
		this->processBudgetItemsDue();
	else if (this->addPshbttn_.observed() == button)
		this->add({});
	else
		throw logic_error{"Unknown button pressed, " + button->text().toStdString() + '.'};
}

void PostBudgetItemsWidget::updateTotals()
{
	qDebug() << "Updating total post amount.";
	Money total{this->totalMnyntrydgt->code()};

	for (const auto& entry : this->findChildren<PostBudgetItemEntryWidget*>())
		if (BudgetItemDueAction::Post == entry->dueAction())
			try
			{
				const auto amount{entry->amount()};

				if (Iso4217Codes::XXX != amount.code())
					total += amount;
			}
			catch (const exception& error)
			{
				qWarning() << "Failure while calculating the total posted." << error.what();
			}
	this->totalMnyntrydgt->setMoney(total);
}

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

void PostBudgetItemsWidget::onLookedUpBankAccounts(
	const BankName& bn,
	const set<BankAccountType>& accountTypes
)
{
	qDebug() << "Found" << accountTypes.size() << "account codes for the bank" << bn.institution();
	auto postBudgetItem{
		this->findPostBudgetItem(
			[] (const auto& entry)
			{
				return entry.hasRequestedAccounts();
			}
		)
	};

	if ( ! postBudgetItem.hasValue())
	{
		qFatal("Failed to locate the post budget item that requested the bank account look-up.");
		return;
	}
	(*postBudgetItem)->setAccounts(accountTypes);
}

void PostBudgetItemsWidget::onLookUpBudgetItemDueSources(
	const BudgetItemIdentifier& requestedBy,
	const BudgetItemTypes& bit,
	set<BudgetSource> sources
)
{
	qDebug() << "Found" << sources.size() << "budget item sources for the type"
			<< presentationText(bit);
	auto postBudgetItem{
		this->findPostBudgetItem(
			[&requestedBy] (const auto& entry)
			{
				return entry.itemType() == requestedBy.type_
					&& entry.itemDue() == requestedBy.source_;
			}
		)
	};

	if ( ! postBudgetItem.hasValue())
	{
		qFatal(
			"Failed to locate the post budget item that requested the budget item sources look-up."
		);
		return;
	}

	// Removing the budget item due sources already currently displayed.
	for (const auto& entry : this->findChildren<PostBudgetItemEntryWidget*>())
		if (entry->itemType() == bit)
			sources.erase(entry->itemDue());
	(*postBudgetItem)->setItemDueSources(sources);
}

void PostBudgetItemsWidget::onSaveSucceeded(
	const BudgetItemIdentifier& bii,
	const Optional<TransactionNumber>& txnNum
)
{
	qDebug() << "Posting budget item" << presentationText(bii) << "succeeded.";
	auto postBudgetItem{
		this->findPostBudgetItem(
			[&bii] (const auto& entry)
			{
				return bii.type_ == entry.itemType() && bii.source_ == entry.itemDue();
			}
		)
	};

	if ( ! postBudgetItem.hasValue())
	{
		qFatal("Failed to locate the post budget item that failed to be saved.");
		return;
	}

	if (txnNum.hasValue())
		(*postBudgetItem)->setPosted(*txnNum);
	else
		(*postBudgetItem)->setSkipped();
	(*postBudgetItem)->saved();

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

void PostBudgetItemsWidget::onSaveFailed(
	const BudgetItemIdentifier& bii,
	const QString& errorMessage
)
{
	qDebug() << "Posting budget item" << presentationText(bii) << "failed.";
	auto postBudgetItem{
		this->findPostBudgetItem(
			[&bii] (const auto& entry)
			{
				return bii.type_ == entry.itemType() && bii.source_ == entry.itemDue();
			}
		)
	};

	if ( ! postBudgetItem.hasValue())
	{
		qFatal("Failed to locate the post budget item that failed to be saved.");
		return;
	}
	(*postBudgetItem)->setErrorMessage(errorMessage);
}

PostBudgetItemsWidget::PostBudgetItemsWidget(
	const qint32& dueWithin,
	const vector<BudgetItemDue>& itemsDue,
	const set<BankName>& banks,
	const Iso4217Codes& preferred,
	const set<Iso4217Codes>& usableCurrencies,
	const ObserverPtr<QWidget>& parent
) :
	QWidget{parent.observed()},
	banks_{banks},
	usableCurrencies_{usableCurrencies},
	addPshbttn_{nullptr}
{
	this->setAttribute(Qt::WA_DeleteOnClose);
	this->setupUi(this);
	this->dueWithinLbl->setText(tr("Budget Items Due Within %1 Days:").arg(dueWithin));
	this->totalMnyntrydgt->setUsableCodes(this->usableCurrencies_);
	this->totalMnyntrydgt->setMoney(Money{preferred});
	this->totalMnyntrydgt->setLabel(tr("Posted Total:"));

	for (const auto& itemDue : itemsDue)
		this->add(itemDue);

	//{ Set-up Add Button

	connect(
		this->saveDiscardBttnbx,
		&QDialogButtonBox::clicked,
		this,
		&PostBudgetItemsWidget::onClicked
	);
	this->addPshbttn_ = this->saveDiscardBttnbx->addButton(
		tr("&Add"),
		QDialogButtonBox::ButtonRole::ActionRole
	);

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

	//}
}
