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

#include "src/datovka_shared/compat_qt/variant.h" /* nullVariantWhenIsNull */
#include "src/datovka_shared/graphics/colour.h"
#include "src/datovka_shared/log/log.h"
#include "src/io/tag_db.h"
#include "src/io/tag_db_tables.h"

bool TagDb::insertTag(const QString &tagName, const QString &tagColour)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	if (Q_UNLIKELY(tagName.isEmpty())) {
		logErrorNL("%s", "Cannot create tag with empty name.");
		return false;
	}
	if (Q_UNLIKELY(!Colour::isValidColourStr(tagColour))) {
		logErrorNL("Cannot create tag with invalid colour '%s'.",
		    tagColour.toUtf8().constData());
		return false;
	}

	/*
	 * No transaction should be needed. There is only one write operation
	 * coupled with read operations.
	 * RETURNING statement was introduced in SQLite 3.35.
	 */

	/* Check tag presence. */
	QString queryStr = "SELECT id FROM tag WHERE tag_name = :tag_name";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	query.bindValue(":tag_name", tagName);

	if (query.exec() && query.isActive()) {
		query.first();
		if (Q_UNLIKELY(query.isValid())) {
			/* Already exists. */
			return false;
		}
	}

	/* Insert if not present. */
	queryStr = "INSERT INTO tag (tag_name, tag_color) "
	    "VALUES (:tag_name, :tag_color)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	query.bindValue(":tag_name", tagName);
	query.bindValue(":tag_color", nullVariantWhenIsNull(tagColour));

	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	/* Read inserted value. */
	queryStr = "SELECT id, tag_name, tag_color "
	    "FROM tag WHERE tag_name = :tag_name";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	query.bindValue(":tag_name", tagName);

	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		TagEntry insertedEntry(query.value(0).toInt(),
		    query.value(1).toString(), query.value(2).toString());
		emit tagInserted(QList<TagEntry>() << insertedEntry);
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}

	return true;
}

/*!
 * @brief Get tag data from database file.
 *
 * @param[in] query SQL query to work with.
 * @param[in] id Tag identifier.
 * @return Tag data.
 */
static
TagEntry getTagEntry(QSqlQuery &query, int id)
{
	QString queryStr = "SELECT tag_name, tag_color FROM tag WHERE id = :id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":id", id);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return TagEntry(id, query.value(0).toString(),
		    query.value(1).toString());
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return TagEntry();
}

bool TagDb::updateTag(int id, const QString &tagName, const QString &tagColour)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	if (Q_UNLIKELY(tagName.isEmpty())) {
		logErrorNL("%s", "Cannot change tag name to empty string.");
		return false;
	}
	if (Q_UNLIKELY(!Colour::isValidColourStr(tagColour))) {
		logErrorNL("Cannot change tag colour to invalid value '%s'.",
		    tagColour.toUtf8().constData());
		return false;
	}

	/*
	 * No transaction should be needed. There is only one write operation
	 * coupled with a read operation.
	 */

	{
		const TagEntry entry = getTagEntry(query, id);
		if (Q_UNLIKELY(entry.isNull())) {
			logErrorNL(
			    "Cannot update non-existent tag with id '%d'.", id);
			return false;
		}
		if (Q_UNLIKELY((entry.name() == tagName) && (entry.colour() == tagColour))) {
			/* No change. */
			return false;
		}
	}

	QString queryStr = "UPDATE tag SET tag_name = :tag_name, "
	    "tag_color = :tag_color WHERE id = :id";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	query.bindValue(":tag_name", tagName);
	query.bindValue(":tag_color", nullVariantWhenIsNull(tagColour));
	query.bindValue(":id", id);

	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	TagEntry updatedEntry(id, tagName, tagColour);
	emit tagUpdated(QList<TagEntry>() << updatedEntry);

	return true;
}

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

/*!
 * @brief Filter out identifiers not existing in the database.
 *
 * @param[in] query SQL query to work with.
 * @param[in] ids Tag identifiers to look for.
 * @return List consisting only of supplied identifiers existing in the database.
 *     Empty list on error.
 */
static
QList<int> existingIdentifiers(QSqlQuery &query, const QList<int> &ids)
{
	QString queryStr;
	{
		QString idListing = toListString(ids);
		if (Q_UNLIKELY(idListing.isEmpty())) {
			return QList<int>();
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("SELECT id FROM tag WHERE id IN (%1)")
		    .arg(idListing);
	}

	QList<int> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(query.value(0).toInt());
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<int>();
}

bool TagDb::deleteTags(QList<int> ids)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	bool transaction = false;
	QString queryStr;
	QString idListing;

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

	/* Get only existing identifiers. */
	ids = existingIdentifiers(query, ids);
	if (Q_UNLIKELY(ids.isEmpty())) {
		goto fail;
	}
	idListing = toListString(ids);
	if (Q_UNLIKELY(idListing.isEmpty())) {
		goto fail;
	}

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

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

	commitTransaction();

	emit tagDeleted(ids);

	return true;

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

bool TagDb::deleteAllTags(void)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	bool transaction = false;
	QString queryStr;
	QList<int> deletedIds;

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

	queryStr = "SELECT id FROM tag";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			deletedIds.append(query.value(0).toInt());
			query.next();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "DELETE FROM message_tags";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "DELETE FROM tag";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	commitTransaction();

	if (!deletedIds.isEmpty()) {
		emit tagDeleted(deletedIds);
	}

	return true;

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

TagEntry TagDb::getTagData(int id) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	return getTagEntry(query, id);
}

QList<TagEntry> TagDb::getAllTags(void) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QList<TagEntry> tagList;

	QString queryStr = "SELECT * FROM tag ORDER BY tag_name ASC";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			tagList.append(TagEntry(query.value(0).toInt(),
			    query.value(1).toString(),
			    query.value(2).toString()));
			query.next();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return tagList;
fail:
	return QList<TagEntry>();
}

int TagDb::getTagAssignmentCount(int tagId) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	QString queryStr("SELECT COUNT(*) AS entries FROM message_tags "
	    "WHERE tag_id = :tag_id");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return -1;
	}
	query.bindValue(":tag_id", tagId);

	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toInt();
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return -1;
	}
}

/*!
 * @brief Get all tags related to given message.
 *
 * @param[in] query SQL query to work with.
 * @param[in] userName Account identifier.
 * @param[in] msgId Message identifier.
 * @return List of tags related to message.
 */
static
QList<TagEntry> getMessageTagEntries(QSqlQuery &query, const QString &username,
    quint64 msgId)
{
	QList<TagEntry> tagList;

	QString queryStr = "SELECT t.id, t.tag_name, t.tag_color "
	    "FROM tag AS t "
	    "LEFT JOIN message_tags AS m "
	    "ON (t.id = m.tag_id) "
	    "WHERE m.message_id  = :msgId AND m.user_name = :userName "
	    "ORDER BY t.tag_name ASC";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":msgId", msgId);
	query.bindValue(":userName", username);

	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			tagList.append(TagEntry(query.value(0).toInt(),
			    query.value(1).toString(),
			    query.value(2).toString()));
			query.next();
		}
		return tagList;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return QList<TagEntry>();
}

QList<TagEntry> TagDb::getMessageTags(const QString &userName,
    quint64 msgId) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	return getMessageTagEntries(query, userName, msgId);
}

bool TagDb::removeAllTagsFromMsg(const QString &userName, bool testing,
    qint64 msgId)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	/*
	 * No transaction should be needed. There is only one write operation
	 * coupled with read operations.
	 * RETURNING statement was introduced in SQLite 3.35.
	 */

	QString queryStr = "SELECT COUNT(*) AS nrRecords FROM message_tags WHERE "
	    "user_name = :userName AND message_id = :msgId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":msgId", msgId);
	query.bindValue(":userName", userName);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		if (Q_UNLIKELY(query.value(0).toInt() <= 0)) {
			/* No entry to be deleted found. */
			return false;
		}
	} else {
		logErrorNL("Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	queryStr = "DELETE FROM message_tags WHERE "
	    "user_name = :userName AND message_id = :msgId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":msgId", msgId);
	query.bindValue(":userName", userName);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	TagAssignment modifiedAssignment(userName, testing, msgId,
	    QList<TagEntry>());
	emit tagAssignmentChanged(QList<TagAssignment>() << modifiedAssignment);

	return true;
}

/*!
 * @brief Get set of tag identifiers from supplied list which are assigned
 *     to given message.
 *
 * @param[in] query SQL query to work with.
 * @param[in] userName Account identifier.
 * @param[in] testing True for a testing environment account.
 * @param[in] msgId Message identifier.
 * @param[in] tagIds List to take the identifiers from.
 * @return Set of identifiers from the list assigned to the message,
 *         empty set on error.
 */
static
QSet<int> assignedTags(QSqlQuery &query, const QString &username, bool testing,
    qint64 msgId, const QList<int> &tagIds)
{
	Q_UNUSED(testing); /* Currently unused. */

	QSet<int> result;
	QString queryStr;
	{
		QString idListing = toListString(tagIds);
		if (Q_UNLIKELY(idListing.isEmpty())) {
			goto fail;
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("SELECT tag_id FROM message_tags WHERE "
		    "user_name = :userName AND message_id = :msgId AND tag_id IN (%1)")
		    .arg(idListing);
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":userName", username);
	query.bindValue(":msgId", msgId);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.insert(query.value(0).toInt());
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QSet<int>();
}

/*!
 * @brief Subtract the content of the set from the list.
 *
 * @param[in] fromList List of values from which to subtract.
 * @param[in] whatSet Set of values that should be subtracted from the list.
 * @return List of values remaining in the list.
 */
static
QList<int> subtractTagIds(const QList<int> &fromList, const QSet<int> &whatSet)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
	QSet<int> fromSet(fromList.begin(), fromList.end());
#else /* < Qt-5.14.0 */
	QSet<int> fromSet = fromList.toSet();
#endif /* >= Qt-5.14.0 */

	fromSet -= whatSet;

#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
	return QList<int>(fromSet.begin(), fromSet.end());
#else /* < Qt-5.14.0 */
	return fromSet.toList();
#endif /* >= Qt-5.14.0 */
}

bool TagDb::assignTagToMsg(const QString &userName, bool testing, qint64 msgId,
    QList<int> tagIds)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	bool transaction = false;
	QString queryStr;
	QList<TagEntry> tagEnties;

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

	/* Remove tags already assigned to the message. */
	{
		QSet<int> assignedTagSet = assignedTags(query, userName,
		    testing, msgId, tagIds);
		tagIds = subtractTagIds(tagIds, assignedTagSet);
		if (Q_UNLIKELY(tagIds.isEmpty())) {
			goto fail;
		}
	}

	/* Assign new tags. */
	foreach (int tagId, tagIds) {
		queryStr = "INSERT INTO message_tags (user_name, message_id, tag_id) "
		    "VALUES (:userName, :msgId, :tagId)";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":msgId", msgId);
		query.bindValue(":tagId", tagId);
		query.bindValue(":userName", nullVariantWhenIsNull(userName));
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
	}

	tagEnties = getMessageTagEntries(query, userName, msgId);

	commitTransaction();

	{
		TagAssignment modifiedAssignment(userName, testing, msgId,
		    tagEnties);
		emit tagAssignmentChanged(
		    QList<TagAssignment>() << modifiedAssignment);
	}

	return true;

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

bool TagDb::removeTagFromMsg(const QString &userName, bool testing,
    qint64 msgId, QList<int> tagIds)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	bool transaction = false;
	QString queryStr;
	QList<TagEntry> tagEnties;

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

	/* Remove tags not assigned to the message. */
	{
		QSet<int> assignedTagSet = assignedTags(query, userName,
		    testing, msgId, tagIds);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
		tagIds = QList<int>(assignedTagSet.begin(), assignedTagSet.end());
#else /* < Qt-5.14.0 */
		tagIds = assignedTagSet.toList();
#endif /* >= Qt-5.14.0 */
		if (Q_UNLIKELY(tagIds.isEmpty())) {
			goto fail;
		}
	}

	{
		QString idListing = toListString(tagIds);
		if (Q_UNLIKELY(idListing.isEmpty())) {
			goto fail;
		}

		queryStr = QString("DELETE FROM message_tags WHERE "
		    "message_id = :msgId AND user_name = :userName AND tag_id IN (%1)")
		    .arg(idListing);
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":msgId", msgId);
	query.bindValue(":userName", userName);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	tagEnties = getMessageTagEntries(query, userName, msgId);

	commitTransaction();

	{
		TagAssignment modifiedAssignment(userName, testing, msgId,
		    tagEnties);
		emit tagAssignmentChanged(
		    QList<TagAssignment>() << modifiedAssignment);
	}

	return true;

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

bool TagDb::removeAllMsgTagsFromAccount(const QString &userName, bool testing)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);

	bool transaction = false;
	QString queryStr;
	QList<qint64> dmIds;

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

	queryStr = "SELECT DISTINCT message_id FROM message_tags WHERE "
	    "user_name = :userName";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":userName", userName);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			dmIds.append(query.value(0).toLongLong());
			query.next();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

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

	queryStr = "DELETE FROM message_tags WHERE "
	    "user_name = :userName";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":userName", userName);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	commitTransaction();

	{
		QList<TagAssignment> tagAssignmentList;
		foreach (qint64 dmId, dmIds) {
			tagAssignmentList.append(
			    TagAssignment(userName, testing, dmId, QList<TagEntry>()));
		}
		emit tagAssignmentChanged(tagAssignmentList);
	}

	return true;

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

QList<qint64> TagDb::getMsgIdsContainSearchTagText(const QString &text) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QList<qint64> msgIdList;

	/* TODO - remove duplication if (account1.sent == account2.received) */

	QString queryStr = "SELECT m.message_id FROM message_tags AS m "
	    "LEFT JOIN tag AS t ON (m.tag_id = t.id) "
	    "WHERE t.tag_name LIKE '%'||:text||'%'";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	query.bindValue(":text", text);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			msgIdList.append(query.value(0).toLongLong());
			query.next();
		}
		return msgIdList;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<qint64>();
}

/*!
 * @brief This method ensures that the foreign key functionality is enabled
 *     at runtime.
 *
 * @return True on success.
 */
static
bool enableForeignKeyFunctionality(const QSqlDatabase &db)
{
	QSqlQuery query(db);
	bool ret = false;

	/* Set value. */
	QString queryStr = "PRAGMA foreign_keys=ON";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Check set value. */
	queryStr = "PRAGMA foreign_keys";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			ret = query.value(0).toBool();
		} else {
			ret = false;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return ret;

fail:
	return false;
}

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

	QMutexLocker locker(&m_lock);

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

QList<class SQLiteTbl *> TagDb::listOfTables(void) const
{
	QList<class SQLiteTbl *> tables;
	tables.append(&tagTbl);
	tables.append(&msgtagsTbl);
	return tables;
}
