/*
 * 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 <QThread>

#include "src/datovka_shared/isds/error.h"
#include "src/datovka_shared/isds/message_interface2.h"
#include "src/datovka_shared/isds/type_description.h"
#include "src/datovka_shared/isds/types.h"
#include "src/datovka_shared/log/log.h"
#include "src/global.h"
#include "src/io/dbs.h"
#include "src/io/isds_sessions.h"
#include "src/io/message_db.h"
#include "src/io/message_db_set.h"
#include "src/isds/message_conversion.h"
#include "src/isds/services.h"
#include "src/settings/accounts.h"
#include "src/worker/message_emitter.h"
#include "src/worker/task_download_message.h"

TaskDownloadMessage::TaskDownloadMessage(const AcntIdDb &acntIdDb,
    enum MessageDirection msgDirect, const MsgId &msgId, bool storeRawInDb,
    int processFlags, const QString &recMgmtHierarchyId)
    : m_result(DM_ERR),
    m_isdsError(),
    m_isdsLongError(),
    m_mId(msgId),
    m_acntIdDb(acntIdDb),
    m_msgDirect(msgDirect),
    m_storeRawInDb(storeRawInDb),
    m_processFlags(processFlags),
    m_recMgmtHierarchyId(recMgmtHierarchyId)
{
	Q_ASSERT(m_acntIdDb.isValid());
	Q_ASSERT(0 <= msgId.dmId());
}

void TaskDownloadMessage::run(void)
{
	if (Q_UNLIKELY(!m_acntIdDb.isValid())) {
		Q_ASSERT(0);
		return;
	}

	if (Q_UNLIKELY((MSG_RECEIVED != m_msgDirect) && (MSG_SENT != m_msgDirect))) {
		Q_ASSERT(0);
		return;
	}

	if (Q_UNLIKELY(0 > m_mId.dmId())) {
		Q_ASSERT(0);
		return;
	}

	logDebugLv0NL("Starting download message task in thread '%p'",
	    (void *) QThread::currentThreadId());

	/* ### Worker task begin. ### */

	const AcntId acntId(m_acntIdDb);

	logDebugLv1NL("%s", "-----------------------------------------------");
	logDebugLv1NL("Downloading %s message '%" PRId64 "' for account '%s'.",
	    (MSG_RECEIVED == m_msgDirect) ? "received" : "sent",
	    UGLY_QINT64_CAST m_mId.dmId(),
	    GlobInstcs::acntMapPtr->acntData(acntId).accountName().toUtf8().constData());
	logDebugLv1NL("%s", "-----------------------------------------------");

	m_result = downloadAndStoreSignedMessage(m_acntIdDb, m_mId, m_msgDirect,
	    m_storeRawInDb, m_isdsError, m_isdsLongError, PL_DOWNLOAD_MESSAGE);

	if (DM_SUCCESS == m_result) {
		logDebugLv1NL(
		    "Done downloading message '%" PRId64 "' for account '%s'.",
		    UGLY_QINT64_CAST m_mId.dmId(),
		    GlobInstcs::acntMapPtr->acntData(acntId).accountName().toUtf8().constData());
	} else {
		logErrorNL("Downloading message '%" PRId64 "' for account '%s' failed.",
		    UGLY_QINT64_CAST m_mId.dmId(),
		    GlobInstcs::acntMapPtr->acntData(acntId).accountName().toUtf8().constData());
	}

	emit GlobInstcs::msgProcEmitterPtr->downloadMessageFinished(acntId,
	    m_mId.dmId(), m_mId.deliveryTime(), m_result,
	    (!m_isdsError.isEmpty() || !m_isdsLongError.isEmpty()) ?
	        m_isdsError + " " + m_isdsLongError : "",
	    m_processFlags, m_recMgmtHierarchyId);

	emit GlobInstcs::msgProcEmitterPtr->progressChange(PL_IDLE, 0);

	/* ### Worker task end. ### */

	logDebugLv0NL("Download message task finished in thread '%p'",
	    (void *) QThread::currentThreadId());
}

/*!
 * @brief Download signed message.
 *
 * @param[in,out] session Communication session.
 * @param[in]     msgDirect Received or sent message.
 * @param[in]     dmId Message identifier.
 * @param[in]     isVodz True if message is a VoDZ.
 * @param[out]    message Downloaded message.
 * @param[out]    error Error description.
 * @param[out]    longError Long error description.
 * @return Error state.
 */
static
enum TaskDownloadMessage::Result downloadSignedMessage(Isds::Session *session,
    enum MessageDirection msgDirect, qint64 dmId, bool isVodz,
    Isds::Message &message, QString &error, QString &longError)
{
	if (Q_UNLIKELY(Q_NULLPTR == session)) {
		logErrorNL("%s", "Missing active session.");
		return TaskDownloadMessage::DM_ERR;
	}

	Isds::Error err;
	if (isVodz) {
		err = (MSG_RECEIVED == msgDirect) ?
		    Isds::Service::signedReceivedBigMessageDownload(session, dmId, message) :
		    Isds::Service::signedSentBigMessageDownload(session, dmId, message);
	} else {
		err = (MSG_RECEIVED == msgDirect) ?
		    Isds::Service::signedReceivedMessageDownload(session, dmId, message) :
		    Isds::Service::signedSentMessageDownload(session, dmId, message);
	}

	if (Q_UNLIKELY((err.code() != Isds::Type::ERR_SUCCESS) ||
	        (message.isNull()) || (message.envelope().isNull()))) {
		error = Isds::Description::descrError(err.code());
		longError = err.longDescr();
		logErrorNL("Downloading message returned status %d: '%s' '%s'.",
		    err.code(), error.toUtf8().constData(),
		    longError.toUtf8().constData());
		return TaskDownloadMessage::DM_ISDS_ERROR;
	}

	return TaskDownloadMessage::DM_SUCCESS;
}

/*!
 * @brief Download signed delivery info.
 *
 * @param[in,out] session Communication session.
 * @param[in]     dmId Message identifier.
 * @param[out]    delInfo Downloaded delivery info.
 * @param[out]    error Error description.
 * @param[out]    longError Long error description.
 * @return Error state.
 */
static
enum TaskDownloadMessage::Result downloadSignedDeliveryInfo(
    Isds::Session *session, qint64 dmId, Isds::Message &delInfo,
    QString &error, QString &longError)
{
	if (Q_UNLIKELY(Q_NULLPTR == session)) {
		logErrorNL("%s", "Missing active session.");
		return TaskDownloadMessage::DM_ERR;
	}

	Isds::Error err = Isds::Service::getSignedDeliveryInfo(session, dmId,
	    delInfo);

	if (Q_UNLIKELY(err.code() != Isds::Type::ERR_SUCCESS)) {
		error = Isds::Description::descrError(err.code());
		longError = err.longDescr();
		logErrorNL(
		    "Downloading delivery information returned status %d: '%s'.",
		    err.code(), error.toUtf8().constData());
		return TaskDownloadMessage::DM_ISDS_ERROR;
	}

	return TaskDownloadMessage::DM_SUCCESS;
}

/*!
 * @brief Download additional info about the message author (sender).
 *
 * @param[in,out] session Communication session.
 * @param[in]     dmId Message identifier.
 * @param[out]    dmMessageAuthor Message author info.
 * @param[out]    error Error description.
 * @param[out]    longError Long error description.
 * @return Error state.
 */
static
enum TaskDownloadMessage::Result downloadMessageAuthor2(Isds::Session *session,
    qint64 dmId, Isds::DmMessageAuthor &dmMessageAuthor, QString &error,
    QString &longError)
{
	if (Q_UNLIKELY(Q_NULLPTR == session)) {
		logErrorNL("%s", "Missing active session.");
		return TaskDownloadMessage::DM_ERR;
	}

	Isds::Error err = Isds::Service::getMessageAuthor2(session, dmId,
	    dmMessageAuthor);

	if (Q_UNLIKELY(err.code() != Isds::Type::ERR_SUCCESS)) {
		error = Isds::Description::descrError(err.code());
		longError = err.longDescr();
		logErrorNL(
		    "Downloading author information returned status %d: '%s'.",
		    err.code(), error.toUtf8().constData());
		return TaskDownloadMessage::DM_ISDS_ERROR;
	}

	return TaskDownloadMessage::DM_SUCCESS;
}

enum TaskDownloadMessage::Result TaskDownloadMessage::downloadAndStoreSignedDeliveryInfo(
    const AcntIdDb &acntIdDb, qint64 dmId, bool storeRawInDb,
    QString &error, QString &longError)
{
	debugFuncCall();

	Isds::Message delInfo;

	enum TaskDownloadMessage::Result ret = downloadSignedDeliveryInfo(
	    GlobInstcs::isdsSessionsPtr->session(acntIdDb.username()),
	    dmId, delInfo, error, longError);
	if (Q_UNLIKELY(ret != DM_SUCCESS)) {
		return ret;
	}

	Q_ASSERT(!delInfo.isNull());
	qdatovka_error err = Task::storeSignedDeliveryInfo(
	    *acntIdDb.messageDbSet(), delInfo, storeRawInDb);
	if (Q_UNLIKELY(err != Q_SUCCESS)) {
		return DM_DB_INS_ERR;
	}

	return DM_SUCCESS;
}

/*!
 * @brief Set message as downloaded from ISDS.
 *
 * TODO -- Is there a way how to download the information about read
 *     messages and apply it on the database?
 *
 * @param[in,out] session Communication session.
 * @param[in]     dmId Message identifier.
 * @param[out]    error Error description.
 * @param[out]    longError Long error description.
 * @return Error state.
 */
static
enum TaskDownloadMessage::Result markMessageAsDownloaded(
    Isds::Session *session, qint64 dmId, QString &error, QString &longError)
{
	debugFuncCall();

	if (Q_UNLIKELY(Q_NULLPTR == session)) {
		logErrorNL("%s", "Missing active session.");
		return TaskDownloadMessage::DM_ERR;
	}

	Isds::Error err = Isds::Service::markMessageAsDownloaded(session, dmId);

	if (Q_UNLIKELY(err.code() != Isds::Type::ERR_SUCCESS)) {
		error = Isds::Description::descrError(err.code());
		longError = err.longDescr();
		logErrorNL(
		    "Marking message as downloaded returned status %d: '%s'.",
		    err.code(), error.toUtf8().constData());
		return TaskDownloadMessage::DM_ISDS_ERROR;
	}

	return TaskDownloadMessage::DM_SUCCESS;
}

enum TaskDownloadMessage::Result TaskDownloadMessage::downloadAndStoreSignedMessage(
    const AcntIdDb &acntIdDb, MsgId &mId, enum MessageDirection msgDirect,
    bool storeRawInDb, QString &error, QString &longError,
    const QString &progressLabel)
{
#define USE_EXPLICIT_TRANSACTION 1
#define IMPLICIT_TRANSACTION false /* Must be disabled when using explicit transactions. */
#define TRANSACTION_ENABLE true
	debugFuncCall();

	logDebugLv0NL("Trying to download complete message '%" PRId64 "'",
	    UGLY_QINT64_CAST mId.dmId());

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 0);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		return DM_ABORTED;
	}

	Isds::Session *session = GlobInstcs::isdsSessionsPtr->session(acntIdDb.username());
	if (Q_UNLIKELY(Q_NULLPTR == session)) {
		logErrorNL("Missing active session for username '%s'.",
		    acntIdDb.username().toUtf8().constData());
		return DM_ERR;
	}

	/* Download data without delays. */
	Isds::Message message;
	Isds::Message delInfo;
	Isds::DmMessageAuthor dmMsgAuthor;

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 10);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		return DM_ABORTED;
	}

	MessageDbSet *dbSet = acntIdDb.messageDbSet();
	MessageDb *messageDb = dbSet->accessMessageDb(mId.deliveryTime(), false);
	if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
		Q_ASSERT(0);
		logErrorNL("Cannot access database to store message '%" PRId64 "'.",
		    UGLY_QINT64_CAST mId.dmId());
		emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 100);
		return DM_DB_INS_ERR;
	}
	bool isVodz = messageDb->isVodz(mId.dmId());

	enum Result msgRes = downloadSignedMessage(session, msgDirect,
	    mId.dmId(), isVodz, message, error, longError);

	if (Q_UNLIKELY(msgRes != DM_SUCCESS)) {
		return msgRes;
	}

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 20);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		return DM_ABORTED;
	}

	enum Result delInfoRes = downloadSignedDeliveryInfo(session, mId.dmId(),
	    delInfo, error, longError);
	if (Q_UNLIKELY(delInfoRes != DM_SUCCESS)) {
		logErrorNL("Cannot download delivery info for message '%" PRId64 "'.",
		    UGLY_QINT64_CAST mId.dmId());
	}

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 30);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		return DM_ABORTED;
	}

	enum Result sndrInfoRes = downloadMessageAuthor2(session, mId.dmId(),
	    dmMsgAuthor, error, longError);
	if (Q_UNLIKELY(sndrInfoRes != DM_SUCCESS)) {
		logErrorNL("Cannot download author information for message '%" PRId64 "'.",
		    UGLY_QINT64_CAST mId.dmId());
	}

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 40);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		return DM_ABORTED;
	}

	{
		QString secKeyBeforeDownload(dbSet->secondaryKey(mId.deliveryTime()));
		const QDateTime &newDeliveryTime(message.envelope().dmDeliveryTime());
		QString secKeyAfterDonwload(dbSet->secondaryKey(newDeliveryTime));

		/*
		 * Delivery time may be unknown (i.e. invalid).
		 */
		messageDb = dbSet->accessMessageDb(newDeliveryTime, true);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			logErrorNL("Cannot access database to store message '%" PRId64 "'.",
			    UGLY_QINT64_CAST mId.dmId());
			emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 100);
			return DM_DB_INS_ERR;
		}

#ifdef USE_EXPLICIT_TRANSACTION
		messageDb->lock();
		if (Q_UNLIKELY(!messageDb->beginTransaction())) {
			logErrorNL("Cannot begin transaction to store message '%" PRId64 "'.",
			    UGLY_QINT64_CAST mId.dmId());
			emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 100);
			messageDb->unlock();
			return DM_DB_INS_ERR;
		}
#endif /* USE_EXPLICIT_TRANSACTION */

		if (Q_UNLIKELY(GlobInstcs::req_halt)) {
			goto abort;
		}

		/*
		 * Secondary keys may be the same for valid and invalid
		 * delivery time when storing into single file.
		 */
		if (secKeyBeforeDownload != secKeyAfterDonwload) {
			/*
			 * The message was likely moved from invalid somewhere
			 * else.
			 */
			/*
			 * Delete the message from invalid.
			 * The method creates an internal transaction.
			 */
			MessageDb *invalidDb = dbSet->accessMessageDb(mId.deliveryTime(), false);
			if (Q_NULLPTR != invalidDb) {
				invalidDb->msgsDeleteMessageData(mId.dmId(), TRANSACTION_ENABLE);
			}

			/* Store envelope in new location. */
			Task::storeMessageEnvelope(IMPLICIT_TRANSACTION,
			    msgDirect, *dbSet, message.envelope());
		}
		/* Update message delivery time. */
		mId.setDeliveryTime(newDeliveryTime);
	}

	Q_ASSERT(message.envelope().dmId() == mId.dmId());

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 50);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		goto abort;
	}

	/* Store the message. */
	Task::storeSignedMessage(IMPLICIT_TRANSACTION, msgDirect, *dbSet,
	    message, storeRawInDb);

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 70);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		goto abort;
	}

	/* Save delivery info and message events. */
	if (delInfoRes == DM_SUCCESS) {
		qdatovka_error err = Task::storeSignedDeliveryInfo(*dbSet,
		    delInfo, storeRawInDb);
		if (err == Q_SUCCESS) {
			logDebugLv0NL(
			    "Delivery info of message '%" PRId64 "' saved.",
			    UGLY_QINT64_CAST mId.dmId());
		} else {
			logErrorNL(
			    "Saving delivery info of message '%" PRId64 "' failed.",
			    UGLY_QINT64_CAST mId.dmId());
		}
	}

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 80);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		goto abort;
	}

	/* Save author information. */
	if (sndrInfoRes == DM_SUCCESS) {
		if (messageDb->updateMessageAuthorInfo2(mId.dmId(), dmMsgAuthor)) {
			logDebugLv0NL(
			    "Author information of message '%" PRId64 "' were updated.",
			    UGLY_QINT64_CAST mId.dmId());
		} else {
			logErrorNL(
			    "Updating author information of message '%" PRId64 "' failed.",
			    UGLY_QINT64_CAST mId.dmId());
		}
	}

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 90);

	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		goto abort;
	}

	if (MSG_RECEIVED == msgDirect) {
		/*  Mark this message as downloaded in ISDS */
		if (DM_SUCCESS == markMessageAsDownloaded(session,
		        mId.dmId(), error, longError)) {
			logDebugLv0NL(
			    "Message '%" PRId64 "' marked as downloaded.",
			    UGLY_QINT64_CAST mId.dmId());
		} else {
			logErrorNL(
			    "Marking message '%" PRId64 "' as downloaded failed.",
			    UGLY_QINT64_CAST mId.dmId());
		}
	}

#ifdef USE_EXPLICIT_TRANSACTION
	messageDb->commitTransaction();
	messageDb->unlock();
#endif /* USE_EXPLICIT_TRANSACTION */

	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 100);

	logDebugLv0NL("Done with %s().", __func__);

	return DM_SUCCESS;

abort:
#ifdef USE_EXPLICIT_TRANSACTION
	messageDb->rollbackTransaction();
	messageDb->unlock();
#endif /* USE_EXPLICIT_TRANSACTION */
	emit GlobInstcs::msgProcEmitterPtr->progressChange(progressLabel, 100);
	return DM_ABORTED;

#undef USE_EXPLICIT_TRANSACTION
#undef IMPLICIT_TRANSACTION
#undef TRANSACTION_ENABLE
}
