/*
 * 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 <QByteArray>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QString>
#include <QStringBuilder>
#include <QVariant> /* Qt-5.7 requires it. */

#include "src/datovka_shared/log/log.h"
#include "src/io/db_convert_mobile_to_desktop.h"
#include "src/io/message_db.h"
#include "src/io/message_db_set.h"
#include "src/io/message_db_set_container.h"
#include "src/io/message_db_single.h"
#include "src/isds/message_functions.h"

#define STORE_RAW_INTO_DB true

/*!
 * @brief Check if message database is for testing account.
 *
 * @param[in] dbBaseName Message database base file name.
 * @return True on success.
 */
static inline
bool isTesting(const QString &dbBaseName)
{
	return (dbBaseName.right(1) == "1");
}

/*!
 * @brief Read ZFO content from ZFO database.
 *
 * @param[in] zfoDb ZFO database.
 * @param[in] dmId Message id.
 * @param[in] isTestAccount Testing account.
 * @return Raw message data.
 */
static
QByteArray readZfoContentDb(QSqlDatabase &zfoDb,
    qint64 msgId, bool isTestAccount)
{
	QSqlQuery query(zfoDb);

	QString queryStr = "SELECT data FROM message_zfos WHERE "
	    "dmID = :msgId AND testEnv = :isTestAccount";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s",
		    query.lastError().text().toUtf8().constData());
		return QByteArray();
	}
	query.bindValue(":msgId", msgId); /* The constructor of QVariant needs to be known here. */
	query.bindValue(":isTestAccount", isTestAccount);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toByteArray();
	}

	return QByteArray();
}

/*!
 * @brief Read ZFO content from file.
 *
 * @param[in] zfoPath ZFO file path.
 * @return Raw message data.
 */
static
QByteArray readZfoContentFile(const QString &zfoPath)
{
	QFile file(zfoPath);
	if (!file.open(QIODevice::ReadOnly)) {
		return QByteArray();
	}
	QByteArray zfoContent(file.readAll());
	file.close();

	return zfoContent;
}

/*!
 * @brief Get ZFO message file name.
 *
 * @param[in] dmId Message id.
 * @param[in] msgDirect Message direction.
 * @return ZFO message file name.
 */
static
QString getMsgZfoName(qint64 dmId, enum MessageDirection msgDirect)
{
	switch (msgDirect) {
	case MessageDirection::MSG_RECEIVED:
		return QString("DDZ_%1.zfo").arg(dmId);
		break;
	case MessageDirection::MSG_SENT:
		return QString("ODZ_%1.zfo").arg(dmId);
		break;
	default:
		return QString("DZ_%1.zfo").arg(dmId);
		break;
	}
}

/*!
 * @brief Get ZFO delivery info file name.
 *
 * @param[in] dmId Message id.
 * @return ZFO delivery info file name.
 */
QString getDelInfoZfoName(qint64 dmId)
{
	return QString("DD_%1.zfo").arg(dmId);
}

/*!
 * @brief Get ZFO location directory path.
 *
 * @param[in] baseDir Base directory.
 * @param[in] zfoSubdir ZFO sub-directory.
 * @param[in] dmId Message id.
 * @return ZFO location directory path.
 */
static
QString getZfoDirPath(const QString &baseDir, const QString &zfoSubdir,
    qint64 dmId)
{
	return baseDir % QDir::separator() % zfoSubdir
	    % QDir::separator() % QString::number(dmId);
}

/*!
 * @brief Get ZFO message path.
 *
 * @param[in] baseDir Base directory.
 * @param[in] subDir ZFO sub-directory.
 * @param[in] dmId Message id.
 * @param[in] msgDirect Message direction.
 * @return ZFO message path.
 */
static
QString getMsgZfoFilePath(const QString &baseDir, const QString &subDir,
    qint64 msgId, enum MessageDirection msgDirect)
{
	return getZfoDirPath(baseDir, subDir, msgId)
	    % QDir::separator() % getMsgZfoName(msgId, msgDirect);
}

/*!
 * @brief Get ZFO delivery info path.
 *
 * @param[in] baseDir Base directory.
 * @param[in] subDir ZFO sub-directory.
 * @param[in] dmId Message id.
 * @return ZFO delivery info path.
 */
static
QString getDeliveryZfoFilePath(const QString &baseDir, const QString &subDir,
    qint64 msgId)
{
	return getZfoDirPath(baseDir, subDir, msgId)
	    % QDir::separator() % getDelInfoZfoName(msgId);
}

/*!
 * @brief Get valid account id from message database name.
 *
 * @param[in] msgDbPath Message database path.
 * @return Account id.
 */
static
AcntId getAcntIdFromDbName(const QString &msgDbPath)
{
	QString userName, year, errTxt;
	bool testing;
	if (Q_UNLIKELY(!MessageDbSet::isValidDbFileName(
	    QFileInfo(msgDbPath).fileName(), userName, year, testing, errTxt))) {
		return AcntId();
	}
	return AcntId(userName, testing);
}

/*!
 * @brief Get list of years from message database.
 *
 * @param[in] dbSet Message database set.
 * @return List of years.
 */
static
QStringList getYearsFromDb(const MessageDbSet *dbSet)
{
	QStringList years = dbSet->msgsYears(MessageDb::TYPE_RECEIVED,
	    DESCENDING);
	years.append(dbSet->msgsYears(MessageDb::TYPE_SENT,
	    DESCENDING));
	years.removeDuplicates();
	return years;
}

bool DbConvertMobileToDesktop::convertMsgDb(const QString &msgDbPath,
    const QString &zfoDbPath, QStringList &errList)
{
	QFileInfo fi(msgDbPath);

	/* Open message database. */
	MessageDbSingle *msgDb =
	     MessageDbSingle::createNew(msgDbPath, "TEMPORARY_MSG_DB");
	if (Q_UNLIKELY(Q_NULLPTR == msgDb)) {
		logErrorNL("Cannot open message database file '%s'.",
		    msgDbPath.toUtf8().constData());
		errList.append(tr("Cannot open message database file '%1'.").arg(fi.fileName()));
		return false;
	}

	/* Open ZFO database if exists. */
	bool isZfoDbPresent = !zfoDbPath.isEmpty();
	QSqlDatabase zfoDb;
	if (isZfoDbPresent) {
		zfoDb = QSqlDatabase::addDatabase("QSQLITE", "TEMPORARY_ZFO_DB");
		zfoDb.setDatabaseName(zfoDbPath);
		if (Q_UNLIKELY(!zfoDb.open())) {
			logErrorNL("Cannot open ZFO database file '%s'.",
			    zfoDbPath.toUtf8().constData());
			isZfoDbPresent = false;
		}
	} else {
		logWarningNL("%s", "ZFO database file is missing.");
	}

	/* Get message database file path and base file name. */
	QString dbAbsPath = fi.absolutePath();
	QString dbBaseName = fi.baseName();
	bool testing = isTesting(dbBaseName);

	/* Get all messages from source message database. */
	const QSet<MsgId> msgIds(msgDb->getAllMsgIds());

	for (const MsgId &msgId : msgIds) {

		/* Message. */
		enum MessageDirection msgDirect = MessageDirection::MSG_RECEIVED;
		switch (msgDb->getMessageType(msgId.dmId())) {
		case MessageDb::TYPE_RECEIVED:
			msgDirect = MessageDirection::MSG_RECEIVED;
			break;
		case MessageDb::TYPE_SENT:
			msgDirect = MessageDirection::MSG_SENT;
			break;
		default:
			msgDirect = MessageDirection::MSG_RECEIVED;
			break;
		}

		QString filePath = getMsgZfoFilePath(dbAbsPath, dbBaseName,
		    msgId.dmId(), msgDirect);

		/* Load ZFO from file. */
		QByteArray zfoContent = readZfoContentFile(filePath);
		if (Q_UNLIKELY(zfoContent.isEmpty())) {
			/* Load ZFO from database. */
			if (isZfoDbPresent) {
				zfoContent = QByteArray::fromBase64(
				    readZfoContentDb(zfoDb, msgId.dmId(),
				    testing));
			}
			if (Q_UNLIKELY(zfoContent.isEmpty())) {
				logErrorNL("Cannot find/read message ZFO file '%s'.",
				    filePath.toUtf8().constData());
				errList.append(tr("%1: cannot find/read message ZFO file '%2'.").arg(fi.fileName()).arg(msgId.dmId()));
				continue;
			}
		}

		enum Isds::LoadType zfoType = Isds::LT_ANY;
		bool iOk = false;
		const Isds::Message message = Isds::parseZfoData(zfoContent, zfoType, &iOk);
		if (Q_UNLIKELY(!iOk)) {
			logErrorNL("Cannot parse ZFO file '%s'.",
			    filePath.toUtf8().constData());
			errList.append(tr("%1: cannot parse ZFO file '%2'.").arg(fi.fileName()).arg(msgId.dmId()));
			continue;
		}

		if (Q_UNLIKELY(!msgDb->insertOrReplaceCompleteMessage(STORE_RAW_INTO_DB, message,
		    msgDirect, true, false))) {
			logErrorNL("Cannot insert complete message '%s' into database.",
			    QString::number(msgId.dmId()).toUtf8().constData());
			errList.append(tr("%1: cannot insert complete message '%2' into database.").arg(fi.fileName()).arg(msgId.dmId()));
			continue;
		}

		/* Delivery info. */
		filePath = getDeliveryZfoFilePath(dbAbsPath, dbBaseName,
		    msgId.dmId());
		zfoContent = readZfoContentFile(filePath);
		if (Q_UNLIKELY(zfoContent.isEmpty())) {
			logErrorNL("Cannot find/read delivery info ZFO file '%s'.",
			    filePath.toUtf8().constData());
			errList.append(tr("%1: cannot find/read delivery info ZFO file '%2'.").arg(fi.fileName()).arg(msgId.dmId()));
			continue;
		}
		if (Q_UNLIKELY(!msgDb->insertOrReplaceDeliveryInfoRaw(STORE_RAW_INTO_DB,
		        msgId.dmId(), zfoContent))) {
			logErrorNL("Cannot insert delivery info '%s' into database.",
			    QString::number(msgId.dmId()).toUtf8().constData());
			errList.append(tr("%1: cannot insert delivery info '%2' into database.").arg(fi.fileName()).arg(msgId.dmId()));
			continue;
		}
	}

	delete msgDb;

	return errList.isEmpty();
}

QList<AcntId> DbConvertMobileToDesktop::getAcntIdsFromDbName(
    const QStringList &msgDbs)
{
	QList<AcntId> acntIds;
	for (const QString &msgDbPath : msgDbs) {
		AcntId acntId = getAcntIdFromDbName(msgDbPath);
		if (acntId.isValid() && !acntIds.contains(acntId)) {
			acntIds.append(acntId);
		}
	}
	return acntIds;
}

bool DbConvertMobileToDesktop::splitDbByYears(const QString &srcMsgDb,
    const QString &destMsgDb)
{
	const AcntId acntId = getAcntIdFromDbName(srcMsgDb);
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	QFileInfo fi(srcMsgDb);
	MessageDbSet *srcDbSet = MessageDbSet::createNew(fi.absolutePath(),
	    acntId.username(), acntId.testing(),
	    MessageDbSet::DO_UNKNOWN, true, "SPLIT_DB",
	    MessageDbSet::CM_MUST_EXIST);
	if (Q_UNLIKELY(Q_NULLPTR == srcDbSet)) {
		Q_ASSERT(0);
		return false;
	}

	QStringList years = getYearsFromDb(srcDbSet);
	MessageDb *messageDb = srcDbSet->accessMessageDb(QDateTime(), false);
	QString testing = acntId.testing() ? QString("1") : QString("0");

	MessageDbSet *dstDbSet = Q_NULLPTR;
	DbContainer temporaryDbCont(true, "TEMPORARYDBS");
	dstDbSet = temporaryDbCont.accessDbSet(destMsgDb, acntId,
	    MessageDbSet::DO_YEARLY, MessageDbSet::CM_CREATE_ON_DEMAND);

	/*
	 * If the source database for splitting does not contain any message
	 * (years list is empty), create an empty database for current year
	 * to be used for account import.
	 */
	if (Q_UNLIKELY(years.isEmpty())) {
		years.append(
		    MessageDbSet::yearFromDateTime(QDateTime::currentDateTime()));
	}

	for (int i = 0; i < years.count(); ++i) {

		QString dateStr =
		    QString("%1-06-06 06:06:06.000").arg(years.at(i));
		QDateTime fakeTime = QDateTime::fromString(dateStr,
		    "yyyy-MM-dd HH:mm:ss.zzz");

		QString newDbName =
		    QString("%1_%2" DB_UNDERSCORES "%3.db").arg(acntId.username()).arg(years.at(i)).arg(testing);
		const QString fullPath = destMsgDb % QDir::separator() % newDbName;

		/* Select destination database via fake delivery time. */
		MessageDb *dstDb = dstDbSet->accessMessageDb(fakeTime, true);
		if (Q_UNLIKELY(Q_NULLPTR == dstDb)) {
			return false;
		}

		/* Copy all message data to new database. */
		if (Q_UNLIKELY(!messageDb->copyRelevantMsgsToNewDb(fullPath,
		        years.at(i), true))) {
			return false;
		}
	}

	return true;
}
