/*
 * 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 <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QList>
#include <QPair>
#include <QRegularExpression>
#include <QSqlDatabase>
#include <QStringBuilder>

#include "src/datovka_shared/log/log.h"
#include "src/io/message_db_set.h"

MessageDbSet::MessageDbSet(const QString &locDir, const QString &primaryKey,
    bool testing, enum Organisation organisation, bool onDisk,
    const QString &connectionPrefix)
    : QObject(Q_NULLPTR),
    QMap<QString, MessageDb *>(),
    m_connectionPrefix(connectionPrefix),
    m_primaryKey(primaryKey),
    m_testing(testing),
    m_locDir(locDir),
    m_organisation(organisation),
    m_onDisk(onDisk)
{
}

MessageDbSet::~MessageDbSet(void)
{
	QMap<QString, MessageDb *>::iterator i;

	for (i = this->begin(); i != this->end(); ++i) {
		MessageDb *db = i.value();
		db->disconnect(SIGNAL(opened(QString)),
		    this, SLOT(watchOpened(QString)));
		db->disconnect(SIGNAL(openingFailed(QString)),
		    this, SLOT(watchOpeningFailed(QString)));
		db->disconnect(SIGNAL(changedLocallyRead(QList<MsgId>, bool)),
		    this, SLOT(watchChangedLocallyRead(QList<MsgId>, bool)));
		db->disconnect(SIGNAL(changedProcessState(QList<MsgId>, enum MessageDb::MessageProcessState)),
		    this, SLOT(watchChangedProcessState(QList<MsgId>, enum MessageDb::MessageProcessState)));
		db->disconnect(SIGNAL(messageInserted(MsgId, int)),
		    this, SLOT(watchMessageInserted(MsgId, int)));
		db->disconnect(SIGNAL(messageDataDeleted(MsgId)),
		    this, SLOT(watchMessageDataDeleted(MsgId)));
		delete db;
	}
}

bool MessageDbSet::openLocation(const QString &newLocDir,
    enum Organisation organisation, enum CreationManner creationManner,
    enum AbandonManner abandonManner)
{
	QStringList matchingFiles;

	if (Q_UNLIKELY(!m_onDisk)) {
		logErrorNL("%s",
		    "Cannot open location of message database which resides in memory.");
		return false;
	}

	/* New location must exist. */
	if (Q_UNLIKELY(!QDir(newLocDir).exists())) {
		logErrorNL("Directory '%s' does not exist.",
		    newLocDir.toUtf8().constData());
		return false;
	}

	/* Try to determine the database organisation structure. */
	enum Organisation existingOrganisation = dbOrganisation(newLocDir,
	    m_primaryKey, m_testing);
	matchingFiles = existingDbFileNamesInLocation(newLocDir,
	    m_primaryKey, m_testing, DO_UNKNOWN, true);
	if (Q_UNLIKELY((existingOrganisation == DO_UNKNOWN) && (!matchingFiles.isEmpty()))) {
		logErrorNL("Ambiguous organisation of database '%s' in %s'.",
		    m_primaryKey.toUtf8().constData(),
		    newLocDir.toUtf8().constData());
		return false;
	}

	if (organisation == DO_UNKNOWN) {
		if (existingOrganisation != DO_UNKNOWN) {
			organisation = existingOrganisation;
		} else {
			logErrorNL("Unspecified organisation of database '%s' in '%s'.",
			    m_primaryKey.toUtf8().constData(),
			    newLocDir.toUtf8().constData());
			return false;
		}
	} else if (Q_UNLIKELY(organisation != existingOrganisation)) {
		logErrorNL("Differently organised database '%s' in '%s' already exists.",
		    m_primaryKey.toUtf8().constData(),
		    newLocDir.toUtf8().constData());
		return false;
	}

	matchingFiles = existingDbFileNamesInLocation(newLocDir,
	    m_primaryKey, m_testing, organisation, true);

	if (Q_UNLIKELY((creationManner == CM_MUST_EXIST) && matchingFiles.isEmpty())) {
		logErrorNL("No matching database file for '%s' in '%s'.",
		    m_primaryKey.toUtf8().constData(),
		    newLocDir.toUtf8().constData());
		return false;
	}

	for (QMap<QString, MessageDb *>::iterator i = this->begin();
	     i != this->end(); ++i) {
		MessageDb *db = i.value();

		/* Close database. */
		delete db;
	}

	/* Remove all elements from this map. */
	this->clear();

	/* There may be no matching files. */

	/* Check primary keys. */
	for (const QString &fileName : matchingFiles) {
		QString secondaryKey = secondaryKeyFromFileName(
		    fileName, organisation);

		if (Q_UNLIKELY(secondaryKey.isNull())) {
			logErrorNL("Failed to obtain secondary key from file name '%s'.",
			    fileName.toUtf8().constData());
			return false;
		}
	}

	const QString oldLocDir = m_locDir;
	m_locDir = newLocDir;
	m_organisation = organisation;

	MessageDb *db = Q_NULLPTR;
	if (matchingFiles.size()) {
		/* Load files that have been found. */
		for (const QString &fileName : matchingFiles) {
			QString secondaryKey = secondaryKeyFromFileName(
			    fileName, organisation);
			Q_ASSERT(!secondaryKey.isNull());

			db = _accessMessageDb(secondaryKey, false);
			if (Q_UNLIKELY(db == Q_NULLPTR)) {
				logErrorNL("Failed to open database file '%s'.",
				    fileName.toUtf8().constData());
				/* TODO -- How can be this operation aborted? */
			}
		}
	} else if (creationManner == CM_CREATE_EMPTY_CURRENT) {
		/* Create empty file matching current date. */
		QString secKey = secondaryKey(QDateTime::currentDateTime());
		Q_ASSERT(!secKey.isNull());

		db = _accessMessageDb(secKey, true);
		if (Q_UNLIKELY(db == Q_NULLPTR)) {
			QString fileName(constructDbFileName(m_locDir,
			    m_primaryKey, secKey, m_testing, m_organisation));
			logErrorNL("Failed to open database file '%s'.",
			    fileName.toUtf8().constData());
			return false;
		}
	}

	if (abandonManner == AM_DELETE) {
		deleteRelatedInLocation(oldLocDir,
		    m_primaryKey, m_testing, DO_UNKNOWN);
	}

	return true;
}

bool MessageDbSet::copyToLocation(const QString &newLocDir)
{
	if (Q_UNLIKELY(m_organisation == DO_UNKNOWN)) {
		return false;
	}

	if (Q_UNLIKELY(!m_onDisk)) {
		logErrorNL("%s",
		    "Cannot copy message database which resides in memory.");
		return false;
	}

	if (Q_UNLIKELY(!QDir(newLocDir).exists())) {
		logErrorNL("Directory '%s' does not exist.",
		    newLocDir.toUtf8().constData());
		return false;
	}

	bool sucessfullyCopied = true;
	QList< QPair<QString, MessageDb *> > oldLocations;
	/* No need to store old directory locations. */
	QList<QString> newLocations;
	QList<QString> newDirLocations;

	for (QMap<QString, MessageDb *>::iterator i = this->begin();
	     i != this->end(); ++i) {
		MessageDb *db = i.value();
		QString oldFileName = db->fileName();
		QFileInfo fileInfo(oldFileName);
		QString newFileName =
		    newLocDir + QDir::separator() + fileInfo.fileName();
		QString newDirLocation = MessageDb::_assocFileDirPath(newFileName);

		sucessfullyCopied = db->copyDb(newFileName,
		    SQLiteDb::CREATE_MISSING);
		if (sucessfullyCopied) {
			/* Store origins of successful copies. */
			oldLocations.append(QPair<QString, MessageDb *>(oldFileName, db));
			/* Store new copies. */
			newLocations.append(newFileName);
			newDirLocations.append(newDirLocation);
		} else {
			break;
		}
	}

	if (Q_UNLIKELY(!sucessfullyCopied)) {
		/* Restore origins. */
		QList< QPair<QString, MessageDb *> >::iterator oi;
		for (oi = oldLocations.begin(); oi != oldLocations.end(); ++oi) {
			oi->second->openDb(oi->first, SQLiteDb::CREATE_MISSING);
		}
		/* Delete new copies. */
		QList<QString>::iterator ni;
		for (ni = newLocations.begin(); ni != newLocations.end(); ++ni) {
			QFile::remove(*ni);
		}
		for (const QString &dirLocation : newDirLocations) {
			QDir(dirLocation).removeRecursively();
		}
		return false;
	} else {
		m_locDir = newLocDir;
	}
	return true;
}

bool MessageDbSet::moveToLocation(const QString &newLocDir)
{
	if (Q_UNLIKELY(m_organisation == DO_UNKNOWN)) {
		return false;
	}

	if (Q_UNLIKELY(!m_onDisk)) {
		logErrorNL("%s",
		    "Cannot move message database which resides in memory.");
		return false;
	}

	if (Q_UNLIKELY(!QDir(newLocDir).exists())) {
		logErrorNL("Directory '%s' does not exist.",
		    newLocDir.toUtf8().constData());
		return false;
	}

	bool sucessfullyCopied = true;
	QList< QPair<QString, MessageDb *> > oldLocations;
	QList<QString> oldDirLocations;
	QList<QString> newLocations;
	QList<QString> newDirLocations;

	for (QMap<QString, MessageDb *>::iterator i = this->begin();
	     i != this->end(); ++i) {
		MessageDb *db = i.value();
		QString oldFileName = db->fileName();
		QString oldDirLocation = db->assocFileDirPath();
		QFileInfo fileInfo(oldFileName);
		QString newFileName =
		    newLocDir + QDir::separator() + fileInfo.fileName();
		QString newDirLocation = MessageDb::_assocFileDirPath(newFileName);

		sucessfullyCopied = db->copyDb(newFileName,
		    SQLiteDb::CREATE_MISSING);
		if (sucessfullyCopied) {
			/* Store origins of successful copies. */
			oldLocations.append(QPair<QString, MessageDb *>(oldFileName, db));
			oldDirLocations.append(oldDirLocation);
			/* Store new copies. */
			newLocations.append(newFileName);
			newDirLocations.append(newDirLocation);
		} else {
			break;
		}
	}

	if (Q_UNLIKELY(!sucessfullyCopied)) {
		/* Restore origins. */
		QList< QPair<QString, MessageDb *> >::iterator oi;
		for (oi = oldLocations.begin(); oi != oldLocations.end(); ++oi) {
			oi->second->openDb(oi->first, SQLiteDb::CREATE_MISSING);
		}
		/* Delete new copies. */
		QList<QString>::iterator ni;
		for (ni = newLocations.begin(); ni != newLocations.end(); ++ni) {
			QFile::remove(*ni);
		}
		for (const QString &dirLocation : newDirLocations) {
			QDir(dirLocation).removeRecursively();
		}
		return false;
	} else {
		/* Delete origins. */
		QList< QPair<QString, MessageDb *> >::iterator oi;
		for (oi = oldLocations.begin(); oi != oldLocations.end(); ++oi) {
			QFile::remove(oi->first);
		}
		for (const QString &dirLocation : oldDirLocations) {
			QDir(dirLocation).removeRecursively();
		}
		m_locDir = newLocDir;
	}

	return true;
}

bool MessageDbSet::reopenLocation(const QString &newLocDir,
    enum Organisation organisation, enum CreationManner creationManner,
    enum AbandonManner abandonManner)
{
	if (Q_UNLIKELY(m_organisation == DO_UNKNOWN)) {
		return false;
	}

	if (Q_UNLIKELY(!m_onDisk)) {
		logErrorNL("%s",
		    "Cannot reopen location of message database which resides in memory.");
		return false;
	}

	if (Q_UNLIKELY(!QDir(newLocDir).exists())) {
		logErrorNL("Directory '%s' does not exist.",
		    newLocDir.toUtf8().constData());
		return false;
	}

	for (QMap<QString, MessageDb *>::iterator i = this->begin();
	     i != this->end(); ++i) {
		MessageDb *db = i.value();

		/* Close database. */
		delete db;
	}

	/* Remove all elements from this map. */
	this->clear();

	/* Remove all possible database files from new location. */
	QStringList impedingFiles = existingDbFileNamesInLocation(newLocDir,
	    m_primaryKey, m_testing, DO_UNKNOWN, false);
	for (const QString &fileName : impedingFiles) {
		const QString name = newLocDir + QDir::separator() + fileName;
		QFile::remove(name);
		QDir(MessageDb::_assocFileDirPath(name)).removeRecursively();
	}

	const QString oldLocDir = m_locDir;
	m_locDir = newLocDir;
	m_organisation = organisation;

	MessageDb *db = Q_NULLPTR;
	if (creationManner == CM_CREATE_EMPTY_CURRENT) {
		/* Create empty file matching current date. */
		QString secKey = secondaryKey(QDateTime::currentDateTime());
		Q_ASSERT(!secKey.isNull());

		db = _accessMessageDb(secKey, true);
		if (Q_UNLIKELY(db == Q_NULLPTR)) {
			QString fileName(constructDbFileName(m_locDir,
			    m_primaryKey, secKey, m_testing, m_organisation));
			logErrorNL("Failed to open database file '%s'.",
			    fileName.toUtf8().constData());
			return false;
		}
	}

	if (abandonManner == AM_DELETE) {
		deleteRelatedInLocation(oldLocDir,
		    m_primaryKey, m_testing, DO_UNKNOWN);
	}

	return true;
}

bool MessageDbSet::deleteLocation(void)
{
	if (Q_UNLIKELY(m_organisation == DO_UNKNOWN)) {
		return false;
	}

	bool sucessfullyDeleted = true;
	for (QMap<QString, MessageDb *>::iterator i = this->begin();
	     i != this->end(); ++i) {
		MessageDb *db = i.value();

		/* Get file name. */
		const QString fileName(db->fileName());

		/* Close database. */
		delete db;

		/* Delete file. */
		logInfoNL("Deleting database file '%s'.",
		    fileName.toUtf8().constData());

		if (fileName == SQLiteDb::memoryLocation) {
			continue;
		}

		if (Q_UNLIKELY(!QFile::remove(fileName))) {
			logErrorNL("Failed to delete database file '%s'.",
			    fileName.toUtf8().constData());
			sucessfullyDeleted = false;
		} else {
			QDir(MessageDb::_assocFileDirPath(fileName)).removeRecursively();
		}
	}

	/* Remove all elements from this map. */
	this->clear();

	m_organisation = DO_UNKNOWN;

	return sucessfullyDeleted;
}

/*!
 * @brief Creates secondary key from given time.
 *
 * @param[in] time Time.
 * @return Secondary key or null string on error.
 */
static
QString secondaryKeySingleFile(const QDateTime &time)
{
	Q_UNUSED(time);

	return QString(SINGLE_FILE_SEC_KEY);
}

QString MessageDbSet::secondaryKey(const QDateTime &time) const
{
	switch (m_organisation) {
	case DO_SINGLE_FILE:
		return secondaryKeySingleFile(time);
		break;
	case DO_YEARLY:
		return yearFromDateTime(time);
		break;
	case DO_UNKNOWN:
	default:
		Q_ASSERT(0);
		return QString();
	}
}

/*!
 * @brief Returns only years fitting the supplied date range.
 *
 * @param[in] fromTime Start time, ignored when invalid.
 * @param[in] toTime End time, ignored when converted.
 * @return List of year keys or empty list on error. Returns all
 *     years when both limits are invalid.
 */
static
QStringList yearsInRange(const QStringList &years, const QDateTime &fromTime,
    const QDateTime &toTime)
{
	QStringList matchingYears;
	const bool fromValid = fromTime.isValid();
	const int fromYear = fromValid ? fromTime.toString("yyyy").toInt() : -1;
	const bool toValid = toTime.isValid();
	const int toYear = toValid ? toTime.toString("yyyy").toInt() : -1;

	for (const QString &yearStr : years) {
		bool iOk = false;
		const int year = yearStr.toInt(&iOk);
		if (Q_UNLIKELY(!iOk)) {
			continue;
		}
		if ((fromValid && (year < fromYear))
		    || (toValid && (year > toYear))) {
			continue;
		}

		matchingYears.append(
		    QStringLiteral("%1").arg(year, 4, 10, QLatin1Char('0')));
	}

	return matchingYears;
}

QStringList MessageDbSet::secondaryKeys(const QDateTime &fromTime,
    const QDateTime &toTime) const
{
	switch (m_organisation) {
	case DO_SINGLE_FILE:
		return {secondaryKeySingleFile(fromTime)};
		break;
	case DO_YEARLY:
		return yearsInRange(this->keys(), fromTime, toTime);
		break;
	case DO_UNKNOWN:
	default:
		Q_ASSERT(0);
		return QStringList();
	}
}

MessageDb *MessageDbSet::constAccessMessageDb(
    const QDateTime &deliveryTime) const
{
	const QString secondary(secondaryKey(deliveryTime));

	if (Q_UNLIKELY(secondary.isNull())) {
		return Q_NULLPTR;
	}

	/* Already opened. */
	if (this->constFind(secondary) != this->end()) {
		return (*this)[secondary];
	}

	return Q_NULLPTR;
}

MessageDb *MessageDbSet::accessMessageDb(const QDateTime &deliveryTime,
    bool writeNew)
{
	const QString secondary(secondaryKey(deliveryTime));

	if (Q_UNLIKELY(secondary.isNull())) {
		return Q_NULLPTR;
	}

	return _accessMessageDb(secondary, writeNew);
}

MessageDbSet::Organisation MessageDbSet::organisation(void) const
{
	return m_organisation;
}

QString MessageDbSet::locationDirectory(void) const
{
	return m_locDir;
}

QStringList MessageDbSet::fileNames(void) const
{
	QStringList fileList;

	bool inFiles = false;
	bool inMemory = false;

	QMap<QString, MessageDb *>::const_iterator i;
	for (i = this->begin(); i != this->end(); ++i) {
		QString fileName(i.value()->fileName());
		if (fileName == SQLiteDb::memoryLocation) {
			inMemory = true;
		} else {
			inFiles = true;
		}
		fileList.append(fileName);
	}

	if (Q_UNLIKELY(inMemory && inFiles)) {
		Q_ASSERT(0);
	}

	if (inFiles) {
		fileList.sort();
	} else if (inMemory) {
		fileList = QStringList(SQLiteDb::memoryLocation);
	}

	return fileList;
}

QStringList MessageDbSet::assocFileDirNames(void) const
{
	QStringList dirList;

	bool inFiles = false;
	bool inMemory = false;

	QMap<QString, MessageDb *>::const_iterator i;
	for (i = this->begin(); i != this->end(); ++i) {
		const QString fileName = i.value()->fileName();
		if (fileName == SQLiteDb::memoryLocation) {
			inMemory = true;
		} else {
			inFiles = true;
			QString dirName = i.value()->assocFileDirPath();
			const QFileInfo fileInfo(dirName);
			if (fileInfo.exists() && fileInfo.isDir() &&
			    (QDir(dirName).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).count() > 0)) {
				/* Directory exists and is non-empty. */
				dirList.append(dirName);
			}
		}
	}

	if (Q_UNLIKELY(inMemory && inFiles)) {
		Q_ASSERT(0);
	}

	if (inFiles) {
		dirList.sort();
	} else if (inMemory) {
		dirList.clear();
	}

	return dirList;
}

bool MessageDbSet::changePrimaryKey(const QString &newPrimaryKey)
{
	if (Q_UNLIKELY(newPrimaryKey.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	/*
	 * TODO - Test whether there are sufficient rights to perform this
	 * operation.
	 * However, it is unlikely that the user doesn't have sufficient access
	 * rights when he's using those files.
	 */

	bool inFiles = false;
	bool inMemory = false;

	const QString oldPrimaryKey(m_primaryKey);
	m_primaryKey = newPrimaryKey;

	QMap<QString, MessageDb *>::iterator i;
	for (i = this->begin(); i != this->end(); ++i) {
		QString fileName(i.value()->fileName());
		if (fileName == SQLiteDb::memoryLocation) {
			inMemory = true;
			continue;
		} else {
			inFiles = true;
		}

		QFileInfo fileInfo(fileName);
		const QString dirPath(fileInfo.absolutePath());
		fileName = fileInfo.fileName();
		fileName.replace(oldPrimaryKey, newPrimaryKey);

		const QString secondaryKey(
		    secondaryKeyFromFileName(fileName, m_organisation));
		QString newConnectionName(
		    constructKey(newPrimaryKey, secondaryKey, m_organisation));

		i.value()->renameDb(newConnectionName, dirPath + QStringLiteral("/") + fileName);
	}

	if (Q_UNLIKELY(inMemory && inFiles)) {
		Q_ASSERT(0);
	}

	return true;
}

/*!
 * @brief Using explicit enumeration values to prevent accidental usage of
 *     wrong values.
 */
enum DbStorageType {
	DST_FILE, /* Database file. */
	DST_DIR /* Accopmanying directory. */
};

/*!
 * @brief Constructs a database file/directory name.
 *
 * @param[in] primaryKey Usually user name.
 * @param[in] testing True if testing account.
 * @param[in] storageType Name for file or related directory.
 * @teturn File or directory name.
 */
static inline
QString singleFileDbName(const QString &primaryKey, bool testing,
   enum DbStorageType storageType)
{
	static const QString underscores(DB_UNDERSCORES);
	static const QString markerProd(DB_MARKER_PROD);
	static const QString markerTest(DB_MARKER_TEST);
	static const QString dbSuff(DB_SUFFIX);

	if (DST_FILE == storageType) {
		return primaryKey % underscores
		    % (testing ? markerTest : markerProd) % dbSuff;
	} else {
		return primaryKey % underscores
		    % (testing ? markerTest : markerProd);
	}
}

/*!
 * @brief Returns true if file name matches single file naming conventions.
 *
 * @param[in] fileName File name.
 * @param[in] primaryKey Usually user name.
 * @param[in] testing True if testing account.
 * @return True if file name matches the naming convention.
 */
static
bool fileNameMatchesSingleFile(const QString &fileName,
    const QString &primaryKey, bool testing)
{
	return fileName == singleFileDbName(primaryKey, testing, DST_FILE);
}

/*!
 * @brief Returns true if directory name name matches single file naming conventions.
 *
 * @param[in] dirName File name.
 * @param[in] primaryKey Usually user name.
 * @param[in] testing True if testing account.
 * @return True if directory name matches the naming convention.
 */
static
bool dirNameMatchesSingleFile(const QString &dirName,
    const QString &primaryKey, bool testing)
{
	return dirName == singleFileDbName(primaryKey, testing, DST_DIR);
}

/*!
 * @brief Constructs a yearly-organised database file/directory name matching
 *     expression.
 *
 * @param[in] primaryKey Usually user name.
 * @param[in] testing True if testing account.
 * @param[in] storageType Name for file or related directory.
 * @teturn File or directory matching regular expression.
 */
static inline
QRegularExpression yearlyFileDbRe(const QString &primaryKey, bool testing,
   enum DbStorageType storageType)
{
	static const QString lineStart("^");
	static const QString yearSection("_" YEARLY_SEC_KEY_RE DB_UNDERSCORES);
	static const QString markerProd(DB_MARKER_PROD);
	static const QString markerTest(DB_MARKER_TEST);
	static const QString dbSuff(DB_SUFFIX);
	static const QString lineEnd("$");

	if (DST_FILE == storageType) {
		return QRegularExpression(lineStart % primaryKey % yearSection
		    % (testing ? markerTest : markerProd) % dbSuff % lineEnd);
	} else {
		return QRegularExpression(lineStart % primaryKey % yearSection
		    % (testing ? markerTest : markerProd) % lineEnd);
	}
}

/*!
 * @brief Constructs an invalid database file/directory name.
 *
 * @param[in] primaryKey Usually user name.
 * @param[in] testing True if testing account.
 * @param[in] storageType Name for file or related directory.
 * @teturn File or directory name.
 */
static inline
QString invalidFileDbName(const QString &primaryKey, bool testing,
   enum DbStorageType storageType)
{
	static const QString lineStart("^");
	static const QString underscore("_");
	static const QString invalid(YEARLY_SEC_KEY_INVALID);
	static const QString underscores(DB_UNDERSCORES);
	static const QString markerProd(DB_MARKER_PROD);
	static const QString markerTest(DB_MARKER_TEST);
	static const QString dbSuff(DB_SUFFIX);
	static const QString lineEnd("$");

	if (DST_FILE == storageType) {
		return primaryKey % underscore % invalid % underscores
		    % (testing ? markerTest : markerProd) % dbSuff;
	} else {
		return primaryKey % underscore % invalid % underscores
		    % (testing ? markerTest : markerProd);
	}
}

/*!
 * @brief Returns true if file name matches yearly naming conventions.
 *
 * @param[in] fileName File name.
 * @param[in] primaryKey Usually user name.
 * @param[in] testing True if testing account.
 * @return True if file name matches the naming convention.
 */
static
bool fileNameMatchesYearly(const QString &fileName, const QString &primaryKey,
    bool testing)
{
	return yearlyFileDbRe(primaryKey, testing, DST_FILE).match(fileName).hasMatch()
	    || (fileName == invalidFileDbName(primaryKey, testing, DST_FILE));
}

/*!
 * @brief Returns true if directory name matches yearly naming conventions.
 *
 * @param[in] dirName File name.
 * @param[in] primaryKey Usually user name.
 * @param[in] testing True if testing account.
 * @return True if file name matches the naming convention.
 */
static
bool dirNameMatchesYearly(const QString &dirName, const QString &primaryKey,
    bool testing)
{
	return yearlyFileDbRe(primaryKey, testing, DST_DIR).match(dirName).hasMatch()
	    || (dirName == invalidFileDbName(primaryKey, testing, DST_DIR));
}

QStringList MessageDbSet::fileNamesAvailable(const QString &locDir,
    const QString &primaryKey, enum Organisation organisation, bool testing)
{
	if (Q_UNLIKELY(locDir.isEmpty() || primaryKey.isEmpty())) {
		return QList<QString>();
	}

	if (Q_UNLIKELY(!QFileInfo(locDir).isDir())) {
		return QList<QString>();
	}

	QStringList foundFiles;
	for (const QString &fileName : QDir(locDir).entryList(QDir::Files)) {
		if ((organisation == DO_UNKNOWN) || (organisation == DO_SINGLE_FILE)) {
			if (fileNameMatchesSingleFile(fileName, primaryKey, testing)) {
				foundFiles.append(fileName);
				continue;
			}
		}
		if ((organisation == DO_UNKNOWN) || (organisation == DO_YEARLY)) {
			if (fileNameMatchesYearly(fileName, primaryKey, testing)) {
				foundFiles.append(fileName);
				continue;
			}
		}
	}
	return foundFiles;
}

QStringList MessageDbSet::assocFileDirNamesAvailable(const QString &locDir,
    const QString &primaryKey, enum Organisation organisation, bool testing)
{
	if (Q_UNLIKELY(locDir.isEmpty() || primaryKey.isEmpty())) {
		return QList<QString>();
	}

	if (Q_UNLIKELY(!QFileInfo(locDir).isDir())) {
		return QList<QString>();
	}

	QStringList foundDirs;
	for (const QString &dirName : QDir(locDir).entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
		if ((organisation == DO_UNKNOWN) || (organisation == DO_SINGLE_FILE)) {
			if (dirNameMatchesSingleFile(dirName, primaryKey, testing)) {
				foundDirs.append(dirName);
				continue;
			}
		}
		if ((organisation == DO_UNKNOWN) || (organisation == DO_YEARLY)) {
			if (dirNameMatchesYearly(dirName, primaryKey, testing)) {
				foundDirs.append(dirName);
				continue;
			}
		}
	}
	return foundDirs;
}

QString MessageDbSet::yearFromDateTime(const QDateTime &time)
{
	if (time.isValid()) {
		return time.toString("yyyy");
	} else {
		return YEARLY_SEC_KEY_INVALID;
	}
}

MessageDbSet *MessageDbSet::createNew(const QString &locDir,
    const QString &primaryKey, bool testing, enum Organisation organisation,
    bool onDisk, const QString &connectionPrefix, enum CreationManner manner)
{
	MessageDbSet *dbSet = Q_NULLPTR;
	QStringList matchingFiles;

	if (!onDisk) {
		logInfoNL("Accessing message database in memory for '%s'.",
		    primaryKey.toUtf8().constData());
		organisation = DO_SINGLE_FILE;
	} else if (manner == CM_MUST_EXIST) {
		if (organisation == DO_UNKNOWN) {
			/*
			 * Try to determine the database organisation
			 * structure.
			 */
			organisation = dbOrganisation(locDir, primaryKey,
			    testing);
		}

		matchingFiles = existingDbFileNamesInLocation(locDir,
		    primaryKey, testing, organisation, true);

		if (Q_UNLIKELY(matchingFiles.isEmpty())) {
			return Q_NULLPTR;
		}

		/* Check primary keys. */
		foreach (const QString &fileName, matchingFiles) {
			QString secondaryKey = secondaryKeyFromFileName(
			    fileName, organisation);

			if (Q_UNLIKELY(secondaryKey.isNull())) {
				logErrorNL("Failed to obtain secondary key from file name '%s'.",
				    fileName.toUtf8().constData());
				return Q_NULLPTR;
			}
		}
	} else {
		/* Create missing directory. */
		QDir dir(locDir);
		if (!dir.exists()) {
			/* Empty file will be created automatically. */
			if (Q_UNLIKELY(!dir.mkpath(dir.absolutePath()))) {
				/* Cannot create directory. */
				return Q_NULLPTR;
			}
		}
	}

	if (Q_UNLIKELY(organisation == DO_UNKNOWN)) {
		logErrorNL("Ambiguous organisation of database '%s' in %s'.",
		    primaryKey.toUtf8().constData(),
		    locDir.toUtf8().constData());
		return Q_NULLPTR;
	}

	/* Create database set. */
	dbSet = new (::std::nothrow) MessageDbSet(locDir, primaryKey, testing,
	    organisation, onDisk, connectionPrefix);
	if (Q_UNLIKELY(dbSet == Q_NULLPTR)) {
		Q_ASSERT(0);
		return Q_NULLPTR;
	}

	MessageDb *db = Q_NULLPTR;
	if (matchingFiles.size()) {
		/* Load files that have been found. */
		foreach (const QString &fileName, matchingFiles) {
			QString secondaryKey = secondaryKeyFromFileName(
			    fileName, organisation);
			Q_ASSERT(!secondaryKey.isNull());

			db = dbSet->_accessMessageDb(secondaryKey, false);
			if (Q_UNLIKELY(db == Q_NULLPTR)) {
				logErrorNL("Failed to open database file '%s'.",
				    fileName.toUtf8().constData());
				delete dbSet;
				return Q_NULLPTR;
			}
		}
	} else if (manner == CM_CREATE_EMPTY_CURRENT) {
		/* Create empty file matching current date. */
		QString secKey = dbSet->secondaryKey(
		    QDateTime::currentDateTime());
		Q_ASSERT(!secKey.isNull());

		db = dbSet->_accessMessageDb(secKey, true);
		if (Q_UNLIKELY(db == Q_NULLPTR)) {
			QString fileName(constructDbFileName(dbSet->m_locDir,
			    dbSet->m_primaryKey, secKey, dbSet->m_testing,
			    dbSet->m_organisation));
			logErrorNL("Failed to open database file '%s'.",
			    fileName.toUtf8().constData());
			delete dbSet;
			return Q_NULLPTR;
		}
	}

	return dbSet;
}

enum MessageDbSet::Organisation MessageDbSet::dbOrganisation(
    const QString &locDir, const QString &primaryKey, bool testing)
{
	enum Organisation org = DO_UNKNOWN;

	QString singleFile;
	QStringList yearlyFiles;

	QDirIterator dirIt(locDir, QDirIterator::NoIteratorFlags);

	while (dirIt.hasNext()) {
		dirIt.next();
		if (!QFileInfo(dirIt.filePath()).isFile()) {
			continue;
		}

		QString fileName = dirIt.fileName();

		if (fileNameMatchesSingleFile(fileName, primaryKey, testing)) {
			singleFile = fileName;
		}

		if (fileNameMatchesYearly(fileName, primaryKey, testing)) {
			yearlyFiles.append(fileName);
		}
	}

	if (!singleFile.isEmpty() && yearlyFiles.isEmpty()) {
		org = DO_SINGLE_FILE;
	} else if (singleFile.isEmpty() && !yearlyFiles.isEmpty()) {
		org = DO_YEARLY;
	}

	return org;
}

/*!
 * @brief Secondary key if file name matches single file naming conventions.
 *
 * @param[in] fileName   File name.
 * @return Key if name matches the naming convention, null string on error.
 */
static
QString fileNameSecondaryKeySingleFile(const QString &fileName)
{
	static const QRegularExpression re(
	    "^" PRIMARY_KEY_RE DB_UNDERSCORES "[01]" DB_SUFFIX "$");

	return re.match(fileName).hasMatch() ? SINGLE_FILE_SEC_KEY : QString();
}

/*!
 * @brief Secondary key if file name matches single file naming conventions.
 *
 * @param[in] fileName   File name.
 * @return Key if name matches the naming convention, null string on error.
 */
static
QString fileNameSecondaryKeyYearly(const QString &fileName)
{
	static const QRegularExpression reInv(QString("^") + PRIMARY_KEY_RE
	    "_" + YEARLY_SEC_KEY_INVALID + DB_UNDERSCORES "[01]" DB_SUFFIX "$");

	if (reInv.match(fileName).hasMatch()) {
		return YEARLY_SEC_KEY_INVALID;
	}

	static const QRegularExpression reValid("^" PRIMARY_KEY_RE
	    "_" YEARLY_SEC_KEY_RE DB_UNDERSCORES "[01]" DB_SUFFIX "$");

	if (reValid.match(fileName).hasMatch()) {
		return fileName.section('_', 1, 1);
	}

	return QString();
}

QString MessageDbSet::secondaryKeyFromFileName(const QString &fileName,
    enum Organisation organisation)
{
	if (fileName.isEmpty() || (organisation == DO_UNKNOWN)) {
		return QString();
	}

	switch (organisation) {
	case DO_SINGLE_FILE:
		return fileNameSecondaryKeySingleFile(fileName);
		break;
	case DO_YEARLY:
		return fileNameSecondaryKeyYearly(fileName);
		break;
	default:
		Q_ASSERT(0);
		break;
	}

	return QString();
}

QStringList MessageDbSet::existingDbFileNamesInLocation(const QString &locDir,
    const QString &primaryKey, bool testing, enum Organisation organisation,
    bool filesOnly)
{
	QStringList matchingFiles;

	bool matches;
	QDirIterator dirIt(locDir, QDirIterator::NoIteratorFlags);

	while (dirIt.hasNext()) {
		dirIt.next();
		if (filesOnly && !QFileInfo(dirIt.filePath()).isFile()) {
			continue;
		}

		QString fileName = dirIt.fileName();

		matches = false;
		switch (organisation) {
		case DO_UNKNOWN:
			matches = matches || fileNameMatchesSingleFile(
			    fileName, primaryKey, testing);
			matches = matches || fileNameMatchesYearly(fileName,
			    primaryKey, testing);
			break;
		case DO_SINGLE_FILE:
			matches = fileNameMatchesSingleFile(fileName,
			    primaryKey, testing);
			break;
		case DO_YEARLY:
			matches = fileNameMatchesYearly(fileName, primaryKey,
			    testing);
			break;
		default:
			break;
		}

		if (matches) {
			matchingFiles.append(fileName);
		}
	}

	return matchingFiles;
}

QString MessageDbSet::constructKey(const QString &primaryKey,
    const QString &secondaryKey, enum Organisation organisation)
{
	if (organisation == DO_UNKNOWN) {
		return QString();
	}

	QString key = primaryKey;
	if (organisation == DO_YEARLY) {
		key += "_" + secondaryKey;
	}
	return key;
}

enum MessageDbSet::Error MessageDbSet::checkExistingDbFile(
    const QString &locDir, const QString &primaryKey, AccessFlags flags,
    bool onDisk)
{
	if (!onDisk) {
		/* Presume that opening database in memory won't fail. */
		return MDSERR_OK;
	}

	bool testing = flags & TESTING_ACCOUNT;

	QStringList fileNames(existingDbFileNamesInLocation(locDir,
	    primaryKey, testing, DO_UNKNOWN, false));
	enum Organisation organisation(dbOrganisation(locDir, primaryKey,
	    testing));

	if (!fileNames.isEmpty() && (organisation == DO_UNKNOWN)) {
		return MDSERR_MULTIPLE;
	}

	if (fileNames.isEmpty()) {
		return MDSERR_MISSFILE;
	}

	foreach (const QString &fileName, fileNames) {
		QString filePath(locDir + QDir::separator() + fileName);

		enum Error ret = checkGivenDbFile(filePath, flags);
		if (Q_UNLIKELY(ret != MDSERR_OK)) {
			return ret;
		}
	}

	return MDSERR_OK;
}

QString MessageDbSet::constructDbFileName(const QString &locDir,
    const QString &primaryKey, const QString &secondaryKey,
    bool testing, enum Organisation organisation)
{
	QString key = constructKey(primaryKey, secondaryKey, organisation);
	if (Q_UNLIKELY(key.isNull())) {
		return QString();
	}

	return locDir + QDir::separator() +
	    key + QString(DB_UNDERSCORES) + (testing ? "1" : "0") + DB_SUFFIX;
}

bool MessageDbSet::isValidDbFileName(const QString &fileName,
    QString &dbUserName, QString &dbYear, bool &dbTestingFlag, QString &errMsg)
{
	QStringList fileNameParts;
	bool ret = false;
	dbUserName.clear();
	dbYear.clear();
	errMsg.clear();

	if (fileName.contains(DB_UNDERSCORES)) {
		// get username from filename
		fileNameParts = fileName.split("_");
		if (fileNameParts.isEmpty() || fileNameParts.count() <= 1) {
			errMsg = tr(
			    "File '%1' does not contain a valid database filename.")
			        .arg(fileName);
			return ret;
		}
		if (fileNameParts[0].isEmpty() ||
		    (fileNameParts[0].length() != 6)) {
			errMsg = tr(
			    "File '%1' does not contain a valid username in the database filename.")
			        .arg(fileName);
			return ret;
		}
		dbUserName = fileNameParts[0];

		// get year from filename
		if (fileNameParts[1].isEmpty()) {
			dbYear.clear();
		} else if (fileNameParts[1] == MessageDb::invalidYearName) {
			dbYear = fileNameParts[1];
		} else if (fileNameParts[1].length() == 4) {
			dbYear = fileNameParts[1];
		} else {
			errMsg = tr(
			    "File '%1' does not contain a valid year in the database filename.")
			        .arg(fileName);
			dbYear.clear();
			return ret;
		}

		// get testing flag from filename
		fileNameParts = fileName.split(".");
		if (fileNameParts.isEmpty()) {
			errMsg = tr(
			    "File '%1' does not contain a valid database filename.")
			        .arg(fileName);
			return ret;
		}
		fileNameParts = fileNameParts[0].split(DB_UNDERSCORES);
		if (fileNameParts.isEmpty()) {
			errMsg = tr(
			    "File '%1' does not contain a valid database filename.")
			        .arg(fileName);
			return ret;
		}

		if (fileNameParts[1] == "1") {
			dbTestingFlag = true;
		} else if (fileNameParts[1] == "0") {
			dbTestingFlag = false;
		} else {
			errMsg = tr(
			    "File '%1' does not contain a valid account type flag or filename has wrong format.")
			        .arg(fileName);
			dbTestingFlag = false;
			return ret;
		}
	} else {
		errMsg = tr(
		    "File '%1' does not contain a valid message database or filename has wrong format.")
		        .arg(fileName);
		return ret;
	}

	return true;
}

void MessageDbSet::watchOpened(const QString &fileName)
{
	Q_UNUSED(fileName);

	Q_EMIT opened(m_primaryKey, m_testing);
}

void MessageDbSet::watchOpeningFailed(const QString &fileName)
{
	Q_EMIT openingFailed(m_primaryKey, m_testing, fileName);
}

void MessageDbSet::watchChangedLocallyRead(const QList<MsgId> &msgIds, bool read)
{
	Q_EMIT changedLocallyRead(m_primaryKey, m_testing, msgIds, read);
}

void MessageDbSet::watchChangedProcessState(const QList<MsgId> &msgIds,
    enum MessageDb::MessageProcessState state)
{
	Q_EMIT changedProcessState(m_primaryKey, m_testing, msgIds, state);
}

void MessageDbSet::watchMessageInserted(const MsgId &msgId, int direction)
{
	Q_EMIT messageInserted(m_primaryKey, m_testing, msgId, direction);
}

void MessageDbSet::watchMessageDataDeleted(const MsgId &msgId)
{
	Q_EMIT messageDataDeleted(m_primaryKey, m_testing, msgId);
}

MessageDb *MessageDbSet::_accessMessageDb(const QString &secondaryKey,
    bool create)
{
	MessageDb *db = Q_NULLPTR;
	bool openRet;

	/* Already opened. */
	if (this->find(secondaryKey) != this->end()) {
		return (*this)[secondaryKey];
	}

	if (create && (m_organisation == DO_UNKNOWN)) {
		/* Organisation structure must be set. */
		return Q_NULLPTR;
	}

	if (!create && (m_organisation == DO_UNKNOWN)) {
		/* Try to determine the structure of the present database. */
		enum Organisation org = dbOrganisation(m_locDir, m_primaryKey,
		    m_testing);
		if (org == DO_UNKNOWN) {
			logErrorNL("The organisation structure of the database '%s' in '%s' could not be determined.",
			    m_primaryKey.toUtf8().constData(),
			    m_locDir.toUtf8().constData());
			return Q_NULLPTR;
		}
		m_organisation = org;
	}

	QString connectionName(constructKey(m_primaryKey, secondaryKey,
	    m_organisation));
	if (!m_connectionPrefix.isEmpty()) {
		connectionName = m_connectionPrefix + "_" + connectionName;
	}

	db = new (::std::nothrow) MessageDb(connectionName);
	if (Q_UNLIKELY(Q_NULLPTR == db)) {
		Q_ASSERT(0);
		return Q_NULLPTR;
	}

	/* TODO -- Handle file name deviations! */
	/*
	 * Test accounts have ___1 in their names, ___0 relates to standard
	 * accounts.
	 */
	const QString dbFileName = constructDbFileName(m_locDir, m_primaryKey,
	    secondaryKey, m_testing, m_organisation);
	{
		const QFileInfo fileInfo(dbFileName);

		if (!create && !fileInfo.isFile()) {
			delete db;
			return Q_NULLPTR;
		} else if (!fileInfo.isFile()) {
			/* Create missing directory. */
			const QDir dir = fileInfo.absoluteDir().absolutePath();
			if (!dir.exists()) {
				/* Empty file will be created automatically. */
				if (!dir.mkpath(dir.absolutePath())) {
					/* Cannot create directory. */
					delete db;
					return Q_NULLPTR;
				}
			}
		}
	}

	SQLiteDb::OpenFlags flags = SQLiteDb::CREATE_MISSING;
	flags |= m_onDisk ? SQLiteDb::NO_OPTIONS : SQLiteDb::FORCE_IN_MEMORY;
	openRet = db->openDb(dbFileName, flags);
	if (!openRet) {
		delete db;
		return Q_NULLPTR;
	}
	if (create) {
		if (!db->accessDb().isValid()) {
			delete db;
			return Q_NULLPTR;
		}
	}

	connect(db, SIGNAL(opened(QString)),
	    this, SLOT(watchOpened(QString)));
	connect(db, SIGNAL(openingFailed(QString)),
	    this, SLOT(watchOpeningFailed(QString)));
	connect(db, SIGNAL(changedLocallyRead(QList<MsgId>, bool)),
	    this, SLOT(watchChangedLocallyRead(QList<MsgId>, bool)));
	connect(db, SIGNAL(changedProcessState(QList<MsgId>, enum MessageDb::MessageProcessState)),
	    this, SLOT(watchChangedProcessState(QList<MsgId>, enum MessageDb::MessageProcessState)));
	connect(db, SIGNAL(messageInserted(MsgId, int)),
	    this, SLOT(watchMessageInserted(MsgId, int)));
	connect(db, SIGNAL(messageDataDeleted(MsgId)),
	    this, SLOT(watchMessageDataDeleted(MsgId)));

	this->insert(secondaryKey, db);
	Q_EMIT madeAvailable(m_primaryKey, m_testing);
	return db;
}

void MessageDbSet::deleteRelatedInLocation(const QString &locDir,
    const QString &primaryKey, bool testing, enum Organisation organisation)
{
	QStringList deletedFiles;
	QStringList deletedDirs;

	/* Only existing database file names. */
	const QStringList abandonedFileNames = existingDbFileNamesInLocation(
	    locDir, primaryKey, testing, organisation, false);

	/* Get full paths to files and existing directories. */
	for (const QString &fileName : abandonedFileNames) {
		QString filePath = locDir % QDir::separator() % fileName;
		QString dirPath = MessageDb::_assocFileDirPath(filePath);

		deletedFiles.append(filePath);
		if (QFileInfo(dirPath).isDir()) {
			deletedDirs.append(dirPath);
		}
	}

	/* Delete files. */
	for (const QString &filePath : deletedFiles) {
		if (Q_UNLIKELY(!QFile::remove(filePath))) {
			logErrorNL("Failed to delete database file '%s'.",
			    filePath.toUtf8().constData());
		}
	}

	/* Delete directories. */
	for (const QString &dirPath : deletedDirs) {
		if (Q_UNLIKELY(!QDir(dirPath).removeRecursively())) {
			logErrorNL("Failed to delete database directory '%s'.",
			    dirPath.toUtf8().constData());
		}
	}
}

enum MessageDbSet::Error MessageDbSet::checkGivenDbFile(const QString &filePath,
    AccessFlags flags)
{
	bool checkQuick = flags & CHECK_QUICK;
	bool checkIntegity = flags & CHECK_INTEGRITY;

	if (checkIntegity) {
		checkQuick = false;
	}

	QFileInfo dbFileInfo(filePath);
	QDir dbDir(dbFileInfo.absoluteDir().absolutePath());
	QFileInfo dbDirInfo(dbDir.absolutePath());

	if (dbFileInfo.exists() && !dbFileInfo.isFile()) {
		return MDSERR_NOTAFILE;
	}

	if (!dbFileInfo.exists()) {
		if (!dbDirInfo.isReadable() || !dbDirInfo.isWritable()) {
			return MDSERR_ACCESS;
		} else {
			return MDSERR_MISSFILE;
		}
	} else {
		if (!dbFileInfo.isReadable() || !dbFileInfo.isWritable()) {
			return MDSERR_ACCESS;
		}
	}

	if (checkIntegity || checkQuick) {
		MessageDb db("someKey");
		if (!db.openDb(filePath, SQLiteDb::NO_OPTIONS)) {
			return MDSERR_DATA;
		}

		if (!db.checkDb(checkQuick)) {
			return MDSERR_DATA;
		}
	}

	return MDSERR_OK;
}
