/*
 * 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>
#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0))
#  include <QDirListing>
#endif /* >= Qt-6.8 */
#include <QFile>
#include <QFileInfo>
#include <QStringBuilder>

#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/io/filesystem.h"
#include "src/settings/preferences.h"

/*! Default configuration folder location. */
#define DFLT_CONF_SUBDIR ".dsgui"
/*! Default configuration file name. */
#define DFLT_CONF_FILE "dsgui.conf"

/* Fixed settings. */
static const QString acntDbFile("messages.shelf.db"); /*!< Account db file. */
static const QString tagDbFile("tag.db"); /*!< Tag db file. */
static const QString recMgmtDbFile("records_management.db"); /*!< Records management db file. */
static const QString prefsDbFile("prefs.db"); /*!< Preferences db file. */
static const QString timestampDbFile("timestamp.db"); /*!< Timestamp db file. */
static const QString draftDbFile("drafts.db"); /*!< Draft db file. */

INIPreferences::INIPreferences(void)
    : _confBaseDir(), /* Empty value means default. */
    _confSubdir(), /* Empty value means default DFLT_CONF_SUBDIR. */
    _confDir(), /* Empty value means _confBaseDir + _confSubdir . */
    _loadFromConf(), /* Empty value means default DFLT_CONF_FILE. */
    _saveToConf() /* Empty value means default DFLT_CONF_FILE. */
{
}

QString INIPreferences::confPresenceErrorMessage(enum ConfPresence presenceCode,
    const QString &problemPath)
{
	switch (presenceCode) {
	case ACCESSIBLE:
		return tr("Path is accessible.");
		break;
	case CONF_DIR_CANNOT_CREATE:
		return tr("Directory '%1' couldn't be created.")
		    .arg(QDir::toNativeSeparators(problemPath));
		break;
	case CONF_DIR_NOT_EXECUTABLE:
		return tr("Directory '%1' cannot be accessed. Execute permission is missing.")
		    .arg(QDir::toNativeSeparators(problemPath));
		break;
	case CONF_DIR_NOT_WRITEABLE:
		return tr("Directory '%1' cannot be written to. Write permission is missing.")
		    .arg(QDir::toNativeSeparators(problemPath));
		break;
	case CONF_DIR_NOT_READABLE:
		return tr("Directory '%1' cannot be read. Read permission is missing.")
		    .arg(QDir::toNativeSeparators(problemPath));
		break;
	case CONF_FILE_CANNOT_CREATE:
		return tr("File '%1' couldn't be created.")
		    .arg(QDir::toNativeSeparators(problemPath));
		break;
	case CONF_FILE_NOT_WRITEABLE:
		return tr("File '%1' cannot be written to. Write permission is missing.")
		    .arg(QDir::toNativeSeparators(problemPath));
		break;
	case CONF_FILE_NOT_READABLE:
		return tr("File '%1' cannot be read. Read permission is missing.")
		    .arg(QDir::toNativeSeparators(problemPath));
		break;
	default:
		return QString();
		break;
	}
}

/*!
 * @brief Create a printable path listing.
 *
 * @param[in] pathList List of paths.
 * @param[in] separator List entry separator.
 * @return String of paths separated by \a separator.
 */
static
QString pathListing(const QStringList &pathList, const QString &separator)
{
	QString output;

	bool isFirst = true;
	for (const QString &path : pathList) {
		if (!isFirst) {
			output += separator;
		}
		output += QDir::toNativeSeparators(path);

		isFirst = false;
	}

	return output;
}

QString INIPreferences::confPresenceErrorMessage2(
    enum ConfPresence presenceCode, const QStringList &problemPaths)
{
	const QString sepStr("\n");

	switch (presenceCode) {
	case ACCESSIBLE:
		return tr("Paths are accessible.");
		break;
	case CONF_DIR_CANNOT_CREATE:
		return tr("Following directories couldn't be created.")
		    % sepStr % sepStr % pathListing(problemPaths, sepStr);
		break;
	case CONF_DIR_NOT_EXECUTABLE:
		return tr("Following directories cannot be accessed. Execute permission is missing.")
		    % sepStr % sepStr % pathListing(problemPaths, sepStr);
		break;
	case CONF_DIR_NOT_WRITEABLE:
		return tr("Following directories cannot be written to. Write permission is missing.")
		    % sepStr % sepStr % pathListing(problemPaths, sepStr);
		break;
	case CONF_DIR_NOT_READABLE:
		return tr("Following directories cannot be read. Read permission is missing.")
		    % sepStr % sepStr % pathListing(problemPaths, sepStr);
		break;
	case CONF_FILE_CANNOT_CREATE:
		return tr("Following files couldn't be created.")
		    % sepStr % sepStr % pathListing(problemPaths, sepStr);
		break;
	case CONF_FILE_NOT_WRITEABLE:
		return tr("Following files cannot be written to. Write permission is missing.")
		    % sepStr % sepStr % pathListing(problemPaths, sepStr);
		break;
	case CONF_FILE_NOT_READABLE:
		return tr("Following files cannot be read. Read permission is missing.")
		    % sepStr % sepStr % pathListing(problemPaths, sepStr);
		break;
	default:
		return QString();
		break;
	}
}

/*!
 * @brief Search for auxiliary file in specified location.
 *
 * @note The file should be created using the auxConfFilePath() function.
 *
 * @param[in] confDir Directory to search in.
 * @param[in] confFileName Normal configuration file name (without the aux suffix).
 * @return Path to the latest found file.
 */
static
QString findAuxFilePath(const QString &confDir, const QString &confFileName)
{
	QStringList auxFiles;

	{
		QDirIterator dirIt(confDir, {confFileName + ".aux.*"},
		    QDir::Files | QDir::NoDotAndDotDot,
		    QDirIterator::NoIteratorFlags);
		while (dirIt.hasNext()) {
			dirIt.next();

			auxFiles.append(dirIt.fileName());
		}
	}

	if (auxFiles.isEmpty()) {
		return QString();
	}

	/* File name contains creation time. */
	auxFiles.sort();

	/* Use last matching found. */
	return confDir % QDir::separator() % auxFiles.last();
}

/*!
 * @brief Return default configuration file name if \a fileName is empty.
 *
 * @param[in] fileName File name.
 * @return Default if \a fileName is empty.
 */
static
QString confFileName(const QString &fileName)
{
	if (fileName.isEmpty()) {
		return DFLT_CONF_FILE;
	} else {
		return fileName;
	}
}

enum INIPreferences::ConfPresence INIPreferences::ensureConfPresence(
    QString *problemPathPtr) const
{
	QString problemPath;
	enum ConfPresence retCode = ACCESSIBLE;

	{
		QDir dir(confDir());
		if (Q_UNLIKELY(!dir.exists())) {
			if (Q_UNLIKELY(!dir.mkpath("."))) {
				problemPath = dir.absolutePath();
				retCode = CONF_DIR_CANNOT_CREATE;
				goto fail;
			}
		}
	}

	{
		const QFileInfo fi(confDir());
		problemPath = fi.absoluteFilePath();
#if !defined(Q_OS_WIN)
		if (Q_UNLIKELY(!fi.isExecutable())) {
			retCode = CONF_DIR_NOT_EXECUTABLE;
			goto fail;
		}
#endif /* !Q_OS_WIN */
		if (Q_UNLIKELY(!fi.isWritable())) {
			retCode = CONF_DIR_NOT_WRITEABLE;
			goto fail;
		}
		if (Q_UNLIKELY(!fi.isReadable())) {
			retCode = CONF_DIR_NOT_READABLE;
			goto fail;
		}
	}

	/* Try finding an auxiliary file and renaming it to the original. */
	if (Q_UNLIKELY(!QFileInfo(loadConfPath()).exists())) {
		const QString auxFilePath = findAuxFilePath(confDir(), confFileName(_loadFromConf));

		if (!auxFilePath.isEmpty()) {
			/* Rename found file. */
			QFile::rename(auxFilePath, loadConfPath());
		}
	}

	{
		/* Create empty file if still missing. */
		QFile file(loadConfPath());
		const QFileInfo fi(loadConfPath());
		problemPath = fi.absoluteFilePath();
		if (file.exists()) {
			if (Q_UNLIKELY(!fi.isWritable())) {
				retCode = CONF_FILE_NOT_WRITEABLE;
				goto fail;
			}
			if (Q_UNLIKELY(!fi.isReadable())) {
				retCode = CONF_FILE_NOT_READABLE;
				goto fail;
			}
		} else {
			if (!file.open(QIODevice::ReadWrite)) {
				retCode = CONF_FILE_CANNOT_CREATE;
				goto fail;
			}
			file.close();
		}
	}
	return ACCESSIBLE;

fail:
	if (Q_NULLPTR != problemPathPtr) {
		*problemPathPtr = macroStdMove(problemPath);
	}
	return retCode;
}

enum INIPreferences::ConfPresence INIPreferences::checkConfiguration(
    QStringList &problemPaths) const
{
	QList<QFileInfo> dirInfos;
	QList<QFileInfo> fileInfos;

#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0))
	for (const QDirListing::DirEntry &entry : QDirListing(confDir(), QDirListing::IteratorFlag::Default)) { /* No subdirectories. */
		QFileInfo fi = entry.fileInfo();

		if (fi.isFile()) {
			fileInfos.append(fi);
		} else if (fi.isDir()) {
			dirInfos.append(fi);
		}
	}
#else /* < Qt-6.8 */
	QDirIterator it(confDir(), QStringList(), QDir::NoDotAndDotDot | QDir::AllEntries, QDirIterator::NoIteratorFlags); /* No subdirectories. */
	while (it.hasNext()) {
		it.next();
		QFileInfo fi = it.fileInfo();

		if (fi.isFile()) {
			fileInfos.append(fi);
		} else if (fi.isDir()) {
			dirInfos.append(fi);
		}
	}
#endif /* >= Qt-6.8 */

	QStringList notExecutableDirs;
	QStringList notWritableDirs;
	QStringList notReadableDirs;

	for (const QFileInfo &fi : dirInfos) {
#if !defined(Q_OS_WIN)
		if (Q_UNLIKELY(!fi.isExecutable())) {
			notExecutableDirs.append(fi.absoluteFilePath());
		} else
#endif /* !Q_OS_WIN */
		if (Q_UNLIKELY(!fi.isWritable())) {
			notWritableDirs.append(fi.absoluteFilePath());
		} else if (Q_UNLIKELY(!fi.isReadable())) {
			notReadableDirs.append(fi.absoluteFilePath());
		}
	}

	QStringList notWritableFiles;
	QStringList notReadableFiles;

	for (const QFileInfo &fi : fileInfos) {
		if (Q_UNLIKELY(!fi.isWritable())) {
			notWritableFiles.append(fi.absoluteFilePath());
		} else if (Q_UNLIKELY(!fi.isReadable())) {
			notReadableFiles.append(fi.absoluteFilePath());
		}
	}

	if (Q_UNLIKELY(!notExecutableDirs.isEmpty())) {
		problemPaths = macroStdMove(notExecutableDirs);
		return CONF_DIR_NOT_EXECUTABLE;
	}
	if (Q_UNLIKELY(!notWritableDirs.isEmpty())) {
		problemPaths = macroStdMove(notWritableDirs);
		return CONF_DIR_NOT_WRITEABLE;
	}
	if (Q_UNLIKELY(!notReadableDirs.isEmpty())) {
		problemPaths = macroStdMove(notReadableDirs);
		return CONF_DIR_NOT_READABLE;
	}

	if (Q_UNLIKELY(!notWritableFiles.isEmpty())) {
		problemPaths = macroStdMove(notWritableFiles);
		return CONF_FILE_NOT_WRITEABLE;
	}
	if (Q_UNLIKELY(!notReadableFiles.isEmpty())) {
		problemPaths = macroStdMove(notReadableFiles);
		return CONF_FILE_NOT_READABLE;
	}

	return ACCESSIBLE;
}

/*!
 * @brief Return default configuration subdirectory if \a subdir is empty.
 *
 * @param[in] subdir Subdirectory name.
 * @return Default if \a subdir is empty.
 */
static
QString confSubdir(const QString &subdir)
{
	if (subdir.isEmpty()) {
		return DFLT_CONF_SUBDIR;
	} else {
		return subdir;
	}
}

QString INIPreferences::confDir(void) const
{
	if (_confDir.isEmpty()) {
		return confDirPath(_confBaseDir, confSubdir(_confSubdir));
	} else {
		return _confDir;
	}
}

QString INIPreferences::loadConfPath(void) const
{
	return confDir() % QDir::separator() % confFileName(_loadFromConf);
}

QString INIPreferences::saveConfPath(void) const
{
	return confDir() % QDir::separator() % confFileName(_saveToConf);
}

QString INIPreferences::acntDbPath(void) const
{
	return confDir() % QDir::separator() % acntDbFile;
}

QString INIPreferences::tagDbPath(void) const
{
	return confDir() % QDir::separator() % tagDbFile;
}

QString INIPreferences::recMgmtDbPath(void) const
{
	return confDir() % QDir::separator() % recMgmtDbFile;
}

QString INIPreferences::prefsDbPath(void) const
{
	return confDir() % QDir::separator() % prefsDbFile;
}

QString INIPreferences::timestampDbPath(void) const
{
	return confDir() % QDir::separator() % timestampDbFile;
}

QString INIPreferences::draftDbPath(void) const
{
	return confDir() % QDir::separator() % draftDbFile;
}
