/*
 * Copyright (C) 2014-2024 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.
 */

#ifndef WIN32
#include <clocale>
#endif /* !WIN32 */
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcessEnvironment>
#include <QSysInfo>
#include <QTimeZone>
#include <QTranslator>

#include "src/cli/cli_parser.h"
#include "src/crypto/crypto_funcs.h"
#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/crypto/crypto_trusted_certs.h"
#include "src/datovka_shared/identifiers/account_id.h"
#include "src/datovka_shared/io/prefs_db.h"
#include "src/datovka_shared/io/records_management_db.h"
#include "src/datovka_shared/localisation/localisation.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/pin.h"
#include "src/datovka_shared/settings/prefs.h"
#include "src/datovka_shared/settings/records_management.h"
#include "src/datovka_shared/worker/pool.h"
#include "src/global.h"
#include "src/identifiers/account_id_db.h"
#include "src/identifiers/message_id.h"
#include "src/initialisation.h"
#include "src/io/account_db.h"
#include "src/io/isds_sessions.h"
#include "src/io/file_downloader.h"
#include "src/io/filesystem.h"
#include "src/io/message_db.h"
#include "src/io/message_db_set_container.h"
#if defined(ENABLE_TAGS)
#  include "src/io/tag_container.h"
#  include "src/io/tag_db.h"
#endif /* defined(ENABLE_TAGS) */
#include "src/isds/qt_signal_slot.h"
#include "src/settings/accounts.h"
#include "src/settings/preferences.h"
#include "src/settings/prefs_specific.h"
#include "src/settings/proxy.h"
#include "src/settings/tag_container_settings.h"
#include "src/single/single_instance.h"
#include "src/worker/message_emitter.h"

void setDefaultLocale(void)
{
#ifndef WIN32
	QString lang(QProcessEnvironment::systemEnvironment().value("LANG"));
	if (lang.isEmpty() || ("c" == lang.toLower())) {
#define LANG_DEF "en_GB.UTF-8"
		logWarning(
		    "The LANG environment variable is missing or unset. Setting to '%s'.\n",
		    LANG_DEF);
		if (Q_UNLIKELY(NULL == setlocale(LC_ALL, LANG_DEF))) {
			logErrorNL("Setting locale to '%s' failed.", LANG_DEF);
		}
#undef LANG_DEF
	}
#endif /* !WIN32 */
}

int preferencesSetUp(const QCommandLineParser &parser, INIPreferences &iniPrefs,
    LogDevice &log)
{
	int logFileId = -1;

	if (parser.isSet(CONF_SUBDIR_OPT)) {
		iniPrefs.confSubdir = parser.value(CONF_SUBDIR_OPT);
	}
	if (parser.isSet(LOAD_CONF_OPT)) {
		iniPrefs.loadFromConf = parser.value(LOAD_CONF_OPT);
	}
	if (parser.isSet(SAVE_CONF_OPT)) {
		iniPrefs.saveToConf = parser.value(SAVE_CONF_OPT);
	}
	if (parser.isSet(LOG_FILE)) {
		QString logFileName = parser.value(LOG_FILE);
		logFileId = log.openFile(logFileName.toUtf8().constData(),
		    LogDevice::LM_APPEND);
		if (Q_UNLIKELY(-1 == logFileId)) {
			logErrorNL("Cannot open log file '%s'.",
			    logFileName.toUtf8().constData());
			return -1;
		}
		/* Log warnings. */
		log.setLogLevelBits(logFileId, LOGSRC_ANY,
		    LOG_UPTO(LOG_WARNING));
	}
#ifdef DEBUG
	if (parser.isSet(DEBUG_OPT) || parser.isSet(DEBUG_VERBOSITY_OPT)) {
		log.setLogLevelBits(LogDevice::LF_STDERR, LOGSRC_ANY,
		    LOG_UPTO(LOG_DEBUG));
		if (-1 != logFileId) {
			log.setLogLevelBits(logFileId, LOGSRC_ANY,
			    LOG_UPTO(LOG_DEBUG));
		}
	}
	if (parser.isSet(DEBUG_VERBOSITY_OPT)) {
		bool ok;
		int value = parser.value(DEBUG_VERBOSITY_OPT).toInt(&ok, 10);
		if (Q_UNLIKELY(!ok)) {
			logErrorNL("%s", "Invalid debug-verbosity parameter.");
			exit(EXIT_FAILURE);
		}
		logInfoNL("Setting debugging verbosity to value '%d'.", value);
		log.setDebugVerbosity(value);
	}
#endif /* DEBUG */
	if (parser.isSet(LOG_VERBOSITY_OPT)) {
		bool ok;
		int value = parser.value(LOG_VERBOSITY_OPT).toInt(&ok, 10);
		if (Q_UNLIKELY(!ok)) {
			logErrorNL("%s", "Invalid log-verbosity parameter.");
			exit(EXIT_FAILURE);
		}
		logInfoNL("Setting logging verbosity to value '%d'.", value);
		log.setLogVerbosity(value);
	}

	return 0;
}

void downloadCRL(void)
{
	/* Start downloading the CRL files. */
	QList<QUrl> urlList;
	FileDownloader fDown(true);
	const struct crl_location *crl = crl_locations;
	const char **url;
	while ((NULL != crl) && (NULL != crl->file_name)) {
		urlList.clear();

		url = crl->urls;
		while ((NULL != url) && (NULL != *url)) {
			urlList.append(QUrl(*url));
			++url;
		}

		QByteArray data = fDown.download(urlList, 2000);
		if (!data.isEmpty()) {
			if (0 != crypto_add_crl(data.data(),
			        data.size())) {
				logWarningNL(
				    "Couldn't load downloaded CRL file '%s'.",
				    crl->file_name);
			} else {
				logInfoNL("Loaded CRL file '%s'.",
				    crl->file_name);
			}
		} else {
			logWarningNL("Couldn't download CRL file '%s'.",
			    crl->file_name);
		}

		++crl;
	}
}

void loadLocalisation(const Prefs &prefs)
{
	static QTranslator qtTranslator, appTranslator;

	QString language;
	{
		QString val;
		if (prefs.strVal("translation.language", val)) {
			if (!val.isEmpty()) {
				language = macroStdMove(val);
			}
		}
	}

	/* Check for application localisation location. */
	QString localisationDir;
	QString localisationFile;

	localisationDir = appLocalisationDir();

	logInfoNL("Loading application localisation from path '%s'.",
	    localisationDir.toUtf8().constData());

	localisationFile = "datovka_" + Localisation::shortLangName(language);

	Localisation::setProgramLocale(language);

	if (!appTranslator.load(localisationFile, localisationDir)) {
		logWarningNL(
		    "Could not load localisation file '%s' from directory '%s'.",
		    localisationFile.toUtf8().constData(),
		    localisationDir.toUtf8().constData());
	}

	QCoreApplication::installTranslator(&appTranslator);

	localisationDir = qtLocalisationDir();

	logInfoNL("Loading Qt localisation from path '%s'.",
	    localisationDir.toUtf8().constData());

	{
		const QString langName(Localisation::shortLangName(language));
		if (langName != Localisation::langEn) {
			localisationFile =
			    "qtbase_" + Localisation::shortLangName(language);
		} else {
			localisationFile = QString();
		}
	}

	if (!localisationFile.isEmpty() &&
	    !qtTranslator.load(localisationFile, localisationDir)) {
		logWarningNL(
		    "Could not load localisation file '%s' from directory '%s'.",
		    localisationFile.toUtf8().constData(),
		    localisationDir.toUtf8().constData());
	}

	QCoreApplication::installTranslator(&qtTranslator);
}

void logAppVersion(void)
{
	/* Crude architecture bit width check. */
	logInfoNL("Application version: %s (%u-bit).", VERSION,
	    (unsigned int)(sizeof(void *) * 8));
}

void logEnvironment(void)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
	logInfoNL("Compile-time target architecture: %s.", QSysInfo::buildAbi().toUtf8().constData());
	logInfoNL("Compile-time CPU architecture: %s.", QSysInfo::buildCpuArchitecture().toUtf8().constData());
	logInfoNL("Run-time CPU architecture: %s.", QSysInfo::currentCpuArchitecture().toUtf8().constData());
	logInfoNL("Running on '%s' version '%s'.", QSysInfo::productType().toUtf8().constData(), QSysInfo::productVersion().toUtf8().constData());
#endif /* >= Qt-5.4 */
}

void logQtVersion(void)
{
	logInfoNL("Compile-time Qt version: 0x%x (%s).", QT_VERSION, QT_VERSION_STR);
	logInfoNL("Run-time Qt version: %s.", qVersion());
}

void logTimeZone(void)
{
	const QTimeZone sysZone(
#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
	    QTimeZone::systemTimeZone()
#else /* < Qt-5.5 */
	    QTimeZone::systemTimeZoneId()
#endif /* >= Qt-5.5 */
	);
	int offset = sysZone.offsetFromUtc(QDateTime::currentDateTime());
	logInfoNL("Local time zone is %s (UTC%+ds).", sysZone.id().constData(),
	    offset);
}

void declareTypes(void)
{
	AcntId::declareTypes();
	AcntIdDb::declareTypes();
	Isds::declareTypes();
	MessageDb::declareTypes();
	MsgId::declareTypes();
}

int allocGlobLog(void)
{
	GlobInstcs::logPtr = new (::std::nothrow) LogDevice;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::logPtr)) {
		return -1;
	}

	return 0;
}

void deallocGlobLog(void)
{
	if (Q_NULLPTR != GlobInstcs::logPtr) {
		delete GlobInstcs::logPtr;
		GlobInstcs::logPtr = Q_NULLPTR;
	}
}

int allocGlobInfrastruct(void)
{
	GlobInstcs::snglInstEmitterPtr =
	    new (::std::nothrow) SingleInstanceEmitter;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::snglInstEmitterPtr)) {
		logErrorNL("%s",
		    "Cannot allocate single instance message emitter.");
		goto fail;
	}

	GlobInstcs::msgProcEmitterPtr =
	    new (::std::nothrow) MessageProcessingEmitter;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::msgProcEmitterPtr)) {
		logErrorNL("%s", "Cannot allocate task message emitter." );
		goto fail;
	}

	/*
	 * Only one worker thread currently.
	 * TODO -- To be able to run multiple threads in the pool a locking
	 * mechanism over isds context structures must be implemented. Also,
	 * per-context queueing ought to be implemented to avoid unnecessary
	 * waiting.
	 */
	GlobInstcs::workPoolPtr = new (::std::nothrow) WorkerPool(1);
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::workPoolPtr)) {
		logErrorNL("%s", "Cannot allocate worker pool.");
		goto fail;
	}

	return 0;

fail:
	deallocGlobInfrastruct();
	return -1;
}

void deallocGlobInfrastruct(void)
{
	if (Q_NULLPTR != GlobInstcs::workPoolPtr) {
		delete GlobInstcs::workPoolPtr;
		GlobInstcs::workPoolPtr = Q_NULLPTR;
	}
	if (Q_NULLPTR != GlobInstcs::msgProcEmitterPtr) {
		delete GlobInstcs::msgProcEmitterPtr;
		GlobInstcs::msgProcEmitterPtr = Q_NULLPTR;
	}

	if (Q_NULLPTR != GlobInstcs::snglInstEmitterPtr) {
		delete GlobInstcs::snglInstEmitterPtr;
		GlobInstcs::snglInstEmitterPtr = Q_NULLPTR;
	}
}

int allocGlobSettings(void)
{
	GlobInstcs::iniPrefsPtr = new (::std::nothrow) INIPreferences;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::iniPrefsPtr)) {
		logErrorNL("%s", "Cannot allocate INI preferences.");
		goto fail;
	}

	GlobInstcs::proxSetPtr = new (::std::nothrow) ProxiesSettings;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::proxSetPtr)) {
		logErrorNL("%s", "Cannot allocate proxy settings.");
		goto fail;
	}

	GlobInstcs::pinSetPtr = new (::std::nothrow) PinSettings;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::pinSetPtr)) {
		logErrorNL("%s", "Cannot allocated PIN settings.");
		goto fail;
	}

	GlobInstcs::recMgmtSetPtr =
	    new (::std::nothrow) RecordsManagementSettings;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::recMgmtSetPtr)) {
		logErrorNL("%s",
		    "Cannot allocate records management settings.");
		goto fail;
	}

	GlobInstcs::prefsPtr = new (::std::nothrow) Prefs;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::prefsPtr)) {
		logErrorNL("%s", "Cannot allocate database preferences.");
		goto fail;
	}

	return 0;

fail:
	deallocGlobSettings();
	return -1;
}

void deallocGlobSettings(void)
{
	if (Q_NULLPTR != GlobInstcs::recMgmtSetPtr) {
		delete GlobInstcs::recMgmtSetPtr;
		GlobInstcs::recMgmtSetPtr = Q_NULLPTR;
	}
	if (Q_NULLPTR != GlobInstcs::pinSetPtr) {
		delete GlobInstcs::pinSetPtr;
		GlobInstcs::pinSetPtr = Q_NULLPTR;
	}
	if (Q_NULLPTR != GlobInstcs::proxSetPtr) {
		delete GlobInstcs::proxSetPtr;
		GlobInstcs::proxSetPtr = Q_NULLPTR;
	}
	if (Q_NULLPTR != GlobInstcs::iniPrefsPtr) {
		delete GlobInstcs::iniPrefsPtr;
		GlobInstcs::iniPrefsPtr = Q_NULLPTR;
	}
}

int allocGlobPrefs(const INIPreferences &iniPrefs)
{
	SQLiteDb::OpenFlags flags = SQLiteDb::NO_OPTIONS;

	GlobInstcs::prefsDbPtr = new (::std::nothrow) PrefsDb("prefsDb", false);
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::prefsDbPtr)) {
		logErrorNL("%s", "Cannot allocate preference db.");
		goto fail;
	}
	/* Open preferences database. */
	flags = SQLiteDb::CREATE_MISSING;
	if (Q_UNLIKELY(!GlobInstcs::prefsDbPtr->openDb(iniPrefs.prefsDbPath(), flags))) {
		logErrorNL("Error opening preference db '%s'.",
		    iniPrefs.prefsDbPath().toUtf8().constData());
		goto fail;
	}

	return 0;

fail:
	deallocGlobPrefs();
	return -1;
}

void deallocGlobPrefs(void)
{
	if (Q_NULLPTR != GlobInstcs::prefsDbPtr) {
		delete GlobInstcs::prefsDbPtr;
		GlobInstcs::prefsDbPtr = Q_NULLPTR;
	}
}

#if defined(ENABLE_TAGS)
void createBackupOfOldFormatData(const INIPreferences &iniPrefs)
{
	bool backUp = false;
	const QString tagDbPath = iniPrefs.tagDbPath();

	{
		if (Q_UNLIKELY(!QFileInfo(tagDbPath).exists())) {
			return;
		}
		TagDb tagDb("tagDb", false);
		if (Q_UNLIKELY(!tagDb.openDb(tagDbPath, SQLiteDb::NO_OPTIONS))) {
			return;
		}
		Json::TagDbInfo info;
		tagDb.getDbInfo(info);
		backUp = info.formatVersionMajor() < 1;
	}

	if (backUp) {
		const QString target =
		    iniPrefs.confDir() + QDir::separator() + "_tag_v0_bak.db";
		bool ret = QFile::copy(tagDbPath, target);
		if (Q_UNLIKELY(!ret)) {
			logErrorNL("Cannot copy file from '%s' to '%s'.",
			    tagDbPath.toUtf8().constData(),
			    target.toUtf8().constData());
		}
	}
}
#endif /* defined(ENABLE_TAGS) */

int allocGlobContainers(const Prefs &prefs, const INIPreferences &iniPrefs)
{
	SQLiteDb::OpenFlags flags = SQLiteDb::NO_OPTIONS;

	/*
	 * These objects cannot be globally accessible static objects.
	 * The unpredictable order of constructing and destructing these
	 * objects causes segmentation faults upon their destruction.
	 *
	 * TODO -- Solve the problem of this globally accessible structures.
	 */

	GlobInstcs::isdsSessionsPtr = new (::std::nothrow) IsdsSessions;
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::isdsSessionsPtr)) {
		logErrorNL("%s", "Cannot allocate session container.");
		goto fail;
	}

	GlobInstcs::accntDbPtr = new (::std::nothrow) AccountDb("accountDb", false);
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::accntDbPtr)) {
		logErrorNL("%s", "Cannot allocate account db.");
		goto fail;
	}
	/* Open accounts database. */
	flags = SQLiteDb::CREATE_MISSING;
	flags |= PrefsSpecific::accountDbOnDisk(prefs) ?
	    SQLiteDb::NO_OPTIONS : SQLiteDb::FORCE_IN_MEMORY;
	if (Q_UNLIKELY(!GlobInstcs::accntDbPtr->openDb(iniPrefs.acntDbPath(), flags))) {
		logErrorNL("Error opening account db '%s'.",
		    iniPrefs.acntDbPath().toUtf8().constData());
		goto fail;
	}

	/* Create message DB container. */
	GlobInstcs::msgDbsPtr = new (::std::nothrow) DbContainer(
	    PrefsSpecific::messageDbOnDisk(prefs), "GLOBALDBS");
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::msgDbsPtr)) {
		logErrorNL("%s", "Cannot allocate message db container.");
		goto fail;
	}

#if defined(ENABLE_TAGS)
	GlobInstcs::tagContSetPtr = new (::std::nothrow) TagContainerSettings();
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::tagContSetPtr)) {
		logErrorNL("%s", "Cannot allocate tag container settings.");
		goto fail;
	}
	GlobInstcs::tagContPtr = new (::std::nothrow) TagContainer(TagContainer::BACK_DB);
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::tagContPtr)) {
		logErrorNL("%s", "Cannot allocate tag container.");
		goto fail;
	}
	/* Check tag database backend presence. */
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::tagContPtr->backendDb())) {
		logErrorNL("%s", "Cannot allocate tag db.");
		goto fail;
	}
	/* Open tags database. */
	flags = SQLiteDb::CREATE_MISSING;
	if (Q_UNLIKELY(!GlobInstcs::tagContPtr->backendDb()->openDb(iniPrefs.tagDbPath(), flags))) {
		logErrorNL("Error opening tag db '%s'.",
		    iniPrefs.tagDbPath().toUtf8().constData());
		goto fail;
	}
	/* Check tag client backend presence. */
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::tagContPtr->backendClient())) {
		logErrorNL("%s", "Cannot allocate tag client.");
		goto fail;
	}
#endif /* defined(ENABLE_TAGS) */

	GlobInstcs::recMgmtDbPtr = new (::std::nothrow)
	    RecordsManagementDb("recordsManagementDb", false);
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::recMgmtDbPtr)) {
		logErrorNL("%s", "Cannot allocate records management db.");
		goto fail;
	}
	/* Open records management database. */
	flags = SQLiteDb::CREATE_MISSING;
	if (Q_UNLIKELY(!GlobInstcs::recMgmtDbPtr->openDb(iniPrefs.recMgmtDbPath(), flags))) {
		logErrorNL("Error opening records management db '%s'.",
		    iniPrefs.recMgmtDbPath().toUtf8().constData());
		goto fail;
	}

	GlobInstcs::acntMapPtr = new (::std::nothrow) AccountsMap(
	    AcntContainer::CT_REGULAR);
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::acntMapPtr)) {
		logErrorNL("%s", "Cannot allocate account container.");
		goto fail;
	}

	GlobInstcs::shadowAcntsPtr = new (::std::nothrow) AccountsMap(
	    AcntContainer::CT_SHADOW);
	if (Q_UNLIKELY(Q_NULLPTR == GlobInstcs::shadowAcntsPtr)) {
		logErrorNL("%s", "Cannot allocate shadow account container.");
		goto fail;
	}

	return 0;

fail:
	deallocGlobContainers();
	return -1;
}

void deallocGlobContainers(void)
{
	if (Q_NULLPTR != GlobInstcs::shadowAcntsPtr) {
		delete GlobInstcs::shadowAcntsPtr;
		GlobInstcs::shadowAcntsPtr = Q_NULLPTR;
	}

	if (Q_NULLPTR != GlobInstcs::acntMapPtr) {
		delete GlobInstcs::acntMapPtr;
		GlobInstcs::acntMapPtr = Q_NULLPTR;
	}

	if (Q_NULLPTR != GlobInstcs::recMgmtDbPtr) {
		delete GlobInstcs::recMgmtDbPtr;
		GlobInstcs::recMgmtDbPtr = Q_NULLPTR;
	}
#if defined(ENABLE_TAGS)
	if (Q_NULLPTR != GlobInstcs::tagContPtr) {
		delete GlobInstcs::tagContPtr;
		GlobInstcs::tagContPtr = Q_NULLPTR;
	}
	if (Q_NULLPTR != GlobInstcs::tagContSetPtr) {
		delete GlobInstcs::tagContSetPtr;
		GlobInstcs::tagContSetPtr = Q_NULLPTR;
	}
#endif /* defined(ENABLE_TAGS) */
	if (Q_NULLPTR != GlobInstcs::msgDbsPtr) {
		delete GlobInstcs::msgDbsPtr;
		GlobInstcs::msgDbsPtr = Q_NULLPTR;
	}
	if (Q_NULLPTR != GlobInstcs::accntDbPtr) {
		delete GlobInstcs::accntDbPtr;
		GlobInstcs::accntDbPtr = Q_NULLPTR;
	}

	if (Q_NULLPTR != GlobInstcs::isdsSessionsPtr) {
		delete GlobInstcs::isdsSessionsPtr;
		GlobInstcs::isdsSessionsPtr = Q_NULLPTR;
	}
}

int loadPreferencesDatabase(void)
{
	if (Q_UNLIKELY((GlobInstcs::prefsPtr == Q_NULLPTR) ||
	        (GlobInstcs::prefsDbPtr == Q_NULLPTR))) {
		return -1;
	}

	return
	    GlobInstcs::prefsPtr->setDatabase(GlobInstcs::prefsDbPtr) ? 0 : -1;
}
