/*******************************************************************************
***  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 "XmlBudgetFile.h"
using std::make_tuple;
using std::move;
using std::tuple;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
using pecunia::currency::toIso4217Code;
using pecunia::currency::toStdString;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <QDebug>
// using qDebug;
#include <QIODevice>
using ::QIODevice;
#include <QObject>
using ::QObject;
#include <QString>
using ::QString;
#include <QXmlStreamReader>
using ::QXmlStreamReader;
#include <QXmlStreamWriter>
using ::QXmlStreamWriter;

#include <algorithm>
using std::find_if;
#include <set>
using std::set;
#include <stdexcept>
using std::exception;
#include <map>
using std::map;
#include <vector>
using std::vector;

#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::toAccountTypes;
#include <accounting/AccountCode.h>
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
#include <accounting/LedgerBooks.h>
using drn::accounting::LedgerAccount;
using drn::accounting::GeneralLedger;
#include <accounting/Transaction.h>
using drn::accounting::creditAbbreviation;
using drn::accounting::debitAbbreviation;
using drn::accounting::presentationText;
using drn::accounting::toTransactionStatuses;
using drn::accounting::Transaction;
using drn::accounting::TransactionNumber;
#include <banking/Bank.h>
using drn::banking::Bank;
#include <banking/BankAccountType.h>
using drn::banking::SupportedAccountTypes;
using drn::banking::presentationText;
using drn::banking::toSupportedAccountTypes;
#include <banking/BankMap.h>
using drn::banking::BankMap;
#include <banking/BankName.h>
using drn::banking::BankName;
#include "BudgetElements.h"
using drn::file_storage::internal::AccountsElement;
using drn::file_storage::internal::ApplicationElement;
using drn::file_storage::internal::BasicElement;
using drn::file_storage::internal::BankElement;
using drn::file_storage::internal::BankAccountElement;
using drn::file_storage::internal::BanksElement;
using drn::file_storage::internal::BillElement;
using drn::file_storage::internal::BillsElement;
using drn::file_storage::internal::DebtElement;
using drn::file_storage::internal::DebtsElement;
using drn::file_storage::internal::DiscretionariesElement;
using drn::file_storage::internal::DiscretionaryElement;
using drn::file_storage::internal::GoalElement;
using drn::file_storage::internal::GoalsElement;
using drn::file_storage::internal::IndexedElement;
using drn::file_storage::internal::LedgerElement;
using drn::file_storage::internal::LedgersElement;
using drn::file_storage::internal::WageElement;
using drn::file_storage::internal::WagesElement;
#include <budgeting/Budget.h>
using drn::budgeting::Budget;
#include <budgeting/BudgetSource.h>
using drn::budgeting::BudgetSource;
#include <budgeting/Nontrack.h>
using drn::budgeting::Nontrack;
#include <budgeting/EventFrequency.h>
using drn::budgeting::presentationText;
using drn::budgeting::toEventFrequency;
#include <budgeting/Bill.h>
using drn::budgeting::Bill;
#include <budgeting/Goal.h>
using drn::budgeting::Goal;
#include <budgeting/Wage.h>
using drn::budgeting::Wage;
#include <budgeting/Debt.h>
using drn::budgeting::Debt;
using drn::budgeting::Percentage;
#include <file-storage/FileStorageErrors.h>
using drn::file_storage::BudgetFileError;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/ObserverPtr.hpp>
using drn::foundation::ObserverPtr;
#include <foundation/Optional.hpp>
using drn::foundation::Optional;
#include <foundation/TypeIndexMap.hpp>
using drn::foundation::TypeIndexMap;


tuple<
	GeneralLedger,
	Budget,
	TypeIndexMap<BudgetSource, AccountCode>,
	BankMap,
	map<BankName, map<AccountNumber, SupportedAccountTypes>>
> drn::file_storage::internal::read(QIODevice& file)
{
	qDebug() << "Reading from budget file.";
	Q_ASSERT(file.isOpen());
	QXmlStreamReader xml{&file};
	ApplicationElement applicationElement{};
	WagesElement wageElements{};
	BillsElement billsElements{};
	DebtsElement debtsElements{};
	GoalsElement goalsElements{};
	DiscretionariesElement nontrackElements{};
	AccountsElement accountsElement{};
	LedgersElement ledgersElement{};
	BanksElement banksElement{};
	const map<QString, ObserverPtr<BasicElement>> readers{
		{applicationElement.tag(), &applicationElement},
		{wageElements.tag(), &wageElements},
		{billsElements.tag(), &billsElements},
		{debtsElements.tag(), &debtsElements},
		{goalsElements.tag(), &goalsElements},
		{nontrackElements.tag(), &nontrackElements},
		{accountsElement.tag(), &accountsElement},
		{ledgersElement.tag(), &ledgersElement},
		{banksElement.tag(), &banksElement}
	};

	while (xml.readNextStartElement())
	{
		const auto elementName{xml.name().toString()};
		if (readers.count(elementName) > 0)
			readers.at(elementName)->read(xml);
		else
			throw BudgetFileError{
				xml.errorString(),
				xml.lineNumber(),
				xml.columnNumber(),
				QObject::tr("An unexpected error top-level element, %1, appeared in the file.")
					.arg(elementName)
			};
	}

	if (xml.hasError())
		throw BudgetFileError{
			xml.errorString(),
			xml.lineNumber(),
			xml.columnNumber(),
			QObject::tr("An unexpected error appeared in the file.")
		};
	Budget b{};

	for (const auto& wageElement : wageElements.wages_)
		try
		{
			b.wages_.add(
				Wage{
					BudgetSource{wageElement.sourceName_},
					Money{
						wageElement.major_,
						wageElement.minor_,
						toIso4217Code(wageElement.currency_.toStdString())
					},
					toEventFrequency(wageElement.period_),
					wageElement.nextOccurOn_
				}
			);
		}
		catch (const exception& error)
		{
			throw BudgetFileError{
				{},
				{},
				{},
				QObject::tr("The budget file contains malformed wage data."),
				error
			};
		}

	for (const auto& billsElement : billsElements.bills_)
		try
		{
			b.bills_.add(
				Bill{
					BudgetSource{billsElement.sourceName_},
					Money{
						billsElement.major_,
						billsElement.minor_,
						toIso4217Code(billsElement.currency_.toStdString())
					},
					toEventFrequency(billsElement.period_),
					billsElement.nextOccurOn_
				}
			);
		}
		catch (const exception& error)
		{
			throw BudgetFileError{
				{},
				{},
				{},
				QObject::tr("The budget file contains malformed bill data."),
				error
			};
		}

	for (const auto& debtElement : debtsElements.debts_)
		try
		{
			b.debts_.add(
				Debt{
					BudgetSource{debtElement.sourceName_},
					Money{
						debtElement.minimumMajor_,
						debtElement.minimumMinor_,
						toIso4217Code(debtElement.currency_.toStdString())
					},
					toEventFrequency(debtElement.period_),
					debtElement.nextOccurOn_,
					Money{
						debtElement.totalMajor_,
						debtElement.totalMinor_,
						toIso4217Code(debtElement.currency_.toStdString())
					},
					Percentage{debtElement.interest_}
				}
			);
		}
		catch (const exception& error)
		{
			throw BudgetFileError{
				{},
				{},
				{},
				QObject::tr("The budget file contains malformed debt data."),
				error
			};
		}

	for (const auto& goalsElement : goalsElements.goals_)
		try
		{
			b.goals_.add(
				Goal{
					BudgetSource{goalsElement.sourceName_},
					Money{
						goalsElement.major_,
						goalsElement.minor_,
						toIso4217Code(goalsElement.currency_.toStdString())
					},
					toEventFrequency(goalsElement.period_),
					goalsElement.nextOccurOn_,
					Money{
						goalsElement.finalMajor_,
						goalsElement.finalMinor_,
						toIso4217Code(goalsElement.currency_.toStdString())
					}
				}
			);
		}
		catch (const exception& error)
		{
			throw BudgetFileError{
				{},
				{},
				{},
				QObject::tr("The budget file contains malformed goal data."),
				error
			};
		}

	for (const auto& discretionaryElement : nontrackElements.discretionaries_)
		try
		{
			b.nontracks_.add(
				Nontrack{
					BudgetSource{discretionaryElement.sourceName_},
					Money{
						discretionaryElement.major_,
						discretionaryElement.minor_,
						toIso4217Code(discretionaryElement.currency_.toStdString())
					},
					toEventFrequency(discretionaryElement.period_),
					discretionaryElement.nextOccurOn_
				}
			);
		}
		catch (const exception& error)
		{
			throw BudgetFileError{
				{},
				{},
				{},
				QObject::tr("The budget file contains malformed nontrack data."),
				error
			};
		}
	map<AccountCode, LedgerAccount> ledgers{};
	// Account ID -> txnNum -> transaction
	map<IndexedElement::IdType, map<TransactionNumber, Transaction>> ledgersTransactions{};
	const auto findAccountName{
		[] (const std::vector<AccountElement>& accounts, const IndexedElement::IdType& id)
		{
			const auto account{
				find_if(
					accounts.cbegin(),
					accounts.cend(),
					[&id] (const auto& element)
					{
						return element.id_ == id;
					}
				)
			};

			if (account == accounts.cend())
				throw BudgetFileError{
					{},
					{},
					{},
					QObject::tr(
						"The budget file contains malformed account/transaction data. Failed to "
							"locate account name for account number %1."
					).arg(id)
				};
			return account->name_;
		}
	};

	for (const auto& ledger : ledgersElement.ledgers_)
		for (const auto& transaction : ledger.transactions_)
		{
			TransactionNumber txnNum{transaction.id_};
			const auto debitingNumber{
				debitAbbreviation() == transaction.entry_
					? transaction.accountId_
					: ledger.id_
			};
			const AccountCode debiting{
				AccountNumber{debitingNumber},
				findAccountName(accountsElement.accounts_, debitingNumber)
			};
			const auto creditingNumber{
				creditAbbreviation() == transaction.entry_
					? transaction.accountId_
					: ledger.id_,
			};
			const AccountCode crediting{
				AccountNumber{creditingNumber},
				findAccountName(accountsElement.accounts_, creditingNumber)
			};
			const Money amount{
				transaction.major_,
				transaction.minor_,
				toIso4217Code(transaction.currency_.toStdString())
			};
			Transaction txn{
				transaction.date_,
				debiting,
				crediting,
				amount,
				transaction.description_.hasValue()
					? transaction.description_
					: Optional<QString>{},
				txnNum,
				toTransactionStatuses(transaction.status_)
			};
			ledgersTransactions[ledger.id_].emplace(move(txnNum), move(txn));
		}

	for (const auto& accountElement : accountsElement.accounts_)
		try
		{
			ledgers.emplace(
				AccountCode{
					AccountNumber{accountElement.id_},
					accountElement.name_,
					accountElement.parent_.hasValue()
						? Optional<AccountNumber>{AccountNumber{*accountElement.parent_}}
						: Optional<AccountNumber>{}
				},
				LedgerAccount{
					Account{
						toAccountTypes(accountElement.type_),
						AccountCode{
							AccountNumber{accountElement.id_},
							accountElement.name_,
							accountElement.parent_.hasValue()
								? Optional<AccountNumber>{AccountNumber{*accountElement.parent_}}
								: Optional<AccountNumber>{}
						},
						Money{
							accountElement.major_,
							accountElement.minor_,
							toIso4217Code(accountElement.currency_.toStdString())
						},
						accountElement.isClosed_
					},
					ledgersTransactions[accountElement.id_]
				}
			);
		}
		catch (const exception& error)
		{
			throw BudgetFileError{
				{},
				{},
				{},
				QObject::tr("The budget file contains malformed ledger data."),
				error
			};
		}
	auto openingAccount{
		[&accountsElement, &ledgersElement, &findAccountName] () -> Optional<AccountCode>
		{
			if ( ! ledgersElement.openingAccountId_.hasValue())
				return {};
			return AccountCode{
				AccountNumber{*ledgersElement.openingAccountId_},
				findAccountName(accountsElement.accounts_, *ledgersElement.openingAccountId_)
			};
		}()
	};
	map<BankName, map<AccountNumber, SupportedAccountTypes>> bankAccountTypes;
	BankMap banks{
		[&banksElement, &bankAccountTypes] ()
		{
			map<BankName, Bank> rawBanks;

			for (const auto& bankElement : banksElement.banks_)
				try
				{
					const BankName name{bankElement.name_};
					set<AccountNumber> numbers;

					for (const auto& account : bankElement.accounts_)
					{
						AccountNumber number{account.accountId_};
						numbers.emplace(number);
						bankAccountTypes[name][move(number)] = toSupportedAccountTypes(account.type_);
					}
					rawBanks[name] = Bank{name, move(numbers), bankElement.isClosed_};
				}
				catch (const exception& error)
				{
					throw BudgetFileError{
						{},
						{},
						{},
						QObject::tr("The budget file contains malformed banks data."),
						error
					};
				}
			return rawBanks;
		}()
	};
	TypeIndexMap<BudgetSource, AccountCode> budgetCodes{};

	for (const auto& wageElement : wageElements.wages_)
		budgetCodes.add<Wage>(
			BudgetSource{wageElement.sourceName_},
			ledgers.at(AccountCode{AccountNumber{wageElement.accountId_}}).account_.code()
		);

	for (const auto& billElement : billsElements.bills_)
		budgetCodes.add<Bill>(
			BudgetSource{billElement.sourceName_},
			ledgers.at(AccountCode{AccountNumber{billElement.accountId_}}).account_.code()
		);

	for (const auto& debtElement : debtsElements.debts_)
		budgetCodes.add<Debt>(
			BudgetSource{debtElement.sourceName_},
			ledgers.at(AccountCode{AccountNumber{debtElement.accountId_}}).account_.code()
		);

	for (const auto& goalElement : goalsElements.goals_)
		budgetCodes.add<Goal>(
			BudgetSource{goalElement.sourceName_},
			ledgers.at(AccountCode{AccountNumber{goalElement.accountId_}}).account_.code()
		);

	for (const auto& nontrackElement : nontrackElements.discretionaries_)
		budgetCodes.add<Nontrack>(
			BudgetSource{nontrackElement.sourceName_},
			ledgers.at(AccountCode{AccountNumber{nontrackElement.accountId_}}).account_.code()
		);
	return make_tuple(
		GeneralLedger{move(ledgers), move(openingAccount)},
		move(b),
		move(budgetCodes),
		move(banks),
		move(bankAccountTypes)
	);
}

void drn::file_storage::internal::write(
	const Budget& b,
	const GeneralLedger& gl,
	const BankMap& banks,
	const map<BankName, map<AccountNumber, SupportedAccountTypes>>& bankAccountTypes,
	const TypeIndexMap<BudgetSource, AccountCode>& budgetCodes,
	QIODevice& file,
	const bool shouldIndent
)
{
	qDebug() << "Preparing to write to the budget file.";
	Q_ASSERT(file.isOpen());
	QXmlStreamWriter xml{&file};

	if (shouldIndent)
	{
		xml.setAutoFormatting(true);
		xml.setAutoFormattingIndent(-1); // One tab.
	}
	xml.writeStartDocument();

	qDebug() << "Filling application element";
	const ApplicationElement applicationElement{};
	qDebug() << "Filling wage elements";
	const WagesElement wageElements{
		[&b, &budgetCodes] ()
		{
			WagesElement we;
			auto id{static_cast<IndexedElement::IdType>(IndexedElement::invalidId_ + 1)};

			for (const auto& wage : b.wages_)
			{
				we.wages_.emplace_back(
					id,
					wage.second.amount().major(),
					wage.second.amount().minor(),
					presentationText(wage.second.period()),
					wage.second.source().name(),
					QString::fromStdString(toStdString(wage.second.amount().code())),
					wage.second.nextOccurOn(),
					budgetCodes.value<Wage>(wage.second.source()).number().integer()
				);
				++id;
			}
			return we;
		}()
	};
	qDebug() << "Filling bill elements";
	const BillsElement billElements{
		[&b, &budgetCodes] ()
		{
			BillsElement ee;
			auto id{static_cast<IndexedElement::IdType>(IndexedElement::invalidId_ + 1)};

			for (const auto& bill : b.bills_)
			{
				ee.bills_.emplace_back(
					id,
					bill.second.amount().major(),
					bill.second.amount().minor(),
					presentationText(bill.second.period()),
					bill.second.source().name(),
					QString::fromStdString(toStdString(bill.second.amount().code())),
					bill.second.nextOccurOn(),
					budgetCodes.value<Bill>(bill.second.source()).number().integer()
				);
				++id;
			}
			return ee;
		}()
	};
	qDebug() << "Filling debt elements";
	const DebtsElement debtsElements{
		[&b, &budgetCodes] ()
		{
			DebtsElement le;
			auto id{static_cast<IndexedElement::IdType>(IndexedElement::invalidId_ + 1)};

			for (const auto& debt : b.debts_)
			{
				le.debts_.emplace_back(
					id,
					debt.second.borrowed().major(),
					debt.second.borrowed().minor(),
					presentationText(debt.second.period()),
					debt.second.source().name(),
					QString::fromStdString(toStdString(debt.second.amount().code())),
					debt.second.amount().major(),
					debt.second.amount().minor(),
					debt.second.interest().rate_,
					debt.second.nextOccurOn(),
					budgetCodes.value<Debt>(debt.second.source()).number().integer()
				);
				++id;
			}
			return le;
		}()
	};
	qDebug() << "Filling goal elements";
	const GoalsElement goalElements{
		[&b, &budgetCodes] ()
		{
			GoalsElement ge;
			auto id{static_cast<IndexedElement::IdType>(IndexedElement::invalidId_ + 1)};

			for (const auto& goal : b.goals_)
			{
				ge.goals_.emplace_back(
					id,
					goal.second.amount().major(),
					goal.second.amount().minor(),
					presentationText(goal.second.period()),
					goal.second.source().name(),
					QString::fromStdString(toStdString(goal.second.amount().code())),
					goal.second.final().major(),
					goal.second.final().minor(),
					goal.second.nextOccurOn(),
					budgetCodes.value<Goal>(goal.second.source()).number().integer()
				);
				++id;
			}
			return ge;
		}()
	};
	qDebug() << "Filling discretionary elements";
	const DiscretionariesElement nontrackElements{
		[&b, &budgetCodes] ()
		{
			DiscretionariesElement des;
			auto id{static_cast<IndexedElement::IdType>(IndexedElement::invalidId_ + 1)};

			for (const auto& nontrack : b.nontracks_)
			{
				des.discretionaries_.emplace_back(
					id,
					nontrack.second.amount().major(),
					nontrack.second.amount().minor(),
					presentationText(nontrack.second.period()),
					nontrack.second.source().name(),
					QString::fromStdString(toStdString(nontrack.second.amount().code())),
					nontrack.second.nextOccurOn(),
					budgetCodes.value<Nontrack>(nontrack.second.source()).number().integer()
				);
				++id;
			}
			return des;
		}()
	};

	AccountsElement accountsElement{};
	LedgersElement ledgersElement{
		gl.openingAccountCode().hasValue()
			? gl.openingAccountCode()->number().integer()
			: Optional<IndexedElement::IdType>{}
	};
	qDebug() << "Filling account, ledger, and transaction elements";
	for (const auto& ledger : gl.ledgers())
	{
		const auto& account{ledger.second.account_};
		accountsElement.accounts_.emplace_back(
			ledger.second.account_.code().number().integer(),
			ledger.second.account_.code().name(),
			presentationText(ledger.second.account_.type()),
			ledger.second.account_.balance().major(),
			ledger.second.account_.balance().minor(),
			QString::fromStdString(toStdString(ledger.second.account_.balance().code())),
			ledger.second.account_.isClosed(),
			account.code().parent().hasValue()
				? Optional<quint32>(account.code().parent()->integer())
				: Optional<quint32>{}
		);
		LedgerElement ledgerElement{ledger.second.account_.code().number().integer()};

		for (const auto& transaction : ledger.second.transactions_)
			ledgerElement.transactions_.emplace_back(
				transaction.second.number()->integer(),
				QDate{
					static_cast<qint32>(transaction.second.occurredOn().year()),
					static_cast<qint32>(static_cast<quint32>(
						transaction.second.occurredOn().month()
					)),
					static_cast<qint32>(static_cast<quint32>(
						transaction.second.occurredOn().day()
					))
				},
				transaction.second.crediting() == ledger.first
					? transaction.second.debiting().number().integer()
					: transaction.second.crediting().number().integer(),
				transaction.second.crediting() == ledger.first
					? debitAbbreviation()
					: creditAbbreviation(),
				transaction.second.amount().major(),
				transaction.second.amount().minor(),
				QString::fromStdString(toStdString(transaction.second.amount().code())),
				presentationText(transaction.second.status()),
				transaction.second.description().hasValue()
					? *transaction.second.description()
					: Optional<QString>{}
			);
		ledgersElement.ledgers_.emplace_back(ledgerElement);
	}
	qDebug() << "Filling bank elements";
	const BanksElement banksElement{
		[&banks, &bankAccountTypes] ()
		{
			vector<BankElement> rawBanks;

			for (const auto& bank : banks)
			{
				vector<BankAccountElement> accounts;

				for (const auto& number : bank.second.accounts())
					accounts.emplace_back(
						number.integer(),
						presentationText(bankAccountTypes.at(bank.first).at(number))
					);
				rawBanks.emplace_back(
					bank.first.institution(),
					bank.second.isClosed(),
					move(accounts)
				);
			}
			return rawBanks;
		}()
	};

	qDebug() << "Writing budget elements";
	applicationElement.write(xml);
	const vector<ObserverPtr<const BasicElement>> budgetWriters{
		&wageElements,
		&billElements,
		&debtsElements,
		&goalElements,
		&nontrackElements,
		&accountsElement,
		&ledgersElement,
		&banksElement
	};

	for (const auto& writer: budgetWriters)
		writer->write(xml);
	xml.writeEndElement(); // Closes the application element.
	qDebug() << "Completed writing budget elements";
}
