/*
 * Copyright (C) 2014-2024 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 <cinttypes>
#include <QMessageBox>
#include <QTimer>

#include "src/datovka_shared/graphics/graphics.h"
#include "src/datovka_shared/io/records_management_db.h"
#include "src/datovka_shared/json/helper.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/records_management/json/entry_error.h"
#include "src/datovka_shared/records_management/json/upload_file.h"
#include "src/datovka_shared/records_management/json/upload_hierarchy.h"
#include "src/dimensions/dimensions.h"
#include "src/global.h"
#include "src/gui/dlg_msg_box_nonmodal.h"
#include "src/models/sort_filter_proxy_model.h"
#include "src/records_management/gui/dlg_records_management_upload.h"
#include "src/records_management/gui/dlg_records_management_upload_progress.h"
#include "src/settings/prefs_specific.h"
#include "ui_dlg_records_management_upload.h"

#define LOGO_EDGE 64

#define RUN_DELAY_MS 500

DlgRecordsManagementUpload::DlgRecordsManagementUpload(const QString &urlStr,
    const QString &tokenStr, qint64 dmId, QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgRecordsManagementUpload),
    m_url(urlStr),
    m_token(tokenStr),
    m_rmc(RecMgmt::Connection::ignoreSslErrorsDflt, this),
    m_uploadModel(),
    m_uploadProxyModel(),
    m_dfltFilerLineStyleSheet(SortFilterProxyModel::invalidFilterEditStyle),
    m_selectedUploadIds()
{
	m_ui->setupUi(this);

	loadRecordsManagementPixmap(LOGO_EDGE);
	m_ui->appealLabel->setText(
	    tr("Select the location where you want\nto upload the message '%1' into.")
	        .arg(dmId));

	connect(m_ui->reloadButton, SIGNAL(clicked(bool)),
	    this, SLOT(callUploadHierarchy()));

	m_ui->filterLine->setClearButtonEnabled(true);
	connect(m_ui->filterLine, SIGNAL(textChanged(QString)),
	    this, SLOT(filterHierarchy(QString)));

	m_ui->uploadView->setNarrowedLineHeight();
	m_ui->uploadView->header()->setDefaultAlignment(Qt::AlignLeft);

	m_ui->uploadView->setModel(&m_uploadProxyModel);
	m_uploadProxyModel.setSourceModel(&m_uploadModel);
	connect(m_ui->uploadView->selectionModel(),
	    SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
	    this, SLOT(uploadHierarchySelectionChanged()));

	m_ui->uploadView->sortByColumn(0, Qt::AscendingOrder);
	m_ui->uploadView->setSortingEnabled(true);

	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);

	connect(&m_rmc, SIGNAL(connectionError(QString)),
	    this, SLOT(notifyCommunicationError(QString)));
}

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

enum DlgRecordsManagementUpload::CommunicationResult DlgRecordsManagementUpload::uploadMessage(
    const RecordsManagementSettings &recMgmtSettings, qint64 dmId,
    const QString &msgFileName, const QByteArray &msgData,
    const QStringList &uploadIds, QWidget *parent)
{
	if (Q_UNLIKELY(!recMgmtSettings.isValid())) {
		Q_ASSERT(0);
		return COMM_ERROR;
	}

	if (Q_UNLIKELY(msgFileName.isEmpty() || msgData.isEmpty())) {
		Q_ASSERT(0);
		return COMM_ERROR;
	}

	DlgRecordsManagementUpload dlg(recMgmtSettings.url(),
	    recMgmtSettings.token(), dmId, parent);
	if (uploadIds.isEmpty()) {
		/* Currently there is no means how to detect whether dialogue is shown. */
		QTimer::singleShot(RUN_DELAY_MS, &dlg, SLOT(callUploadHierarchy()));
		/* Let the user choose the location. */

		const QString dlgName("records_management_upload");
		const QSize dfltSize = dlg.size();
		{
			const QSize newSize = Dimensions::dialogueSize(&dlg,
			    PrefsSpecific::dlgSize(*GlobInstcs::prefsPtr, dlgName),
			    dfltSize);
			if (newSize.isValid()) {
				dlg.resize(newSize);
			}
		}

		int ret = dlg.exec();

		PrefsSpecific::setDlgSize(*GlobInstcs::prefsPtr, dlgName,
		    dlg.size(), dfltSize);

		if (QDialog::Accepted != ret) {
			return COMM_CANCELLED;
		}

		if (Q_UNLIKELY(dlg.m_selectedUploadIds.isEmpty())) {
			Q_ASSERT(0);
			return COMM_ERROR;
		}
	} else {
		/*
		 * Don't check he presence of the identifiers.
		 * Just expect their existence.
		 */
		//if (Q_UNLIKELY(!dlg.checkUploadIdsExistence(uploadIds))) {
		//	return false;
		//}
	}

	dlg.m_rmc.setConnection(dlg.m_url, dlg.m_token);
	return uploadFile(dlg.m_rmc, dmId,
	    uploadIds.isEmpty() ? dlg.m_selectedUploadIds : uploadIds,
	    msgFileName, msgData, parent);
}

/*!
 * @brief Download the records management hierarchy.
 *
 * @param[out]    uhRes Upload hierarchy response.
 * @param[in,out] rmc Records management connection.
 * @param[in]     parent Parent widget.
 * @return True on success.
 */
static
bool downloadUploadHierarchy(RecMgmt::UploadHierarchyResp &uhRes,
    RecMgmt::Connection &rmc, QWidget *parent = Q_NULLPTR)
{
	QByteArray response;

	if (RecMgmt::Connection::COMM_SUCCESS == rmc.communicate(
	        RecMgmt::Connection::SRVC_UPLOAD_HIERARCHY,
	        QByteArray(), response)) {
		if (!response.isEmpty()) {
			logDebugLv1NL("Records management upload hierarchy reply data: '%s'",
			    Json::Helper::reindent(response, true).constData());

			bool ok = false;
			uhRes = RecMgmt::UploadHierarchyResp::fromJson(response,
			    &ok);
			if (!ok || !uhRes.isValid()) {
				QMessageBox::critical(parent,
				    DlgRecordsManagementUpload::tr("Communication Error"),
				    DlgRecordsManagementUpload::tr("Received invalid response."));
				return false;
			}

			return true;
		} else {
			QMessageBox::critical(parent,
			    DlgRecordsManagementUpload::tr("Communication Error"),
			    DlgRecordsManagementUpload::tr("Received empty response."));
			return false;
		}
	} else {
		return false;
	}
}

RecMgmt::AutomaticUploadTarget DlgRecordsManagementUpload::uploadHierarchyTargets(
    const RecordsManagementSettings &recMgmtSettings, QWidget *parent)
{
	if (Q_UNLIKELY(!recMgmtSettings.isValid())) {
		Q_ASSERT(0);
		return RecMgmt::AutomaticUploadTarget();
	}

	DlgRecordsManagementUpload dlg(recMgmtSettings.url(),
	    recMgmtSettings.token(), 0, parent);

	dlg.m_rmc.setConnection(dlg.m_url, dlg.m_token);
	RecMgmt::UploadHierarchyResp uhRes;
	if (downloadUploadHierarchy(uhRes, dlg.m_rmc, parent)) {
		RecMgmt::AutomaticUploadTarget uploadTarggets =
		    RecMgmt::AutomaticUploadTarget::getUnambiguousTargets(uhRes);
		logDebugLv1NL(
		    "Records management case_id to upload hierarchy mapping: %s",
		    uploadTarggets.dumpCaseIdMapping().join(", ").toUtf8().constData());
		logDebugLv1NL(
		    "Records management court_case_id to upload hierarchy mapping: %s",
		    uploadTarggets.dumpCourtCaseIdMapping().join(", ").toUtf8().constData());
		return uploadTarggets;
	}

	return RecMgmt::AutomaticUploadTarget();
}

void DlgRecordsManagementUpload::callUploadHierarchy(void)
{
	QByteArray response;

	/* Clear model. */
	m_uploadModel.setHierarchy(RecMgmt::UploadHierarchyResp());

	m_rmc.setConnection(m_url, m_token);

	RecMgmt::UploadHierarchyResp uhRes;
	if (downloadUploadHierarchy(uhRes, m_rmc, this)) {
		m_uploadModel.setHierarchy(uhRes);
		m_ui->uploadView->expandAll();
	}
}

void DlgRecordsManagementUpload::filterHierarchy(const QString &text)
{
	/* Store style at first invocation. */
	if (m_dfltFilerLineStyleSheet == SortFilterProxyModel::invalidFilterEditStyle) {
		m_dfltFilerLineStyleSheet = m_ui->filterLine->styleSheet();
	}

	m_uploadProxyModel.setFilterRole(UploadHierarchyModel::ROLE_FILTER);

#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
	m_uploadProxyModel.setFilterRegularExpression(
	    QRegularExpression(QRegularExpression::escape(text),
	        QRegularExpression::CaseInsensitiveOption));
#else /* < Qt-5.15 */
	m_uploadProxyModel.setFilterRegExp(QRegExp(text,
	    Qt::CaseInsensitive, QRegExp::FixedString));
#endif /* >= Qt-5.15 */

	m_uploadProxyModel.setFilterKeyColumn(0);

	m_ui->uploadView->expandAll();

	if (text.isEmpty()) {
		m_ui->filterLine->setStyleSheet(m_dfltFilerLineStyleSheet);
	} else if (m_uploadProxyModel.rowCount() != 0) {
		m_ui->filterLine->setStyleSheet(
		    SortFilterProxyModel::foundFilterEditStyle);
	} else {
		m_ui->filterLine->setStyleSheet(
		    SortFilterProxyModel::notFoundFilterEditStyle);
	}
}

void DlgRecordsManagementUpload::uploadHierarchySelectionChanged(void)
{
	m_selectedUploadIds.clear();

	const QModelIndexList indexes(
	    m_ui->uploadView->selectionModel()->selectedIndexes());

	/* Entries with empty identifiers should not be able to be selected. */
	foreach (const QModelIndex &index, indexes) {
		const QString uploadId(
		    index.data(UploadHierarchyModel::ROLE_ID).toString());
		if (!uploadId.isEmpty()) {
			m_selectedUploadIds.append(uploadId);
		} else {
			Q_ASSERT(0);
		}
	}

	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
	    !indexes.isEmpty());
}

void DlgRecordsManagementUpload::notifyCommunicationError(const QString &errMsg)
{
	QMessageBox::critical(this, tr("Communication Error"), errMsg);
}

void DlgRecordsManagementUpload::loadRecordsManagementPixmap(int width)
{
	if (Q_NULLPTR == GlobInstcs::recMgmtDbPtr) {
		return;
	}

	RecordsManagementDb::ServiceInfoEntry entry(
	    GlobInstcs::recMgmtDbPtr->serviceInfo());
	if (!entry.isValid() || entry.logoSvg.isEmpty()) {
		return;
	}
	QPixmap pixmap(Graphics::pixmapFromSvg(entry.logoSvg, width));
	if (!pixmap.isNull()) {
		m_ui->pixmapLabel->setPixmap(pixmap);
	}
}

/*!
 * @brief Collect non-empty upload hierarchy identifiers.
 *
 * @param[out] isSet Set of unique identifiers.
 * @param[in]  node Hierarchy root node.
 */
static
void collectHierarchyIdsRecursive(QSet<QString> &idSet,
    const RecMgmt::UploadHierarchyResp::NodeEntry *node)
{
	if (node == Q_NULLPTR) {
		return;
	}

	if (!node->id().isEmpty()) {
		idSet.insert(node->id());
	}

	foreach (const RecMgmt::UploadHierarchyResp::NodeEntry *n, node->sub()) {
		collectHierarchyIdsRecursive(idSet, n);
	}
}

/*!
 * @brief Check whether all supplied identifier exist within the hierarchy.
 *
 * @param[in] uhRes Upload hierarchy response.
 * @param[in] uploadIds Identifiers to search for.
 * @return False if some of the identifiers are not found within the hierarchy.
 */
static
bool allIdentifiersExist(const RecMgmt::UploadHierarchyResp &uhRes,
    const QStringList &uploadIds)
{
	QSet<QString> idSet;
	collectHierarchyIdsRecursive(idSet, uhRes.root());

	foreach (const QString uploadId, uploadIds) {
		if (!idSet.contains(uploadId)) {
			return false;
		}
	}

	return true;
}

bool DlgRecordsManagementUpload::checkUploadIdsExistence(
    const QStringList &uploadIds)
{
	if (Q_UNLIKELY(!m_selectedUploadIds.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	if (Q_UNLIKELY(uploadIds.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	m_rmc.setConnection(m_url, m_token);

	RecMgmt::UploadHierarchyResp uhRes;
	if (downloadUploadHierarchy(uhRes, m_rmc, this)) {
		if (!allIdentifiersExist(uhRes, uploadIds)) {
			QMessageBox::critical(this, tr("Unknown Hierarchy Identifier"),
			    tr("Some of the identifiers '%1' do not exist in the hierarchy.")
			        .arg(uploadIds.join(", ")));
			return false;
		}

		m_selectedUploadIds = uploadIds;
		return true;
	}

	return false;
}

/*!
 * @brief Process upload file service response.
 *
 * @note Stores location into database.
 * @note Uses non-modal notification dialogues in order not to block the event
 *     loop.
 *
 * @param[in] ifRes Response structure.
 * @param[in] dmId Message identifier.
 * @patam[in] parent Parent window for potential error dialogues.
 * @return True if response could be processed and location has been saved.
 */
static
bool processUploadFileResponse(const RecMgmt::UploadFileResp &ufRes,
    qint64 dmId, QWidget *parent = Q_NULLPTR)
{
	if (ufRes.id().isEmpty()) {
		QString errorMessage(
		    DlgRecordsManagementUpload::tr("Message '%1' could not be uploaded.")
		        .arg(dmId));
		errorMessage += QLatin1String("\n");
		errorMessage += DlgRecordsManagementUpload::tr("Received error") +
		    QLatin1String(": ") + ufRes.error().trVerbose();
		errorMessage += QLatin1String("\n");
		errorMessage += ufRes.error().description();

		DlgMsgBoxNonModal::critical(parent,
		    DlgRecordsManagementUpload::tr("File Upload Error"),
		    errorMessage);
		return false;
	}

#if defined (RM_ADDITIONAL_NOTIFICATIONS)
	/* Can block event loop, disable this to see the result. */
	DlgMsgBoxNonModal::information(parent,
	    DlgRecordsManagementUpload::tr("Successful File Upload"),
	    DlgRecordsManagementUpload::tr("Message '%1' was successfully uploaded into the records management service.").arg(dmId) +
	    QStringLiteral("\n") +
	    DlgRecordsManagementUpload::tr("It can be now found in the records management service in these locations:") +
	    QStringLiteral("\n") +
	    ufRes.locations().join(QStringLiteral("\n")));
#endif

	if (!ufRes.locations().isEmpty()) {
		logInfoNL(
		    "Message '%" PRId64 "'has been stored into records management service.",
		    UGLY_QINT64_CAST dmId);
		if (Q_NULLPTR != GlobInstcs::recMgmtDbPtr) {
			return GlobInstcs::recMgmtDbPtr->updateStoredMsg(dmId,
			    ufRes.locations());
		} else {
			Q_ASSERT(0);
			return true;
		}
	} else {
		logErrorNL(
		    "Received empty location list when uploading message '%" PRId64 "'.",
		    UGLY_QINT64_CAST dmId);
	}

	return false;
}

enum DlgRecordsManagementUpload::CommunicationResult DlgRecordsManagementUpload::uploadFile(
    RecMgmt::Connection &rmc, qint64 dmId, const QStringList &uploadIds,
    const QString &msgFileName, const QByteArray &msgData, QWidget *parent)
{
	RecMgmt::UploadFileReq ufReq(uploadIds, msgFileName, msgData);
	if (Q_UNLIKELY(!ufReq.isValid())) {
		Q_ASSERT(0);
		return COMM_ERROR;
	}

	QByteArray response;

	DlgRecordsManagementUploadProgress progressDlg(dmId, parent);
	progressDlg.show();

	const enum RecMgmt::Connection::CommunicationResult res = rmc.communicate(
	    RecMgmt::Connection::SRVC_UPLOAD_FILE, ufReq.toJsonData(),
	    response, &progressDlg);

	progressDlg.hide();

	if (res == RecMgmt::Connection::COMM_SUCCESS) {
		if (!response.isEmpty()) {
			bool ok = false;
			RecMgmt::UploadFileResp ufRes(
			    RecMgmt::UploadFileResp::fromJson(response, &ok));
			if (!ok || !ufRes.isValid()) {
				QMessageBox::critical(parent,
				    tr("Communication Error"),
				    tr("Received invalid response."));
				logErrorNL("Received invalid response '%s'.",
				    QString(response).toUtf8().constData());
				return COMM_ERROR;
			}

			return processUploadFileResponse(ufRes, dmId, parent) ? COMM_SUCCESS : COMM_ERROR;
		} else {
			QMessageBox::critical(parent, tr("Communication Error"),
			    tr("Received empty response."));
			return COMM_ERROR;
		}
	} else {
		return (res == RecMgmt::Connection::COMM_ERROR) ? COMM_ERROR : COMM_CANCELLED;
	}
}
