/*
 * 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 <QString>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>

#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/compat_qt/variant.h" /* nullVariantWhenIsNull */
#include "src/datovka_shared/identifiers/account_id.h"
#include "src/datovka_shared/io/db_helper.h"
#include "src/datovka_shared/io/db_time.h"
#include "src/datovka_shared/isds/type_conversion.h" /* Isds::variant2NilBool */
#include "src/datovka_shared/json/db_info.h"
#include "src/datovka_shared/log/log.h"
#include "src/io/timestamp_db.h"
#include "src/io/timestamp_db_tables.h"

/* Current database version. */
#define DB_VER_MAJOR 1
#define DB_VER_MINOR 0

void TstValidityHash::declareTypes(void)
{
	/*
	 * Calling qRegisterMetaType() is required for queued signal and slot
	 * connections.
	 */
	qRegisterMetaType<TstValidityHash>("TstValidityHash");
}

void TstEntry::declareTypes(void)
{
	/*
	 * Calling qRegisterMetaType() is required for queued signal and slot
	 * connections.
	 */
	qRegisterMetaType<TstEntry>("TstEntry");
	qRegisterMetaType< QList<TstEntry> >("QList<TstEntry>");
}

enum ErrorCode {
	EC_OK = 0, /*!< Operation succeeded. */
	EC_INPUT, /*!< Invalid input supplied. */
	EC_DB, /*!< Error occurred when interacting with the database engine. */
	EC_NO_DATA, /*!< No available data stored in the database. */
	EC_EXISTS, /*!< Data already exist or inserted values are in conflict with existing ones. */
	EC_NO_CHANGE /*!< No data are changed. */
};

static const QString dbInfoEntryName("db_info");

bool TimestampDb::getDbInfo(Json::DbInfo &info) const
{
	QSqlQuery query(m_db);
	QString jsonStr;
	bool iOk = false;

	QString queryStr = "SELECT entry_json FROM _db_info "
	    "WHERE (entry_name = :entry_name)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":entry_name", dbInfoEntryName);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.first() && query.isValid()) {
		jsonStr = query.value(0).toString();
	} else {
		logWarningNL("Cannot read SQL data: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (Q_UNLIKELY(jsonStr.isEmpty())) {
		goto fail;
	}

	info = Json::DbInfo::fromJson(jsonStr.toUtf8(), &iOk);
	if (Q_UNLIKELY(!iOk)) {
		goto fail;
	}

	return true;

fail:
	info = Json::DbInfo();
	return false;
}

bool TimestampDb::updateDbInfo(const Json::DbInfo &info)
{
	QSqlQuery query(m_db);
	QString jsonStr;

	QString queryStr =
	    "INSERT OR REPLACE INTO _db_info (entry_name, entry_json) "
	    "VALUES (:entry_name, :entry_json)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":entry_name", dbInfoEntryName);
	query.bindValue(":entry_json", (!info.isNull()) ? QString::fromUtf8(info.toJsonData(false)) : QVariant());
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

/*!
 * @brief Insert account identification if it is missing.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in,out] query SQL query to work with.
 * @param[in] acntId Account identifier.
 * @param[out] key Primary key of the corresponding entry.
 * @return Error code.
 */
static
enum ErrorCode _ensureAcntIdPresence(QSqlQuery &query, const AcntId &acntId,
    qint64 &key)
{
	if (Q_UNLIKELY(acntId.username().isEmpty())) {
		logErrorNL("%s", "Cannot use empty username.");
		return EC_INPUT;
	}

	enum ErrorCode ec = EC_OK;

	QString queryStr = "SELECT id FROM accounts "
	    "WHERE test_env = :testEnv AND username = :username";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":testEnv", acntId.testing());
	query.bindValue(":username", acntId.username());
	if (query.exec() && query.isActive()) {
		query.first();
		if (Q_UNLIKELY(query.isValid())) {
			/* Already exists. */
			key = query.value(0).toLongLong();
			ec = EC_EXISTS;
			return ec;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
	/*
	 * The RETURNING syntax has been supported by SQLite since version
	 * 3.35.0 (2021-03-12).
	 */
	queryStr = "INSERT INTO accounts (test_env, username) "
	    "VALUES (:testEnv, :username) RETURNING id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":testEnv", acntId.testing());
	query.bindValue(":username", acntId.username());
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			key = query.value(0).toLongLong();
		} else {
			ec = EC_NO_DATA;
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
#else /* < Qt-6.9.0 */
	/* Insert. */
	queryStr = "INSERT INTO accounts (test_env, username) "
	    "VALUES (:testEnv, :username)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":testEnv", acntId.testing());
	query.bindValue(":username", acntId.username());
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	/* Read inserted. */
	queryStr = "SELECT id FROM accounts "
	    "WHERE test_env = :testEnv AND username = :username";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":testEnv", acntId.testing());
	query.bindValue(":username", acntId.username());
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			key = query.value(0).toLongLong();
		} else {
			ec = EC_NO_DATA;
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
#endif /* >= Qt-6.9.0 */

fail:
	return ec;
}

/*!
 * @brief Insert ZFO identification.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in,out] query SQL query to work with.
 * @param[in] msgId Message/delivery info identifier.
 * @param[in] acntRefKey Account primary key.
 * @return Error code.
 */
static
enum ErrorCode _insertIdentification(QSqlQuery &query,
    const Json::MsgId2 &msgId, const qint64 acntRefKey)
{
	if (Q_UNLIKELY(msgId.testEnv() == Isds::Type::BOOL_NULL)) {
		logErrorNL("%s", "Cannot insert timestamp entry for unspecified message environment.");
		return EC_INPUT;
	}
	if (Q_UNLIKELY(msgId.dmId() < 0)) {
		logErrorNL("%s", "Cannot insert timestamp entry for invalid message identifier.");
		return EC_INPUT;
	}
	if (Q_UNLIKELY(!msgId.deliveryTime().isValid())) {
		logErrorNL("%s", "Cannot insert timestamp entry for message with invalid delivery time.");
		return EC_INPUT;
	}

	enum ErrorCode ec = EC_OK;
	qint64 identifRefKey = 0;

	QString queryStr = "SELECT id FROM identification WHERE test_env = :testEnv AND message_id = :msgId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":testEnv", (msgId.testEnv() == Isds::Type::BOOL_TRUE));
	query.bindValue(":msgId", msgId.dmId());
	if (query.exec() && query.isActive()) {
		query.first();
		if (Q_UNLIKELY(query.isValid())) {
			/* Already exists. */
			ec = EC_EXISTS;
			goto fail;
		}
	} else {
		/* Cannot execute and EC_DB? */
	}

	/* Insert if not present. */
	queryStr = "INSERT INTO identification (test_env, message_id, dm_delivery_time) "
	    "VALUES (:testEnv, :msgId, :dmDeliveryTime)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":testEnv", (msgId.testEnv() == Isds::Type::BOOL_TRUE));
	query.bindValue(":msgId", msgId.dmId());
	query.bindValue(":dmDeliveryTime", nullVariantWhenIsNull(qDateTimeToDbFormat(msgId.deliveryTime())));
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	/* Read inserted value. */
	queryStr = "SELECT id FROM identification WHERE test_env = :testEnv AND message_id = :msgId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":testEnv", (msgId.testEnv() == Isds::Type::BOOL_TRUE));
	query.bindValue(":msgId", msgId.dmId());
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			identifRefKey = query.value(0).toLongLong();
		} else {
			ec = EC_NO_DATA;
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	/* Set account. */
	queryStr = "INSERT OR IGNORE INTO identification_account_affinity (identif_ref_id, account_ref_id) "
	    "VALUES (:identifRefId, :accountRefId)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":identifRefId", identifRefKey);
	query.bindValue(":accountRefId", acntRefKey);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

fail:
	return ec;
}

bool TimestampDb::insertIdentifications(const Json::MsgId2List &msgIds,
    const AcntId &acntId)
{
	Json::MsgId2List inserted;

	{
		enum ErrorCode ec = EC_OK;
		bool transaction = false;

		qint64 acntRefKey = 0;

		QMutexLocker locker(&m_lock);
		QSqlQuery query(m_db);

		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			goto fail;
		}

		ec = _ensureAcntIdPresence(query, acntId, acntRefKey);
		if (Q_UNLIKELY((ec != EC_OK) && (ec != EC_EXISTS))) {
			goto fail;
		}

		for (const Json::MsgId2 &msgId : msgIds) {
			ec = _insertIdentification(query, msgId, acntRefKey);
			if (ec == EC_OK) {
				inserted.append(msgId);
			} else if (ec == EC_EXISTS) {
				/* Do nothing. */
			} else {
				goto fail;
			}
		}

		commitTransaction();
		goto success;

fail:
		if (transaction) {
			rollbackTransaction();
		}
		return false;
	}

success:
	/* Signal must not be emitted when write lock is active. */
	if (!inserted.isEmpty()) {
		Q_EMIT identificationsInserted(inserted, acntId);
	}
	return true;
}

/*!
 * @brief Constructs a string containing a comma-separated list
 *     of numeric identifiers.
 *
 * @param[in] ids List of numeric identifiers.
 * @return String with list or empty string when \a ids empty or on failure.
 */
static
QString toListString(const QList<qint64> &ids)
{
	QStringList list;
	for (qint64 id : ids) {
		if (Q_UNLIKELY(id < 0)) {
			/* Ignore negative identifiers. */
			continue;
		}
		list.append(QString::number(id));
	}
	if (!list.isEmpty()) {
		return list.join(", ");
	}
	return QString();
}

/*!
 * @brief Construct a condition expression from supplied identifiers for matching
 *     testEnv and dmId pairs -- delivery time values are ignored.
 *
 * @param[in] msgIds Message/delivery info identifiers.
 * @return String with condition expression or empty string when \a msgIds empty or on failure.
 */
static
QString toConditionExpr1(const Json::MsgId1List &msgIds)
{
	QStringList list;
	for (const Json::MsgId1 &msgId : msgIds) {
		if (Q_UNLIKELY(msgId.dmId() < 0)) {
			/* Ignore invalid identifiers. */
			continue;
		}
		list.append(
		    QString("((test_env = %1) AND (message_id = %2))")
		        .arg((msgId.testEnv() == Isds::Type::BOOL_TRUE) ? "1" : "0")
		        .arg(msgId.dmId()));
	}
	if (!list.isEmpty()) {
		return list.join(" OR ");
	}
	return QString();
}

/*!
 * @brief Construct a condition expression from supplied identifiers for matching
 *     testEnv and dmId pairs -- delivery time values are ignored.
 *
 * @param[in] msgIds Message/delivery info identifiers.
 * @return String with condition expression or empty string when \a msgIds empty or on failure.
 */
static
QString toConditionExpr2(const Json::MsgId2List &msgIds)
{
	QStringList list;
	for (const Json::MsgId2 &msgId : msgIds) {
		if (Q_UNLIKELY(msgId.dmId() < 0)) {
			/* Ignore invalid identifiers. */
			continue;
		}
		list.append(
		    QString("((test_env = %1) AND (message_id = %2))")
		        .arg((msgId.testEnv() == Isds::Type::BOOL_TRUE) ? "1" : "0")
		        .arg(msgId.dmId()));
	}
	if (!list.isEmpty()) {
		return list.join(" OR ");
	}
	return QString();
}

/*!
 * @brief Filter out identifiers not existing in the database.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in,out] query SQL query to work with.
 * @param[in,out] msgIds Message/delivery info identifiers to look for.
 *                       Only existing identifiers are left in the list on successful return.
 * @param[out] recIds Found database record identifiers (primary table keys).
 * @return Error code.
 */
static
enum ErrorCode _existingIndetificationIds(QSqlQuery &query,
    Json::MsgId2List &msgIds, QList<qint64> &recIds)
{
	QString queryStr;
	{
		QString conditionListing = toConditionExpr2(msgIds);
		if (Q_UNLIKELY(conditionListing.isEmpty())) {
			recIds.clear();
			return EC_OK;
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("SELECT id, test_env, message_id, dm_delivery_time FROM identification WHERE %1")
		    .arg(conditionListing);
	}

	enum ErrorCode ec = EC_OK;

	QList<qint64> foundRecIds;
	Json::MsgId2List foundMsgIds;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			while (query.isValid()) {
				foundRecIds.append(query.value(0).toLongLong());
				foundMsgIds.append(
				    Json::MsgId2(
				        Isds::variant2NilBool(query.value(1)),
				        query.value(2).toLongLong(),
				        dateTimeFromDbFormat(query.value(3).toString())));
				query.next();
			}
		} else {
			ec = EC_NO_DATA;
			goto fail;
		}
	}  else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	recIds = macroStdMove(foundRecIds);
	msgIds = macroStdMove(foundMsgIds);

fail:
	return ec;
}

/*!
 * @brief Get primary keys from \a table where values in \a refColName match
 *    the values in \a refKeys.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in,out] query SQL query to work with.
 * @param[in] table Table to search in.
 * @param[in] refColName Reference key column name.
 * @param[in] refKeys Message/delivery info record primary keys.
 * @param[out] refIds Found database record identifiers (primary table keys).
 * @return Error code.
 */
static
enum ErrorCode _primaryKeysForRefKeys(QSqlQuery &query,
    class SQLiteTbl &table, const QString &refColName,
    const QList<qint64> &refKeys, QList<qint64> &refIds)
{
	QString queryStr;
	{
		QString identifIdListing = toListString(refKeys);
		if (Q_UNLIKELY(identifIdListing.isEmpty())) {
			refIds.clear();
			return EC_OK;
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("SELECT id FROM %1 WHERE %2 IN (%3)")
		    .arg(table.tabName)
		    .arg(refColName)
		    .arg(identifIdListing);
	}

	enum ErrorCode ec = EC_OK;

	QList<qint64> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			while (query.isValid()) {
				result.append(query.value(0).toLongLong());
				query.next();
			}
		} else {
			ec = EC_NO_DATA;
			goto fail;
		}
	}  else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	refIds = macroStdMove(result);

fail:
	return ec;
}

bool TimestampDb::deleteIdentifications(Json::MsgId2List msgIds)
{
	{
		QMutexLocker locker(&m_lock);
		QSqlQuery query(m_db);

		enum ErrorCode ec = EC_OK;
		bool transaction = false;
		QList<qint64> identifKeys;
		QList<qint64> msgTabKeys;
		QList<qint64> delInfoTabKeys;
		QString queryStr;
		QString identifKeyListing;
		QString msgTabKeyListing;
		QString delInfoTabKeyListing;

		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			goto fail;
		}

		/* Get only existing identifiers. */
		ec = _existingIndetificationIds(query, msgIds, identifKeys);
		if (Q_UNLIKELY(ec != EC_OK)) {
			goto fail;
		}
		identifKeyListing = toListString(identifKeys);
		if (Q_UNLIKELY(identifKeyListing.isEmpty())) {
			goto fail;
		}

		ec = _primaryKeysForRefKeys(query,
		    TimestampDbTables::messagesTbl, "identif_id",
		    identifKeys, msgTabKeys);
		if (Q_UNLIKELY((ec != EC_OK) && (ec != EC_NO_DATA))) {
			goto fail;
		}
		msgTabKeyListing = toListString(msgTabKeys);

		ec = _primaryKeysForRefKeys(query,
		    TimestampDbTables::delInfosTbl, "identif_id",
		    identifKeys, delInfoTabKeys);
		if (Q_UNLIKELY((ec != EC_OK) && (ec != EC_NO_DATA))) {
			goto fail;
		}
		delInfoTabKeyListing = toListString(delInfoTabKeys);

		if (!delInfoTabKeyListing.isEmpty()) {
			queryStr = QString("DELETE FROM supplementary_delivery_info_data WHERE ref_id IN (%1)").arg(delInfoTabKeyListing);
			if (Q_UNLIKELY(!query.prepare(queryStr))) {
				logErrorNL("Cannot prepare SQL query: %s; %s.",
				    query.lastQuery().toUtf8().constData(),
				    query.lastError().text().toUtf8().constData());
				goto fail;
			}
			if (Q_UNLIKELY(!query.exec())) {
				logErrorNL("Cannot execute SQL query: %s; %s.",
				    query.lastQuery().toUtf8().constData(),
				    query.lastError().text().toUtf8().constData());
				goto fail;
			}
		}

		queryStr = QString("DELETE FROM delivery_infos WHERE identif_id IN (%1)").arg(identifKeyListing);
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		if (!msgTabKeyListing.isEmpty()) {
			queryStr = QString("DELETE FROM supplementary_message_data WHERE ref_id IN (%1)").arg(msgTabKeyListing);
			if (Q_UNLIKELY(!query.prepare(queryStr))) {
				logErrorNL("Cannot prepare SQL query: %s; %s.",
				    query.lastQuery().toUtf8().constData(),
				    query.lastError().text().toUtf8().constData());
				goto fail;
			}
			if (Q_UNLIKELY(!query.exec())) {
				logErrorNL("Cannot execute SQL query: %s; %s.",
				    query.lastQuery().toUtf8().constData(),
				    query.lastError().text().toUtf8().constData());
				goto fail;
			}
		}

		queryStr = QString("DELETE FROM messages WHERE identif_id IN (%1)").arg(identifKeyListing);
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = QString("DELETE FROM identification_account_affinity WHERE identif_ref_id IN (%1)").arg(identifKeyListing);
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = QString("DELETE FROM identification WHERE id IN (%1)").arg(identifKeyListing);
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s; %s.",
			    query.lastQuery().toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		commitTransaction();
		goto success;

fail:
		if (transaction) {
			rollbackTransaction();
		}
		return false;
	}

success:
	/* Signal must not be emitted when write lock is active. */
	if (!msgIds.isEmpty()) {
		Q_EMIT identificationsDeleted(msgIds);
	}
	return true;
}

/*!
 * @brief Get the number of entries in the \a table.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in] db SQLite database.
 * @param[in] table Table to search in.
 * @param[out] The number of entries.
 * @return Error code.
 */
static
enum ErrorCode _tabEntryCount(const QSqlDatabase &db,
    class SQLiteTbl &table, qint64 &cnt)
{
	QSqlQuery query(db);

	enum ErrorCode ec = EC_OK;

	qint64 readVal = 0;

	QString queryStr = QString("SELECT COUNT(*) FROM %1")
	    .arg(table.tabName);
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	if (query.first() && query.isValid()) {
		readVal = query.value(0).toLongLong();
	} else {
		logWarningNL("Cannot read SQL data: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	cnt = readVal;
	return ec;

fail:
	cnt = 0;
	return ec;
}

bool TimestampDb::getIdentificationCnt(qint64 &cnt) const
{
	enum ErrorCode ec = EC_OK;

	QMutexLocker locker(&m_lock);

	qint64 readVal = 0;
	ec = _tabEntryCount(m_db, TimestampDbTables::identificationTbl,
	    readVal);
	if (Q_UNLIKELY(ec != EC_OK)) {
		cnt = 0;
		return false;
	}

	cnt = readVal;
	return true;
}

/*!
 * @brief Get message timestamp expiration time.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in,out] query SQL query to work with.
 * @param[in] msgId Message identifier.
 * @param[out] value Message expiration value.
 * @return Error code.
 */
static
enum ErrorCode _getMsgTimestampData(QSqlQuery &query,
    const Json::MsgId1 &msgId, TstValidity &value)
{
	if (Q_UNLIKELY(msgId.testEnv() == Isds::Type::BOOL_NULL)) {
		return EC_INPUT;
	}
	if (Q_UNLIKELY(msgId.dmId() < 0)) {
		return EC_INPUT;
	}

	enum ErrorCode ec = EC_OK;

	TstValidity readVal;

	QString queryStr = "SELECT i.id, m.tst_valid_to FROM identification AS i "
	    "LEFT JOIN messages AS m "
	    "ON (i.id = m.identif_id) "
	    "WHERE i.message_id = :msgId "
	    + QString((msgId.testEnv() != Isds::Type::BOOL_NULL) ? "AND i.test_env = :testEnv " : "");

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":msgId", msgId.dmId());
	if (msgId.testEnv() != Isds::Type::BOOL_NULL) {
		query.bindValue(":testEnv", (msgId.testEnv() == Isds::Type::BOOL_TRUE));
	}

	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			/* Only one entry. */
			readVal = macroStdMove(
			    TstValidity(true, dateTimeFromDbFormat(query.value(1).toString())));
		} else {
			ec = EC_NO_DATA;
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	value = macroStdMove(readVal);
	return ec;

fail:
	value = TstValidity();
	return ec;
}

bool TimestampDb::getMsgTimestampData(const Json::MsgId1List &msgIds,
    TstValidityHash &values) const
{
	enum ErrorCode ec = EC_OK;

	if (Q_UNLIKELY(msgIds.isEmpty())) {
		values.clear();
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	TstValidityHash readValues;

	for (const Json::MsgId1 &key : msgIds) {
		TstValidity value;
		ec = _getMsgTimestampData(query, key, value);
		if (ec == EC_OK) {
			readValues[key] = macroStdMove(value);
		} else if (ec == EC_NO_DATA) {
			/* No value. */
		} else {
			goto fail;
		}
	}

	values = macroStdMove(readValues);
	return true;

fail:
	values.clear();
	return false;
}

/*!
 * @brief Get identification id numbers corresponding to message identifier.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in,out] query SQL query to work with.
 * @param[in] msgIds Message identifiers.
 * @param[out] identifIds Found corresponding identification ids.
 * @return Error code.
 */
static
enum ErrorCode _getIdentifIds(QSqlQuery &query, const Json::MsgId1List &msgIds,
    QHash<Json::MsgId1, qint64> &identifIds)
{
	enum ErrorCode ec = EC_OK;

	if (Q_UNLIKELY(msgIds.isEmpty())) {
		identifIds.clear();
		return EC_OK;
	}

	QString queryStr;
	{
		QString conditionListing = toConditionExpr1(msgIds);
		if (Q_UNLIKELY(conditionListing.isEmpty())) {
			identifIds.clear();
			return EC_OK;
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("SELECT id, test_env, message_id FROM identification WHERE %1")
		    .arg(conditionListing);
	}

	QHash<Json::MsgId1, qint64> foundIdentifIds;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			while (query.isValid()) {
				foundIdentifIds[Json::MsgId1(
				        Isds::variant2NilBool(query.value(1)),
				        query.value(2).toLongLong())] = query.value(0).toLongLong();
				query.next();
			}
		} else {
			ec = EC_NO_DATA;
			goto fail;
		}
	}  else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

	identifIds = macroStdMove(foundIdentifIds);

fail:
	return ec;
}

/*!
 * @brief Update message timestamp expiration time.
 *
 * @note This function doesn't lock the database lock.
 *
 * @param[in,out] query SQL query to work with.
 * @param[in] identifId Corresponding identification id.
 * @param[in] tstExpiration Expiration date.
 * @return Error code.
 */
static
enum ErrorCode _updateMsgTimestampData(QSqlQuery &query, const qint64 identifId,
    const QDateTime &tstExpiration)
{
	if (Q_UNLIKELY(!tstExpiration.isValid())) {
		logErrorNL("%s", "Cannot insert timestamp entry invalid time.");
		return EC_INPUT;
	}

	enum ErrorCode ec = EC_OK;

	QString queryStr = "INSERT OR REPLACE INTO messages (identif_id, tst_valid_to) "
	    "VALUES (:identifId, :tstValidTo)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}
	query.bindValue(":identifId", identifId);
	query.bindValue(":tstValidTo", nullVariantWhenIsNull(qDateTimeToDbFormat(tstExpiration)));
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		ec = EC_DB;
		goto fail;
	}

fail:
	return ec;
}

bool TimestampDb::updateMsgTimestampData(const TstValidityHash &values)
{
	TstValidityHash updatedValues;

	{
		enum ErrorCode ec = EC_OK;
		bool transaction = false;

		QHash<Json::MsgId1, qint64> identifIds;

		QMutexLocker locker(&m_lock);
		QSqlQuery query(m_db);

		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			goto fail;
		}

		ec = _getIdentifIds(query, values.keys(), identifIds);
		if (Q_UNLIKELY(ec != EC_OK)) {
			goto fail;
		}

		for (const Json::MsgId1 &msgId : identifIds.keys()) {
			ec = _updateMsgTimestampData(query, identifIds[msgId],
			    values[msgId].tstExpiration);
			if (ec == EC_OK) {
				updatedValues[msgId] = TstValidity(true, values[msgId].tstExpiration);
			} else {
				goto fail;
			}
		}

		commitTransaction();
		goto success;

fail:
		if (transaction) {
			rollbackTransaction();
		}
		return false;
	}

success:
	/* Signal must not be emitted when write lock is active. */
	if (!updatedValues.isEmpty()) {
		Q_EMIT msgTimestampDataUpdated(updatedValues);
	}
	return true;
}

bool TimestampDb::getMsgTimestampCnt(qint64 &cnt,
    const QDateTime &expiringFrom, const QDateTime &expiringTo) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	QString queryStr = "SELECT COUNT(*) AS rnRecords FROM messages";
	if (expiringFrom.isValid() && expiringTo.isValid()) {
		queryStr += " WHERE (:expiringFrom <= tst_valid_to) AND (tst_valid_to < :expiringTo)";
	} else if (expiringFrom.isValid()) {
		queryStr += " WHERE (:expiringFrom <= tst_valid_to)";
	} else if (expiringTo.isValid()) {
		queryStr += " WHERE (tst_valid_to < :expiringTo)";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (expiringFrom.isValid() && expiringTo.isValid()) {
		query.bindValue(":expiringFrom", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringFrom)));
		query.bindValue(":expiringTo", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringTo)));
	} else if (expiringFrom.isValid()) {
		query.bindValue(":expiringFrom", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringFrom)));
	} else if (expiringTo.isValid()) {
		query.bindValue(":expiringTo", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringTo)));
	}

	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.first() && query.isValid()) {
		cnt = query.value(0).toLongLong();
	} else {
		logWarningNL("Cannot read SQL data: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	cnt = -1;
	return false;
}

bool TimestampDb::getMsgTimestampListing(QList<TstEntry> &tstEntries,
    const QDateTime &expiringFrom, const QDateTime &expiringTo) const
{
	QList<TstEntry> foundTstEntries;

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	QString queryStr = "SELECT i.test_env, i.message_id, i.dm_delivery_time, a.username, m.tst_valid_to FROM identification AS i "
	    "LEFT JOIN identification_account_affinity AS ia ON (i.id = ia.identif_ref_id) "
	    "LEFT JOIN accounts AS a ON (a.id = ia.account_ref_id) "
	    "LEFT JOIN messages AS m ON (i.id = m.identif_id)";
	if (expiringFrom.isValid() && expiringTo.isValid()) {
		queryStr += " WHERE (:expiringFrom <= m.tst_valid_to) AND (m.tst_valid_to < :expiringTo)";
	} else if (expiringFrom.isValid()) {
		queryStr += " WHERE (:expiringFrom <= m.tst_valid_to)";
	} else if (expiringTo.isValid()) {
		queryStr += " WHERE (m.tst_valid_to < :expiringTo)";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (expiringFrom.isValid() && expiringTo.isValid()) {
		query.bindValue(":expiringFrom", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringFrom)));
		query.bindValue(":expiringTo", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringTo)));
	} else if (expiringFrom.isValid()) {
		query.bindValue(":expiringFrom", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringFrom)));
	} else if (expiringTo.isValid()) {
		query.bindValue(":expiringTo", nullVariantWhenIsNull(qDateTimeToDbFormat(expiringTo)));
	}

	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			while (query.isValid()) {
				foundTstEntries.append(
				    TstEntry(
				        Json::MsgId2(Isds::variant2NilBool(query.value(0)),
				            query.value(1).toLongLong(),
				            dateTimeFromDbFormat(query.value(2).toString())),
				    query.value(3).toString(),
				    dateTimeFromDbFormat(query.value(4).toString())
				    ));

				query.next();
			}
		} else {
			/* No data. */
		}
	}  else {
		logErrorNL("Cannot execute SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	tstEntries = macroStdMove(foundTstEntries);
	return true;

fail:
	tstEntries.clear();
	return false;
}

QList<class SQLiteTbl *> TimestampDb::listOfTables(void) const
{
	QList<class SQLiteTbl *> tables;
	tables.append(&TimestampDbTables::_dbInfoTbl);
	tables.append(&TimestampDbTables::identificationTbl);
	tables.append(&TimestampDbTables::accountTbl);
	tables.append(&TimestampDbTables::identifAccountAffinityTbl);
	tables.append(&TimestampDbTables::messagesTbl);
	tables.append(&TimestampDbTables::supMsgDataTbl);
	tables.append(&TimestampDbTables::delInfosTbl);
	tables.append(&TimestampDbTables::supDelInfoDataTbl);
	return tables;
}

bool TimestampDb::assureConsistency(void)
{
	QMutexLocker locker(&m_lock);

	bool ret = true;

	/*
	 * Set database content version to current version when no version
	 * is available.
	 * Content version is the first entry in the database.
	 */
	{
		enum ErrorCode ec = EC_OK;
		qint64 entryCount = 0;
		ec = _tabEntryCount(m_db, TimestampDbTables::_dbInfoTbl,
		    entryCount);
		if (Q_UNLIKELY(ec != EC_OK)) {
			ret = false;
			goto fail;
		}
		if (0 == entryCount) {
			Json::DbInfo info;
			info.setFormatVersionMajor(DB_VER_MAJOR);
			info.setFormatVersionMinor(DB_VER_MINOR);
			ret = updateDbInfo(info);
		}
	}

fail:
	return ret;
}

bool TimestampDb::enableFunctionality(void)
{
	logInfoNL(
	    "Enabling SQLite foreign key functionality in database '%s'.",
	    fileName().toUtf8().constData());

	QMutexLocker locker(&m_lock);

	bool ret = DbHelper::enableForeignKeyFunctionality(m_db);
	if (Q_UNLIKELY(!ret)) {
		logErrorNL(
		    "Couldn't enable SQLite foreign key functionality in database '%s'.",
		    fileName().toUtf8().constData());;
	}
	return ret;
}
