/*
 * Copyright (C) 2014-2026 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 <QDesktopServices>
#include <QFileDialog>
#include <QMimeDatabase>
#include <QMimeType>
#include <QProcess>

#include "src/datovka_shared/io/filesystem.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/utility/strings.h" /* generateRandomString */
#include "src/datovka_shared/settings/prefs.h"
#include "src/datovka_shared/utility/date_time.h"
#include "src/global.h"
#include "src/gui/message_operations.h"
#include "src/io/account_db.h"
#include "src/io/filesystem.h"
#include "src/io/message_db.h"
#include "src/io/message_db_set.h"
#include "src/settings/accounts.h"
#include "src/settings/prefs_specific.h"

/*!
 * @brief Appends email footer into \a message.
 *
 * @param[out] message Message body.
 */
static
void appedMessageFooter(QString &message)
{
	const QString newLine("\n"); /* "\r\n" ? */

	message += newLine;
	message += "-- " + newLine; /* Must contain the space. */
	message += " " + GuiMsgOps::tr("Created using Datovka") + " " VERSION "." + newLine;
	message += " <URL: " DATOVKA_HOMEPAGE_URL ">" + newLine;
}

/*!
 * @brief Sets email header and message body into \a message.
 *
 * @param[out] message Message body.
 * @param[in] subj Subject string.
 * @param[in] boundary Boundary string.
 */
static
void createEmailMessage(QString &message, const QString &subj,
    const QString &boundary)
{
	message.clear();

	const QString newLine("\n"); /* "\r\n" ? */

	/* Rudimentary header. */
	message += "Subject: " + subj + newLine;
	message += "MIME-Version: 1.0" + newLine;
	message += "Content-Type: multipart/mixed;" + newLine +
	    " boundary=\"" + boundary + "\"" + newLine;

	/* Body. */
	message += newLine;
	message += "--" + boundary + newLine;
	message += "Content-Type: text/plain; charset=UTF-8" + newLine;
	message += "Content-Transfer-Encoding: 8bit" + newLine;

	appedMessageFooter(message);
}

/*!
 * @brief Adds attachment into email message.
 *
 * @param[in,out] message Message body.
 * @param[in] attachName Attachment file name.
 * @param[in] base64 Attachment content in Base64.
 * @param[in] boundary Boundary string.
 */
static
void addAttachmentToEmailMessage(QString &message,
    const QString &attachName, const QByteArray &base64,
    const QString &boundary)
{
	const QString newLine("\n"); /* "\r\n" ? */

	QMimeDatabase mimeDb;

	QMimeType mimeType(
	    mimeDb.mimeTypeForData(QByteArray::fromBase64(base64)));

	message += newLine;
	message += "--" + boundary + newLine;
	message += "Content-Type: " + mimeType.name() + "; charset=UTF-8;" + newLine +
	    " name=\"" + attachName +  "\"" + newLine;
	message += "Content-Transfer-Encoding: base64" + newLine;
	message += "Content-Disposition: attachment;" + newLine +
	    " filename=\"" + attachName + "\"" + newLine;

	for (int i = 0; i < base64.size(); ++i) {
		if ((i % 60) == 0) {
			message += newLine;
		}
		message += base64.at(i);
	}
	message += newLine;
}

/*!
 * @brief Appends last line into email message.
 *
 * @param[in,out] message Message body.
 * @param[in] boundary Boundary string.
 */
static
void finishEmailMessage(QString &message, const QString &boundary)
{
	const QString newLine("\n"); /* "\r\n" ? */
	message += newLine + "--" + boundary + "--" + newLine;
}

void GuiMsgOps::exportSelectedData(const QList<MsgOrigin> &originList,
    enum Exports::ExportFileType expFileType, QWidget *parent)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	/* Just take the path from the first selected message. */
	QString exportPath = PrefsSpecific::acntZfoDir(
	    *GlobInstcs::prefsPtr, originList.at(0).acntIdDb);
	QString lastPath = exportPath;
	QString errStr;

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}
		const QString dbId = GlobInstcs::accntDbPtr->dbId(
		    AccountDb::keyFromLogin(origin.acntIdDb.username()));
		const QString accountName = GlobInstcs::acntMapPtr->acntData(
		    origin.acntIdDb).accountName();

		Exports::exportAsGUI(parent, *origin.acntIdDb.messageDbSet(),
		    expFileType, exportPath, QString(),
		    origin.acntIdDb.username(), accountName, dbId, origin.msgId,
		    true, lastPath, errStr);
		if (!lastPath.isEmpty()) {
			exportPath = lastPath;
			PrefsSpecific::setAcntZfoDir(*GlobInstcs::prefsPtr,
			    origin.acntIdDb, exportPath);
		}
	}
}

void GuiMsgOps::exportSelectedEnvelopeAndAttachments(
    const QList<MsgOrigin> &originList, QWidget *parent)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	/* Just take the path from the first selected message. */
	QString newDir = QFileDialog::getExistingDirectory(parent,
	    tr("Select Directory"),
	    PrefsSpecific::acntZfoDir(*GlobInstcs::prefsPtr, originList.at(0).acntIdDb),
	    QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
	if (newDir.isEmpty()) {
		return;
	}

	QString errStr;
	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}
		const QString dbId = GlobInstcs::accntDbPtr->dbId(
		    AccountDb::keyFromLogin(origin.acntIdDb.username()));
		const QString accountName = GlobInstcs::acntMapPtr->acntData(
		    origin.acntIdDb).accountName();

		Exports::exportEnvAndAttachments(
		    *origin.acntIdDb.messageDbSet(), newDir,
		    origin.acntIdDb.username(), accountName, dbId,
		    origin.msgId, errStr);

		PrefsSpecific::setAcntZfoDir(*GlobInstcs::prefsPtr,
		    origin.acntIdDb, newDir);
	}
}

/*!
 * @brief Construct a random boundary line.
 *
 * @return Boundary line string.
 */
static
QString emailBoundary(void)
{
	return "-----" + Utility::generateRandomString(16) + "_" +
	    QDateTime::currentDateTimeUtc().toString(Utility::dataTimeMSecsHyphenFormat);
}

/*!
 * @brief Build e-mail subject string according to number of messages.
 *
 * @param[in] originList List of messages to be worked with.
 * @return Subject string.
 */
static
QString msgCreateSubject(const QList<MsgOrigin> &originList)
{
	QString subject(((1 == originList.size()) ?
	    GuiMsgOps::tr("Data message") : GuiMsgOps::tr("Data messages")) + QLatin1String(": "));

	bool first = true;
	for (const MsgOrigin &origin : originList) {
		if (first) {
			subject += QString::number(origin.msgId.dmId());
			first = false;
		} else {
			subject += QLatin1String(", ") +
			    QString::number(origin.msgId.dmId());
		}
	}

	return subject;
}

/*!
 * @brief Creates temporary *.eml file with selected content and opens the file
 *     in a default application.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void msgBuildAndOpenEml(const QList<MsgOrigin> &originList,
    GuiMsgOps::EmailContents attchFlags)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	QString emailMessage;
	const QString boundary = emailBoundary();

	createEmailMessage(emailMessage,
	    msgCreateSubject(originList), boundary);

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb =
		    origin.acntIdDb.messageDbSet()->accessMessageDb(
		        origin.msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		/* ZFO message */
		if (attchFlags & GuiMsgOps::ADD_ZFO_MESSAGE) {

			const QByteArray base64 =
			    messageDb->getCompleteMessageBase64(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(base64.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("%1_%2.zfo")
			    .arg(Exports::dmTypePrefix(messageDb, origin.msgId.dmId()))
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			addAttachmentToEmailMessage(emailMessage,
			    attachName, base64, boundary);
		}

		/* ZFO delivery info */
		if (attchFlags & GuiMsgOps::ADD_ZFO_DELIVERY_INFO) {

			const QByteArray base64 =
			    messageDb->getDeliveryInfoBase64(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(base64.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("DO_%1.zfo")
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			addAttachmentToEmailMessage(emailMessage,
			    attachName, base64, boundary);
		}

		/* attachments */
		if (attchFlags & GuiMsgOps::ADD_ATTACHMENTS) {

			const QList<Isds::Document> attachList =
			    messageDb->getMessageAttachments(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(attachList.isEmpty())) {
				Q_ASSERT(0);
				return;
			}
			for (const Isds::Document &attach : attachList) {
				if (Q_UNLIKELY(attach.fileDescr().isEmpty() ||
				        attach.binaryContent().isEmpty())) {
					Q_ASSERT(0);
					continue;
				}
				addAttachmentToEmailMessage(emailMessage,
				    attach.fileDescr(),
				    attach.base64Content().toUtf8(),
				    boundary);
			}
		}

	}

	finishEmailMessage(emailMessage, boundary);

	/* Email is encoded using UTF-8. */
	QString tmpEmailFile = writeTemporaryFile(
	    TMP_ATTACHMENT_PREFIX "mail.eml", emailMessage.toUtf8());

	if (!tmpEmailFile.isEmpty()) {
		QDesktopServices::openUrl(QUrl::fromLocalFile(tmpEmailFile));
	}
}

/*!
 * @brief Call external command in a separate process.
 *     Waits for command to finish.
 *
 * @param[in] command Command name.
 * @param[in] args Command arguments.
 * @return True on success, false on any error.
 */
static
bool callProcess(const QString &command, const QStringList &args)
{
	if (Q_UNLIKELY(command.isEmpty())) {
		return false;
	}

	int haveError = false;
	QProcess::ProcessError processError = QProcess::UnknownError;

	QProcess proc;

#if (QT_VERSION < QT_VERSION_CHECK(5, 6, 0))
	/*
	 * QProcess::error() is an overloaded method in Qt-5.x.
	 * It is a signal and a method.
	 * Let the compiler get the pointer to the signal.
	 *
	 * The construction
	 * QOverload<QProcess::ProcessError>::of(&QProcess::error)
	 * cannot be used as it was introduced in Qt-5.7.
	 */
	void (QProcess::*fp)(QProcess::ProcessError) = &QProcess::error;
#endif /* < Qt-5.6 */


	/* Watch for errors. */
	QObject::connect(&proc,
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
	    &QProcess::errorOccurred,
#else /* < Qt-5.6 */
	    fp,
#endif /* >= Qt-5.6 */
	    [=, &haveError, &processError](QProcess::ProcessError error) { haveError = true; processError = error; });

	proc.setProgram(command);
	proc.setArguments(args);
	proc.start();

	proc.waitForFinished(-1);

	const bool runOk = (QProcess::NormalExit == proc.exitStatus()) && (!haveError);
	if (Q_UNLIKELY(!runOk)) {
		logErrorNL("Cannot execute command '%s'.",
		    (command + " " + args.join(" ")).toUtf8().constData());
	}
	return runOk;
}

#define XDG_EMAIL "xdg-email"

/*!
 * @brief Check the availability of xdg-email.
 *
 * @return True if command found.
 */
static
bool useXdgEmail(void)
{
#if !defined(Q_OS_MACOS) && (defined(Q_OS_UNIX) || defined(Q_OS_LINUX))
	int emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
	{
		qint64 val = 0;
		if (GlobInstcs::prefsPtr->intVal("action.email.creation_manner", val)) {
			switch (val) {
			case PrefsSpecific::EMAIL_CREATE_DEFAULT:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			case PrefsSpecific::EMAIL_CREATE_EML_FILE:
				emailCreation = PrefsSpecific::EMAIL_CREATE_EML_FILE;
				break;
			default:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			}
		}
	}

	if (PrefsSpecific::EMAIL_CREATE_DEFAULT == emailCreation) {
		const QString command(XDG_EMAIL);
		bool found = callProcess(command, {"--version"});
		logInfoNL("Command '%s' %s.", command.toUtf8().constData(),
		    found ? "found" : "not found");
		return found;
	}
#endif /* !Q_OS_MACOS && (Q_OS_UNIX || Q_OS_LINUX) */
	return false;
}

/*!
 * @brief Uses xdg-email to invoke an e-mail client and create an e-mail with
 *     specified content.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void msgCallXDGEmail(const QList<MsgOrigin> &originList,
    GuiMsgOps::EmailContents attchFlags)
{
	QString command = XDG_EMAIL;
	QStringList args = {"--subject", msgCreateSubject(originList)};

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb =
		    origin.acntIdDb.messageDbSet()->accessMessageDb(
		        origin.msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		/* ZFO message */
		if (attchFlags & GuiMsgOps::ADD_ZFO_MESSAGE) {

			const QByteArray data =
			    messageDb->getCompleteMessageRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("%1_%2.zfo")
			    .arg(Exports::dmTypePrefix(messageDb, origin.msgId.dmId()))
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			const QString attachAbsPath =
			    tmpDirPath + QDir::separator() + attachName;

			enum WriteFileState state = writeFile(attachAbsPath, data, true);
			if (WF_SUCCESS == state) {
				args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
			}
		}

		/* ZFO delivery info */
		if (attchFlags & GuiMsgOps::ADD_ZFO_DELIVERY_INFO) {

			const QByteArray data =
			    messageDb->getDeliveryInfoRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("DO_%1.zfo")
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			const QString attachAbsPath =
			    tmpDirPath + QDir::separator() + attachName;

			enum WriteFileState state = writeFile(attachAbsPath, data, true);
			if (WF_SUCCESS == state) {
				args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
			}
		}

		/* attachments */
		if (attchFlags & GuiMsgOps::ADD_ATTACHMENTS) {

			const QList<Isds::Document> attachList =
			    messageDb->getMessageAttachments(origin.msgId.dmId());
			if (Q_UNLIKELY(attachList.isEmpty())) {
				Q_ASSERT(0);
				return;
			}
			int fileNumber = 0;
			for (const Isds::Document &attach : attachList) {
				if (Q_UNLIKELY(attach.fileDescr().isEmpty() ||
				        attach.binaryContent().isEmpty())) {
					Q_ASSERT(0);
					continue;
				}

				const QString subirPath = tmpDirPath
				    + QDir::separator()
				    + QString("%1_%2")
				        .arg(origin.msgId.dmId())
				        .arg(fileNumber++);
				{
					/*
					 * Create a separate subdirectory because the files
					 * may have equal names.
					 */
					QDir dir(tmpDirPath);
					if (Q_UNLIKELY(!dir.mkpath(subirPath))) {
						logError("Could not create directory '%s'.",
						    subirPath.toUtf8().constData());
						return;
					}
				}

				const QString attachAbsPath =
				    subirPath + QDir::separator() + attach.fileDescr();

				enum WriteFileState state = writeFile(attachAbsPath, attach.binaryContent(), true);
				if (WF_SUCCESS == state) {
					args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
				}
			}
		}

	}

	{
		QString body;
		appedMessageFooter(body);

		args += {"--body", body};
	}

	callProcess(command, args);
}

void GuiMsgOps::createEmail(const QList<MsgOrigin> &originList,
    EmailContents attchFlags)
{
	if (!useXdgEmail()) {
		msgBuildAndOpenEml(originList, attchFlags);
	} else {
		msgCallXDGEmail(originList, attchFlags);
	}
}

/*!
 * @brief Build e-mail subject string, according to number of attachments.
 *
 * @param[in] attachOrigin List of attachments to be worked with.
 * @return Subject string.
 */
static
QString attachCreateSubject(const AttachOrigin &attachOrigin)
{
	return ((1 == attachOrigin.attachData.size())
	    ? GuiMsgOps::tr("Attachment of message %1")
	    : GuiMsgOps::tr("Attachments of message %1"))
	        .arg(attachOrigin.msgId.dmId());
}

/*!
 * @brief Creates temporary *.eml file with selected content and opens the file
 *     in a default application.
 *
 * @param[in] attachOrigin List of attachments to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void attachBuildAndOpenEml(const AttachOrigin &attachOrigin)
{
	if (Q_UNLIKELY(attachOrigin.attachData.isEmpty())) {
		return;
	}

	QString emailMessage;
	const QString boundary = emailBoundary();

	createEmailMessage(emailMessage,
	    attachCreateSubject(attachOrigin), boundary);

	for (const QPair<QString, QByteArray> &pair : attachOrigin.attachData) {
		const QString &fileName = pair.first;
		const QByteArray &data = pair.second;

		addAttachmentToEmailMessage(emailMessage,
		    fileName, data.toBase64(), boundary);
	}

	finishEmailMessage(emailMessage, boundary);

	QString tmpEmailFile = writeTemporaryFile(
	    TMP_ATTACHMENT_PREFIX "mail.eml", emailMessage.toUtf8());

	if (!tmpEmailFile.isEmpty()) {
		QDesktopServices::openUrl(QUrl::fromLocalFile(tmpEmailFile));
	}
}

/*!
 * @brief Uses xdg-email to invoke an e-mail client and create an e-mail with
 *     specified content.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void attachCallXDGEmail(const AttachOrigin &attachOrigin)
{
	QString command = XDG_EMAIL;
	QStringList args = {"--subject", attachCreateSubject(attachOrigin)};

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	int fileNumber = 0;
	for (const QPair<QString, QByteArray> &pair : attachOrigin.attachData) {
		const QString &fileName = pair.first;
		const QByteArray &data = pair.second;

		const QString subirPath = tmpDirPath
		    + QDir::separator()
		    + QString("%1_%2")
		        .arg(attachOrigin.msgId.dmId())
		        .arg(fileNumber++);
		{
			/*
			 * Create a separate subdirectory because the files
			 * may have equal names.
			 */
			QDir dir(tmpDirPath);
			if (Q_UNLIKELY(!dir.mkpath(subirPath))) {
				logError("Could not create directory '%s'.",
				    subirPath.toUtf8().constData());
				return;
			}
		}

		const QString attachAbsPath =
		    subirPath + QDir::separator() + fileName;

		enum WriteFileState state = writeFile(attachAbsPath, data, true);
		if (WF_SUCCESS == state) {
			args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
		}
	}

	{
		QString body;
		appedMessageFooter(body);

		args += {"--body", body};
	}

	callProcess(command, args);
}

void GuiMsgOps::createEmailAttachments(const AttachOrigin &attachOrigin)
{
	if (!useXdgEmail()) {
		attachBuildAndOpenEml(attachOrigin);
	} else {
		attachCallXDGEmail(attachOrigin);
	}
}
