/*
 * Copyright (C) 2014-2022 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 <QFileDialog>
#include <QPrinter>
#include <QTextDocument>

#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/prefs.h"
#include "src/global.h"
#include "src/io/exports.h"
#include "src/io/filesystem.h"
#include "src/io/message_db.h"
#include "src/io/message_db_set.h"

#ifdef Q_COMPILER_RVALUE_REFS
#  define macroStdMove(x) ::std::move(x)
#else /* Q_COMPILER_RVALUE_REFS */
#  define macroStdMove(x) (x)
#endif /* Q_COMPILER_RVALUE_REFS */

QString Exports::attachmentSavePathWithFileName(const MessageDbSet &dbSet,
    const QString &targetPath, const QString &attachName,
    const QString &dbId, const QString &userName, const QString &accountName,
    const MsgId &msgId, bool prohibitDirSep)
{
	debugFuncCall();

	if (Q_UNLIKELY(userName.isEmpty())) {
		Q_ASSERT(0);
		return QString();
	}
	if (Q_UNLIKELY(msgId.dmId() < 0)) {
		Q_ASSERT(0);
		return QString();
	}

	MessageDb *messageDb = dbSet.constAccessMessageDb(msgId.deliveryTime());
	if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
		Q_ASSERT(0);
		return QString();
	}

	MessageDb::FilenameEntry entry =
	    messageDb->msgsGetAdditionalFilenameEntry(msgId.dmId());

	QString attachFilenameFmt;
	GlobInstcs::prefsPtr->strVal("file_system.format.filename.attachment",
	    attachFilenameFmt);

	QString fileName = fileSubpathFromFormat(
	    attachFilenameFmt, prohibitDirSep,
	    msgId.dmId(), dbId, userName, accountName, attachName,
	    entry.dmDeliveryTime, entry.dmAcceptanceTime, entry.dmAnnotation,
	    entry.dmSender, entry.dmRecipient);
	if (fileName.isEmpty()) {
		return QString();
	}
	return targetPath + QDir::separator() + fileName;
}

enum Exports::ExportError Exports::exportAs(QWidget *parent,
    const MessageDbSet &dbSet, enum ExportFileType fileType,
    const QString &targetPath, const QString &attachFileName,
    const QString &userName, const QString &accountName, const QString &dbId,
    const MsgId &msgId, bool askLocation, QString &lastPath, QString &errStr)
{
	debugFuncCall();

	if (Q_UNLIKELY(userName.isEmpty())) {
		Q_ASSERT(0);
		return EXP_ERROR;
	}
	if (Q_UNLIKELY(msgId.dmId() < 0)) {
		Q_ASSERT(0);
		return EXP_ERROR;
	}

	QString fileTypeStr;
	QString fileSufix;
	QByteArray base64;
	QString fileNameformat;
	bool ret = false;
	QString msgID = QString::number(msgId.dmId());

	MessageDb *messageDb = dbSet.constAccessMessageDb(msgId.deliveryTime());
	if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
		Q_ASSERT(0);
		errStr = tr("Cannot access message database for username '%1'.")
		    .arg(userName);
		return EXP_DB_ERROR;
	}

	// what we will export?
	switch (fileType) {
	case ZFO_MESSAGE:
		fileTypeStr = tr("message");
		fileSufix = ".zfo";
		GlobInstcs::prefsPtr->strVal(
		    "file_system.format.filename.message", fileNameformat);
		base64 = messageDb->getCompleteMessageBase64(msgId.dmId());
		break;
	case ZFO_DELIVERY:
		fileTypeStr = tr("acceptance info");
		fileSufix = ".zfo";
		GlobInstcs::prefsPtr->strVal(
		    "file_system.format.filename.acceptance_info", fileNameformat);
		base64 = messageDb->getDeliveryInfoBase64(msgId.dmId());
		break;
	case ZFO_DELIV_ATTACH:
		fileTypeStr = tr("acceptance info");
		fileSufix = ".zfo";
		GlobInstcs::prefsPtr->strVal(
		    "file_system.format.filename.every_attachment_acceptance_info",
		    fileNameformat);
		base64 = messageDb->getDeliveryInfoBase64(msgId.dmId());
		break;
	case PDF_DELIVERY:
		fileTypeStr = tr("acceptance info");
		fileSufix = ".pdf";
		GlobInstcs::prefsPtr->strVal(
		    "file_system.format.filename.acceptance_info", fileNameformat);
		base64 = messageDb->getDeliveryInfoBase64(msgId.dmId());
		break;
	case PDF_DELIV_ATTACH:
		fileTypeStr = tr("acceptance info");
		fileSufix = ".pdf";
		GlobInstcs::prefsPtr->strVal(
		    "file_system.format.filename.every_attachment_acceptance_info",
		    fileNameformat);
		base64 = messageDb->getDeliveryInfoBase64(msgId.dmId());
		break;
	case PDF_ENVELOPE:
		fileTypeStr = tr("message envelope");
		fileSufix = ".pdf";
		GlobInstcs::prefsPtr->strVal(
		    "file_system.format.filename.message", fileNameformat);
		base64 = "n/a";
		break;
	default:
		Q_ASSERT(0);
		errStr = tr("Export file type of message '%1' was not specified!")
		    .arg(msgID);
		return EXP_ERROR;
		break;
	}

	// is complete message in database?
	if (base64.isEmpty()) {
		errStr = tr("Complete message '%1' missing!").arg(msgID);
		return EXP_NOT_MSG_DATA;
	}

	errStr = tr("Export of %1 '%2' to %3 was not successful!")
	    .arg(fileTypeStr).arg(msgID).arg(fileSufix);

	MessageDb::FilenameEntry entry =
	    messageDb->msgsGetAdditionalFilenameEntry(msgId.dmId());

	bool prohibitDirSep = askLocation;

	/*
	 * Create new file name with format string. Allow subdirectories
	 * if the user should not be asked after location.
	 */
	QString fileName = fileSubpathFromFormat(fileNameformat, prohibitDirSep,
	    msgId.dmId(), dbId, userName, accountName, attachFileName,
	    entry.dmDeliveryTime, entry.dmAcceptanceTime, entry.dmAnnotation,
	    entry.dmSender, entry.dmRecipient);

	fileName = targetPath + QDir::separator() + fileName + fileSufix;

	if (Q_UNLIKELY(fileName.isEmpty())) {
		Q_ASSERT(0);
		return EXP_ERROR;
	}

	/* Ask for a new location. */
	if (askLocation) {
		fileName = QFileDialog::getSaveFileName(parent,
		    tr("Save %1 as file (*%2)").arg(fileTypeStr).arg(fileSufix),
		    fileName, tr("File (*%1)").arg(fileSufix));
		if (fileName.isEmpty()) {
			/* Export has been cancelled. */
			return EXP_CANCELED;
		}
	} else {
		createDirStructureRecursive(fileName);
	}

	if (Q_UNLIKELY(fileName.isEmpty())) {
		Q_ASSERT(0);
		return EXP_ERROR;
	}

	// save file to disk
	switch (fileType) {
	case ZFO_DELIV_ATTACH:
	case ZFO_DELIVERY:
	case ZFO_MESSAGE:
		ret = writeZFO(fileName, QByteArray::fromBase64(base64));
		break;
	case PDF_DELIVERY:
	case PDF_DELIV_ATTACH:
		ret = printPDF(fileName,
		    messageDb->deliveryInfoHtmlToPdf(msgId.dmId()));
		break;
	case PDF_ENVELOPE:
		ret = printPDF(fileName,
		    messageDb->envelopeInfoHtmlToPdf(msgId.dmId(), ""));
		break;
	default:
		Q_ASSERT(0);
		return EXP_ERROR;
		break;
	}

	if (ret) {
		// remember last path
		lastPath = QFileInfo(fileName).absoluteDir().absolutePath();
		errStr = tr("Export of %1 '%2' to %3 was successful.")
		    .arg(fileTypeStr).arg(msgID).arg(fileSufix);
		return EXP_SUCCESS;
	} else {
		return EXP_WRITE_FILE_ERROR;
	}
}

/*!
 * @brief Split the file name into the base name and full suffix.
 *
 * @param[in] fileName File name.
 * @return List containing the base name and the suffix if some supplied.
 */
static
QStringList splitSuffix(const QString &fileName)
{
	int dotPos = fileName.indexOf(QChar('.'));
	if (dotPos < 0) {
		return QStringList() << fileName;
	}

	return QStringList() << fileName.left(dotPos)
	    << fileName.right(fileName.size() - dotPos - 1);
}

/*!
 * @brief Construct a file name distinct from the supplied set content.
 *     Leaves suffix intact.
 *
 * @param[in] fileName Original file name.
 * @param[in] fileNames Set of already used names.
 * @param[in] caseSensitive Set to false if the letter case should be ignored.
 * @return New file name distinct form those in the set.
 */
static
QString setNonconflictingFileName(const QString &fileName,
    const QSet<QString> &fileNames, bool caseSensitive)
{
	if (Q_UNLIKELY(fileName.isEmpty())) {
		return QString();
	}

	if (fileNames.contains(caseSensitive ? fileName : fileName.toLower())) {

		int fileCnt = 0;

		QString baseName;
		QString suffix;
		{
			QStringList nameParts = splitSuffix(fileName);
			baseName = nameParts.at(0);
			if (nameParts.size() > 1) {
				suffix = nameParts.at(1);
			}
		}

		QString newFileName;
		do {
			++fileCnt;
			newFileName = baseName + QLatin1String("_") + QString::number(fileCnt);
			if (!suffix.isEmpty()) {
				newFileName += QChar('.') + suffix;
			}
		} while (fileNames.contains(caseSensitive ? newFileName : newFileName.toLower()));

		return newFileName;
	}

	return fileName;
}

/*!
 * @brief Different attachments of same message can have same file names. The
 *     function alters the names in order to make them distinct form each other.
 *
 * @param[in,out] documents Document list to be altered.
 * @param[in]     caseSensitive Set to false if the letter case should be ignored.
 */
static
void makeDocumentNamesDistinct(QList<Isds::Document> &documents,
    bool caseSensitive)
{
	QSet<QString> usedNames;

	QList<Isds::Document>::iterator it;
	for (it = documents.begin(); it != documents.end(); ++it) {
		QString fileName = setNonconflictingFileName(it->fileDescr(),
		    usedNames, caseSensitive);
		usedNames.insert(caseSensitive ? fileName : fileName.toLower());
		if (fileName != it->fileDescr()) {
			/* Replace document name. */
			it->setFileDescr(macroStdMove(fileName));
		}
	}
}

enum Exports::ExportError Exports::exportEnvAndAttachments(
    const MessageDbSet &dbSet, const QString &targetPath,
    const QString &userName, const QString &accountName, const QString &dbId,
    const MsgId &msgId, QString &errStr)
{
	debugFuncCall();

	if (Q_UNLIKELY(userName.isEmpty())) {
		Q_ASSERT(0);
		return EXP_ERROR;
	}
	if (Q_UNLIKELY(msgId.dmId() < 0)) {
		Q_ASSERT(0);
		return EXP_ERROR;
	}

	QString newTargetPath;
	QDir dir(targetPath);
	QString msgID = QString::number(msgId.dmId());
	bool attachWriteSuccess = true;

	// create new folder with message id
	dir.mkdir(QString::number(msgId.dmId()));
	newTargetPath = targetPath + QDir::separator() +
	    QString::number(msgId.dmId());

	MessageDb *messageDb = dbSet.constAccessMessageDb(msgId.deliveryTime());
	if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
		Q_ASSERT(0);
		errStr = tr("Cannot access message database for username '%1'.")
		    .arg(userName);
		return EXP_DB_ERROR;
	}

	MessageDb::FilenameEntry entry =
	    messageDb->msgsGetAdditionalFilenameEntry(msgId.dmId());

	/* Get list of attachment documents. */
	QList<Isds::Document> attachList =
	    messageDb->getMessageAttachments(msgId.dmId());
	if (Q_UNLIKELY(attachList.isEmpty())) {
		errStr = tr("Complete message '%1' is missing.").arg(msgID);
		return EXP_NOT_MSG_DATA;
	}

	/*
	 * Treat all FS as case-insensitive. This will make the life a little
	 * bit easier. Also for the user, as they may copy the generated files
	 * without any worries.
	 */
	makeDocumentNamesDistinct(attachList, false);

	/* Save attachments to target directory. */
	foreach (const Isds::Document &attach, attachList) {
		QString attName(attach.fileDescr());
		if (Q_UNLIKELY(attName.isEmpty())) {
			Q_ASSERT(0);
			errStr = tr("Some attachments of message '%1' couldn't be stored.")
			    .arg(msgID);
			attachWriteSuccess = false;
			continue;
		}

		/* Create complete file path according to format string. */
		attName = attachmentSavePathWithFileName(dbSet, newTargetPath,
		    attName, dbId, userName, accountName, msgId, false);

		/* Recursively create subdirectories. */
		createDirStructureRecursive(attName);

		const QByteArray &data(attach.binaryContent());

		/* Write file. */
		if (WF_SUCCESS != writeFile(attName, data)) {
			errStr = tr("Some files of message '%1' have not been written to storage.")
			    .arg(msgID);
			attachWriteSuccess = false;
			continue;
		}
	}

	QString msgFilenameFmt;
	GlobInstcs::prefsPtr->strVal("file_system.format.filename.message",
	    msgFilenameFmt);

	/* Create new file name according to the format string. */
	QString fileName = fileSubpathFromFormat(msgFilenameFmt, false,
	    msgId.dmId(), dbId, userName, accountName, QString(),
	    entry.dmDeliveryTime, entry.dmAcceptanceTime, entry.dmAnnotation,
	    entry.dmSender, entry.dmRecipient);

	fileName = newTargetPath + QDir::separator() + fileName + ".pdf";
	if (fileName.isEmpty()) {
		errStr = tr("Export of message envelope '%1' to PDF was not successful.")
		    .arg(msgID);
		return EXP_ERROR;
	}

	/* Recursively create subdirectories. */
	createDirStructureRecursive(fileName);

	/* Write file. */
	if (printPDF(fileName,
	    messageDb->descriptionHtml(msgId.dmId()) +
	    messageDb->fileListHtmlToPdf(msgId.dmId())) && attachWriteSuccess) {
		errStr = tr("All attachments of message '%1' and the envelope PDF have been successfully saved to target directory.")
		    .arg(msgID);
		return EXP_SUCCESS;
	} else {
		errStr = tr("The envelope PDF of message '%1' hasn't been saved.")
		    .arg(msgID);
		return EXP_WRITE_FILE_ERROR;
	}
}

enum Exports::ExportError Exports::saveAttachmentsWithExports(
    const MessageDbSet &dbSet, const QString &targetPath,
    const QString &userName, const QString &accountName, const QString &dbId,
    const MsgId &msgId, QString &errStr)
{
	debugFuncCall();

	QString lastPath;
	QString msgID = QString::number(msgId.dmId());
	bool attachWriteSuccess = true;

	MessageDb *messageDb = dbSet.constAccessMessageDb(msgId.deliveryTime());
	if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
		Q_ASSERT(0);
		errStr = tr("Cannot access message database for username '%1'.")
		    .arg(userName);
		return EXP_DB_ERROR;
	}

	/* Get list of attachment documents. */
	QList<Isds::Document> attachList =
	    messageDb->getMessageAttachments(msgId.dmId());
	if (Q_UNLIKELY(attachList.isEmpty())) {
		errStr = tr("Complete message '%1' is missing.").arg(msgID);
		return EXP_NOT_MSG_DATA;
	}

	/*
	 * Treat all FS as case-insensitive. This will make the life a little
	 * bit easier. Also for the user, as they may copy the generated files
	 * without any worries.
	 */
	makeDocumentNamesDistinct(attachList, false);

	bool exportMsgZfo = false;
	GlobInstcs::prefsPtr->boolVal(
	    "action.save_all_attachments.tie.action.export_message_zfo",
	    exportMsgZfo);
	bool exportAccInfoZfo = false;
	GlobInstcs::prefsPtr->boolVal(
	    "action.save_all_attachments.tie.action.export_acceptance_info_zfo",
	    exportAccInfoZfo);
	bool exportEnvelPdf = false;
	GlobInstcs::prefsPtr->boolVal(
	    "action.save_all_attachments.tie.action.export_envelope_pdf",
	    exportEnvelPdf);
	bool exportAccInfoPdf = false;
	GlobInstcs::prefsPtr->boolVal(
	    "action.save_all_attachments.tie.action.export_acceptance_info_pdf",
	    exportAccInfoPdf);
	bool accInfoForEveryFile = false;
	GlobInstcs::prefsPtr->boolVal(
	    "action.save_all_attachments.every_attachment.export_acceptance_info",
	    accInfoForEveryFile);

	/* Save attachments to target directory. */
	foreach (const Isds::Document &attach, attachList) {
		QString attName(attach.fileDescr());
		if (Q_UNLIKELY(attName.isEmpty())) {
			Q_ASSERT(0);
			errStr = tr("Some attachments of message '%1' couldn't be stored.")
			    .arg(msgID);
			attachWriteSuccess = false;
			continue;
		}

		/* Create complete file path according to format string. */
		attName = attachmentSavePathWithFileName(dbSet, targetPath,
		    attName, dbId, userName, accountName, msgId, false);

		/* Recursively create subdirectories. */
		createDirStructureRecursive(attName);

		const QByteArray &data(attach.binaryContent());

		/* Write file. */
		if (WF_SUCCESS != writeFile(attName, data)) {
			errStr = tr("Some files of message '%1' have not been written to storage.")
			    .arg(msgID);
			attachWriteSuccess = false;
			continue;
		}

		if (accInfoForEveryFile) {
			if (exportAccInfoZfo) {
				exportAs(Q_NULLPTR, dbSet, Exports::ZFO_DELIV_ATTACH,
				    targetPath, attach.fileDescr(), userName,
				    accountName, dbId, msgId, false, lastPath,
				    errStr);
			}
			if (exportAccInfoPdf) {
				exportAs(Q_NULLPTR, dbSet, Exports::PDF_DELIV_ATTACH,
				    targetPath, attach.fileDescr(), userName,
				    accountName, dbId, msgId, false, lastPath,
				    errStr);
			}
		}
	}

	if (exportMsgZfo) {
		exportAs(Q_NULLPTR, dbSet, Exports::ZFO_MESSAGE,
		    targetPath, QString(), userName, accountName, dbId, msgId,
		    false, lastPath, errStr);
	}

	if (exportEnvelPdf) {
		exportAs(Q_NULLPTR, dbSet, Exports::PDF_ENVELOPE,
		    targetPath, QString(), userName, accountName, dbId, msgId,
		    false, lastPath, errStr);
	}

	if (!accInfoForEveryFile) {
		if (exportAccInfoZfo) {
			exportAs(Q_NULLPTR, dbSet, Exports::ZFO_DELIVERY,
			    targetPath, QString(), userName, accountName, dbId,
			    msgId, false, lastPath, errStr);
		}
		if (exportAccInfoPdf) {
			exportAs(Q_NULLPTR, dbSet, Exports::PDF_DELIVERY,
			    targetPath, QString(), userName, accountName, dbId,
			    msgId, false, lastPath, errStr);
		}
	}

	if (attachWriteSuccess) {
		errStr = tr("All attachments of message '%1' have been successfully saved to target directory.")
		    .arg(msgID);
		return EXP_SUCCESS;
	} else {
		errStr = tr("Some attachments of message '%1' haven't been saved.")
		    .arg(msgID);
		return EXP_WRITE_FILE_ERROR;
	}
}

bool Exports::printPDF(const QString &fileName, const QString &data)
{
	if (data.isEmpty() || fileName.isEmpty()) {
		return false;
	}

	QTextDocument doc;
	doc.setHtml(data);
	QPrinter printer;
	printer.setOutputFileName(fileName);
	printer.setOutputFormat(QPrinter::PdfFormat);
	doc.print(&printer);

	return true;
}

bool Exports::writeZFO(const QString &fileName, const QByteArray &data)
{
	if (data.isEmpty() || fileName.isEmpty()) {
		return false;
	}
	return (WF_SUCCESS == writeFile(fileName, data));
}
