/*
 * Copyright (C) 2014-2021 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QFile>
#include <QFileDialog>
#include <QMap>
#include <QMessageBox>
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
#include <QStorageInfo>
#else /* < Qt-5.4 */
#warning "Compiling against version < Qt-5.4 which does not have QStorageInfo."
#endif /* >= Qt-5.4 */

#include "src/common.h" /* dateTimeDisplayFormat */
#include "src/datovka_shared/identifiers/account_id.h"
#include "src/datovka_shared/log/log.h"
#include "src/global.h"
#include "src/gui/dlg_restore.h"
#include "src/gui/dlg_restore_internal.h"
#include "src/gui/helper.h"
#include "src/io/account_db.h"
#include "src/io/message_db_set.h"
#include "src/io/tag_db.h"
#include "src/json/backup.h"
#include "src/settings/accounts.h"
#include "src/settings/preferences.h"
#include "src/views/table_home_end_filter.h"
#include "src/views/table_space_selection_filter.h"
#include "src/views/table_tab_ignore_filter.h"
#include "ui_dlg_restore.h"

#define macroAccountName(aId) \
	GlobInstcs::acntMapPtr->acntData(aId).accountName()

void *DlgRestore::restoreDescr = Q_NULLPTR;

DlgRestore::DlgRestore(const QList<AcntIdDb> &acntIdDbList,
    QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgRestore),
    m_acntIdDbList(acntIdDbList),
    m_restoreSelectionModel(this)
{
	m_ui->setupUi(this);

	initDialogue();

	enableOkButton();
}

DlgRestore::~DlgRestore(void)
{
	delete m_ui;
}

bool DlgRestore::restore(const QList<AcntIdDb> &acntIdDbList, QWidget *parent)
{
	DlgRestore dlg(acntIdDbList, parent);
	if (QDialog::Accepted != dlg.exec()) {
		Q_ASSERT(restoreDescr == Q_NULLPTR);
		return false;
	}

	restoreDescr = dlg.collectRestorationDescr();
	return restoreDescr != Q_NULLPTR;
}

/*!
 * @brief Load data into model.
 *
 * @param[in,out] model Model to be set.
 * @param[in]     backup Backup description structure.
 */
static
void loadModelData(RestoreSelectionModel &model, const Json::Backup &backup)
{
	model.clear();
	foreach (const Json::Backup::MessageDb &msgDbEntry, backup.messageDbs()) {
		model.appendData(RestoreSelectionModel::ACT_NONE,
		    msgDbEntry.accountName(),
		    AcntId(msgDbEntry.username(), msgDbEntry.testing()),
		    msgDbEntry.boxId());
	}
}

/*!
 * @brief Read backup content from JSON file.
 *
 * @param[out] backuEntry Read file content.
 * @param[in]  jsonPath Path for JSON file.
 * @param[in]  parent Parent object.
 * @return True on success.
 */
static
bool readBackup(Json::Backup &backup, const QString &jsonPath,
    QWidget *parent = Q_NULLPTR)
{
	if (Q_UNLIKELY(jsonPath.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	QFile jsonFile(jsonPath);
	if (Q_UNLIKELY(!jsonFile.open(QIODevice::ReadOnly))) {
		QMessageBox::warning(parent, DlgRestore::tr("Cannot Open File"),
		    DlgRestore::tr("File '%1' cannot be opened.").arg(jsonPath),
		    QMessageBox::Ok, QMessageBox::Ok);
		return false;
	}

	QByteArray data(jsonFile.readAll());
	jsonFile.close();

	bool iOk = false;
	backup = Json::Backup::fromJson(data, &iOk);
	if (Q_UNLIKELY((!iOk) || (!backup.isValid()))) {
		QMessageBox::warning(parent, DlgRestore::tr("Cannot Read Content"),
		    DlgRestore::tr("File '%1' seems not to contain a valid back-up description.").arg(jsonPath),
		    QMessageBox::Ok, QMessageBox::Ok);
		return false;
	}

	return true;
}

void DlgRestore::chooseJsonFile(void)
{
	QString jsonPath(QFileDialog::getOpenFileName(this,
	    tr("Open JSON File"), QString(), tr("JSON File (*.json)")));
	if (jsonPath.isEmpty()) {
		return;
	}

	{
		QFileInfo fileInfo(jsonPath);
		if (Q_UNLIKELY((!fileInfo.isFile()) || (!fileInfo.isReadable()))) {
			QMessageBox::warning(this, tr("Not a Readable File"),
			    tr("File '%1' cannot be read.").arg(jsonPath),
			    QMessageBox::Ok, QMessageBox::Ok);
			return;
		}
	}

	Json::Backup backup;
	if (!readBackup(backup, jsonPath, this)) {
		return;
	}

	m_ui->jsonLine->setText(jsonPath);

	m_ui->backupTimeLabel->setText(backup.dateTime().isValid() ?
	    tr("Backup was taken at %1.")
	        .arg(backup.dateTime().toLocalTime().toString(dateTimeDisplayFormat)) :
	    QString());

	loadModelData(m_restoreSelectionModel, backup);
	m_ui->accountView->setEnabled(m_restoreSelectionModel.rowCount() > 0);

	m_ui->tagsCheckBox->setEnabled(!backup.tagDb().fileName().isEmpty());
}

/*!
 * @brief Check whether the account list contains the account identifier.
 *
 * @param[in] acntIdDbList Account list.
 * @param[in] acntId Account identifier.
 * @return True of corresponding username is found.
 */
static
bool containsAcntId(const QList<AcntIdDb> &acntIdDbList, const AcntId &acntId)
{
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	foreach (const AcntIdDb &acntIdDb, acntIdDbList) {
		if ((acntIdDb.username() == acntId.username()) &&
		    (acntIdDb.testing() == acntId.testing())) {
			return true;
		}
	}

	return false;
}

void DlgRestore::modifiedMessageSelection(const QModelIndex &topLeft,
    const QModelIndex &bottomRight)
{
	if ((topLeft.column() > RestoreSelectionModel::COL_MSGS_CHECKBOX) ||
	    (bottomRight.column() < RestoreSelectionModel::COL_MSGS_CHECKBOX)) {
		/* Selection did not change. */
		return;
	}

	const int row = topLeft.row();
	if (row != bottomRight.row()) {
		/* We expect only a single line to change at a time. */
		Q_ASSERT(0);
		return;
	}

	const QModelIndex checkBoxIdx(
	    topLeft.sibling(row, RestoreSelectionModel::COL_MSGS_CHECKBOX));

	if (checkBoxIdx.data(Qt::CheckStateRole) == Qt::Checked) {
		const AcntId acntId(
		    topLeft.sibling(row, RestoreSelectionModel::COL_USERNAME).data().toString(),
		    topLeft.sibling(row, RestoreSelectionModel::COL_TESTING).data().toBool());
		if (Q_UNLIKELY(!acntId.isValid())) {
			Q_ASSERT(0);
			m_restoreSelectionModel.setData(checkBoxIdx, Qt::Unchecked,
			    Qt::CheckStateRole);
			return;
		}

		const QModelIndex actionIdx(
		    topLeft.sibling(row, RestoreSelectionModel::COL_RESTORE_ACTION));

		/* Test whether account exists. */
		if (containsAcntId(m_acntIdDbList, acntId)) {
			const QString aName(macroAccountName(acntId));
			enum QMessageBox::StandardButton reply = QMessageBox::question(
			    this, tr("Restore Message Database?"),
			    tr("The account '%1' with the corresponding username already exists. "
			       "The action will delete all existing message data for this account and replace them with data from the back-up. "
			       "Do you wish to restore the message database using the data from the back-up?")
			           .arg(aName),
			    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
			if (reply == QMessageBox::No) {
				m_restoreSelectionModel.setData(checkBoxIdx,
				    Qt::Unchecked, Qt::CheckStateRole);
			} else {
				m_restoreSelectionModel.setData(actionIdx,
				    RestoreSelectionModel::ACT_REPLACE,
				    Qt::EditRole);
			}
		} else {
			enum QMessageBox::StandardButton reply = QMessageBox::question(
			    this, tr("Create New Account?"),
			    tr("There is no account corresponding to the username. "
			       "The action will create an additional account and fill it with data from the back-up. "
			       "Do you wish to restore the message database using the data from the back-up?"),
			    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
			if (reply == QMessageBox::No) {
				m_restoreSelectionModel.setData(checkBoxIdx,
				    Qt::Unchecked, Qt::CheckStateRole);
			} else {
				m_restoreSelectionModel.setData(actionIdx,
				    RestoreSelectionModel::ACT_CREATE,
				    Qt::EditRole);
			}
		}
	}

	checkAvailableSpace();
}

void DlgRestore::modifiedTagSelection(int state)
{
	if (Q_UNLIKELY((state != Qt::Unchecked) && (state != Qt::Checked))) {
		Q_ASSERT(0);
		m_ui->tagsCheckBox->setCheckState(Qt::Unchecked);
		m_ui->tagsActionLabel->setText(
		    RestoreSelectionModel::actionString(RestoreSelectionModel::ACT_NONE));
		return;
	}

	if (state == Qt::Checked) {
		enum QMessageBox::StandardButton reply = QMessageBox::question(
		    this, tr("Restore Tag Database?"),
		    tr("The action will delete all existing tag data and replace them with data from the backup. "
		       "Do you wish to restore the tag database using the data from the back-up?"),
		       QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
		if (reply == QMessageBox::No) {
			state = Qt::Unchecked;
			m_ui->tagsCheckBox->setCheckState(Qt::Unchecked);
		}
	}

	/* State is unchecked or checked. */
	m_ui->tagsActionLabel->setText(RestoreSelectionModel::actionString(
	    (state == Qt::Unchecked) ? RestoreSelectionModel::ACT_NONE : RestoreSelectionModel::ACT_REPLACE));

	checkAvailableSpace();
}

void DlgRestore::enableOkButton(void)
{
	bool modelOk = m_restoreSelectionModel.numChecked() > 0;
	bool tagsOk = m_ui->tagsCheckBox->checkState() == Qt::Checked;

	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
	    modelOk || tagsOk);
}

void DlgRestore::askConfirmation(void)
{
	setEnabled(false);
	enum QMessageBox::StandardButton reply = QMessageBox::question(
	    this, tr("Proceed?"),
	    tr("The action will close the application and will continue with the restoration of selected data. "
	       "According to the selection some currently available data may be deleted. "
	       "Do you wish to restore the selected data from the back-up?"),
	    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
	setEnabled(true);
	if (reply == QMessageBox::Yes) {
		accept();
	}
}

void DlgRestore::initDialogue(void)
{
	m_ui->infoLabel->setText(
	    tr("Selected data will be restored from the specified location.") +
	    QStringLiteral("\n") +
	    tr("Data in the application will be replaced by the data from the back-up."));

	m_ui->backupTimeLabel->setText(QString());
	m_ui->jsonLine->setReadOnly(true);

	m_ui->accountView->setNarrowedLineHeight();
	m_ui->accountView->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
	m_ui->accountView->setSelectionMode(QAbstractItemView::ExtendedSelection);
	m_ui->accountView->setSelectionBehavior(QAbstractItemView::SelectRows);

	m_ui->accountView->installEventFilter(
	    new (::std::nothrow) TableHomeEndFilter(m_ui->accountView));
	m_ui->accountView->installEventFilter(
	    new (::std::nothrow) TableSpaceSelectionFilter(
	        RestoreSelectionModel::COL_MSGS_CHECKBOX, m_ui->accountView));
	m_ui->accountView->installEventFilter(
	    new (::std::nothrow) TableTabIgnoreFilter(m_ui->accountView));

	m_ui->accountView->setEnabled(false);

	m_ui->accountView->setModel(&m_restoreSelectionModel);

	m_ui->accountView->setSquareColumnWidth(RestoreSelectionModel::COL_MSGS_CHECKBOX);
	m_ui->accountView->setColumnHidden(RestoreSelectionModel::COL_TESTING, true);

	m_ui->tagsCheckBox->setCheckState(Qt::Unchecked);
	m_ui->tagsCheckBox->setEnabled(false);
	m_ui->tagsActionLabel->setText(
	    RestoreSelectionModel::actionString(RestoreSelectionModel::ACT_NONE));

	m_ui->warningLabel->setEnabled(false);
	m_ui->warningLabel->setVisible(false);
	m_ui->warningLabel->setStyleSheet(QString());
	m_ui->warningLabel->setText(QString());

	connect(m_ui->chooseDirButton, SIGNAL(clicked()),
	    this, SLOT(chooseJsonFile()));
	connect(&m_restoreSelectionModel,
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(modifiedMessageSelection(QModelIndex, QModelIndex)));
	connect(&m_restoreSelectionModel,
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(enableOkButton()));
	connect(m_ui->tagsCheckBox, SIGNAL(stateChanged(int)),
	    this, SLOT(modifiedTagSelection(int)));
	connect(m_ui->tagsCheckBox, SIGNAL(stateChanged(int)),
	    this, SLOT(enableOkButton()));

	/* Signal rejected() is connected in the ui file. */
	connect(m_ui->buttonBox, SIGNAL(accepted()),
	    this, SLOT(askConfirmation()));
}

#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
/*!
 * @brief Obtains the total size of the message databases from the back-up.
 *
 * @param[in] backupDir Full path to back-up directory.
 * @param[in] buMsgDb Message database backup entry to obtain size from.
 * @return Total size, 0 on any error.
 */
static
qint64 backedMessageDbSize(const QString &backupDir,
    const Json::Backup::MessageDb &buMsgDb)
{
	if (Q_UNLIKELY(backupDir.isEmpty())) {
		Q_ASSERT(0);
		return 0;
	}

	qint64 sum = 0;
	foreach (const Json::Backup::File &file, buMsgDb.files()) {
		const QString filePath(
		    backupDir + QStringLiteral("/") + buMsgDb.subdir() + QStringLiteral("/") + file.fileName());
		QFileInfo info(filePath);
		if (Q_UNLIKELY((!info.isFile() || (!info.isReadable())))) {
			logWarningNL("Cannot access '%s'.",
			    filePath.toUtf8().constData());
			continue;
		}
		sum += info.size();
	}
	return sum;
}
#endif /* >= Qt-5.4 */

bool DlgRestore::checkAvailableSpace(void)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
{
	const QString jsonPath(m_ui->jsonLine->text());
	Json::Backup backup;
	if (!readBackup(backup, jsonPath)) {
		Q_ASSERT(0);
		return true;
	}
	const QString backupDir(QFileInfo(jsonPath).absolutePath());

	/* QStorageInfo.displayName(), amount of additional space needed. */
	QMap<QString, qint64> storageBalance;
	QMap<QString, bool> storageExceeded;

	/* Compute storage balance for message databases. */
	foreach (const RestoreSelectionModel::Entry &e,
	    m_restoreSelectionModel.selectedEntries()) {
		if (Q_UNLIKELY(e.action == RestoreSelectionModel::ACT_NONE)) {
			Q_ASSERT(0);
			continue;
		}
		if (Q_UNLIKELY(!e.acntId.isValid())) {
			Q_ASSERT(0);
			continue;
		}

		qint64 removed = 0;
		QString tgtDir;
		if (e.action == RestoreSelectionModel::ACT_REPLACE) {
			MessageDbSet *dbSet =
			    GuiHelper::getDbSet(m_acntIdDbList, e.acntId);
			if (Q_UNLIKELY(dbSet == Q_NULLPTR)) {
				Q_ASSERT(0);
				continue;
			}
			tgtDir = dbSet->locationDirectory();
			removed = dbSet->fileSize(MessageDbSet::SC_SUM);
			if (Q_UNLIKELY(removed < 0)) {
				/* Database in memory. */
				Q_ASSERT(0);
				continue;
			}
		} else if (e.action == RestoreSelectionModel::ACT_CREATE) {
			tgtDir = GlobInstcs::iniPrefsPtr->confDir();
		}
		qint64 added = backedMessageDbSize(backupDir,
		    backup.messageDb(e.acntId.username(), e.acntId.testing()));

		const QStorageInfo info(tgtDir);
		qint64 balance = storageBalance.value(info.displayName(), 0);
		balance += added - removed;
		storageBalance[info.displayName()] = balance;
		storageExceeded[info.displayName()] = balance > info.bytesAvailable();
	}

	if (m_ui->tagsCheckBox->checkState() == Qt::Checked) {
		const QString tgtDir(GlobInstcs::iniPrefsPtr->confDir());
		qint64 added = QFileInfo(backupDir + QStringLiteral("/") + backup.tagDb().fileName()).size();
		qint64 removed = GlobInstcs::tagDbPtr->fileSize();
		const QStorageInfo info(tgtDir);
		qint64 balance = storageBalance.value(info.displayName(), 0);
		balance += added - removed;
		storageBalance[info.displayName()] = balance;
		storageExceeded[info.displayName()] = balance > info.bytesAvailable();
	}

	/* Ignore the account database. */

	QStringList exceededMessages;
	foreach (const QString &key, storageExceeded.keys()) {
		if (storageExceeded[key]) {
			exceededMessages.append(tr("Not enough space on volume %1.").arg(key));
		}
	}

	if (exceededMessages.size() > 0) {
		m_ui->warningLabel->setEnabled(true);
		m_ui->warningLabel->setVisible(true);
		m_ui->warningLabel->setStyleSheet("QLabel { color: red }");
		m_ui->warningLabel->setText(exceededMessages.join(QStringLiteral("\n")));
	} else {
		m_ui->warningLabel->setEnabled(false);
		m_ui->warningLabel->setVisible(false);
		m_ui->warningLabel->setStyleSheet(QString());
		m_ui->warningLabel->setText(QString());
	}

	return exceededMessages.size() == 0;
}
#else /* < Qt-5.4 */
{
	return true;
}
#endif /* >= Qt-5.4 */

void *DlgRestore::collectRestorationDescr(void) const
{
	Json::Backup backup;
	const QString jsonPath(m_ui->jsonLine->text());

	RestoreWorker::RestorationDescr *descr =
	    new (::std::nothrow) RestoreWorker::RestorationDescr;
	if (Q_UNLIKELY(descr == Q_NULLPTR)) {
		Q_ASSERT(0);
		goto fail;
	}

	if (!readBackup(backup, jsonPath)) {
		goto fail;
	}

	descr->backupDir = QFileInfo(jsonPath).absolutePath();
	descr->configDir = GlobInstcs::iniPrefsPtr->confDir();
	descr->saveConfPath = GlobInstcs::iniPrefsPtr->saveConfPath();

	foreach (const RestoreSelectionModel::Entry &e,
	    m_restoreSelectionModel.selectedEntries()) {
		if (Q_UNLIKELY(e.action == RestoreSelectionModel::ACT_NONE)) {
			Q_ASSERT(0);
			continue;
		}
		if (Q_UNLIKELY(!e.acntId.isValid())) {
			Q_ASSERT(0);
			continue;
		}

		const Json::Backup::MessageDb &buMsgDb(
		    backup.messageDb(e.acntId.username(), e.acntId.testing()));
		if (Q_UNLIKELY(buMsgDb.isNull())) {
			Q_ASSERT(0);
			continue;
		}

		RestoreWorker::RestorationDescr::MsgDb msgDb;

		msgDb.createNew = (e.action == RestoreSelectionModel::ACT_CREATE);
		msgDb.testEnv = buMsgDb.testing();
		msgDb.srcAcntName = e.accountName;
		msgDb.srcUname = e.acntId.username();
		msgDb.srcDir = descr->backupDir + QStringLiteral("/") + buMsgDb.subdir();
		{
			QList<QString> fileNames;
			foreach (const Json::Backup::File &file, buMsgDb.files()) {
				fileNames.append(file.fileName());
			}
			msgDb.srcFiles = fileNames;
		}
		if (e.action == RestoreSelectionModel::ACT_REPLACE) {
			msgDb.tgtAcntName = macroAccountName(e.acntId);
			msgDb.tgtUname = e.acntId.username();
			MessageDbSet *dbSet =
			    GuiHelper::getDbSet(m_acntIdDbList, e.acntId);
			if (Q_UNLIKELY(dbSet == Q_NULLPTR)) {
				Q_ASSERT(0);
				continue;
			}
			msgDb.tgtDir = dbSet->locationDirectory();
		} else if (e.action == RestoreSelectionModel::ACT_CREATE) {
			/* Store into configuration directory. */
			msgDb.tgtAcntName = tr("Restored account") + QStringLiteral(" ") + e.accountName;
			msgDb.tgtUname = e.acntId.username();
			msgDb.tgtDir = descr->configDir;
		}
		/* Don't set target files. */

		descr->msgDbs.append(msgDb);
	}

	if (m_ui->tagsCheckBox->checkState() == Qt::Checked) {
		descr->tagDbFile = backup.tagDb().fileName();
		descr->runningTagDbFilePath = GlobInstcs::tagDbPtr->fileName();
	}

	/* Don't restore the account database. */
#if 0
	descr->accountDbFile = backup.accountDb().fileName();
	descr->runningAccountDbFilePath = GlobInstcs::accntDbPtr->fileName();
#endif

	return descr;

fail:
	if (descr != Q_NULLPTR) {
		Q_ASSERT(restoreDescr == Q_NULLPTR);
		delete descr;
	}
	return Q_NULLPTR;
}
