/*
 * 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.
 */

#include <QByteArray>
#include <QDateTime>
#include <QNetworkInterface>
#include <QRegularExpression>
#include <QSysInfo>
#include <QtEndian>

#include "src/datovka_shared/compat_qt/random.h"
#include "src/io/stats_reporter.h"

//#define MATOMO_URL "http://localhost/matomo"
#define MATOMO_URL "https://matomo.nic.cz/"
//#define SITE_ID 1
#define SITE_ID 80
//#define WEBAPP_NAME "matomo_test"
#define WEBAPP_NAME "usage.datovka.cz"

#define UNIQUE_ID_BINARY_SIZE 32 /* Bytes */

StatsReporter::StatsReporter(void)
    : m_reporter(Q_NULLPTR)
{
	m_reporter.setTrackerUrl(QUrl(MATOMO_URL));
	m_reporter.setSiteId(SITE_ID);
	m_reporter.setWebAppName(WEBAPP_NAME);
	m_reporter.setUseRand(true);
}

QString StatsReporter::escapeQuotes(QString str)
{
	return str.replace(QRegularExpression("\""), "\\\"");
}

/*!
 * @brief Return current time data.
 */
static
QByteArray timeData(void)
{
	return QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmsszzz").toUtf8();
}

/*!
 * @brief Return any MAC address if some could be acquired as binary data.
 */
static
QByteArray macNumData(void)
{
	qint64 num = 0;

	for (const QNetworkInterface &iface : QNetworkInterface::allInterfaces()) {
		/* Return the first non-loop-back MAC address of active device. */
		const int flags = iface.flags();
		if ((flags & (QNetworkInterface::IsUp | QNetworkInterface::IsRunning)
		    && (!(flags & QNetworkInterface::IsLoopBack)))) {
			/* Remove colons and convert from hex to integer. */
			bool ok = false;
			num = iface.hardwareAddress().replace(':', "").toLongLong(&ok, 16);
			if (ok) {
				break;
			}
		}
	}

	if (0 != num) {
		qint64 beNum;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
		qToBigEndian(num, &beNum);
#else /* < Qt-5.7 */
		beNum = qToBigEndian(num);
#endif /* >= Qt-5.7 */
		return QByteArray((const char *)&beNum, sizeof(beNum));
	}

	return QByteArray();
}

/*!
 * @brief Construct possibly unique identifier.
 *
 * @note This identifier is not stable in time.
 */
static
QByteArray constructReplacementId(void)
{
	QByteArray data = timeData();
	data.append(macNumData());

	int size = data.size();
	if (UNIQUE_ID_BINARY_SIZE > size) {
		/* Add additional random characters. */
		int missing = UNIQUE_ID_BINARY_SIZE - size;
		for (int i = 0; i < missing; ++i) {
			data.append((char)Compat::randBounded(256));
		}
		return data;
	} else if (UNIQUE_ID_BINARY_SIZE == size) {
		return data;
	} else {
		return data.left(UNIQUE_ID_BINARY_SIZE);
	}
}

QString StatsReporter::constructUniqueId(void)
{
	QByteArray data;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
	/*
	 * On some systems this value may persist upon reboot on others not.
	 * Empty data can be acquired on some systems.
	 * Returned value is usually 32 bytes long.
	 */
	data = QSysInfo::machineUniqueId();
#endif /* >= Qt-5.11 */
	if (Q_UNLIKELY(data.isEmpty())) {
		data = constructReplacementId();
	}
	return data.toBase64();
}

/*!
 * @brief Returns the name of the visit root based on the application type.
 */
static
const QString &visitRoot(enum Json::Report::AppType appType)
{
	static const QString unknown("UNKNOWN");
	static const QString testing("TESTING");
	static const QString desktop("DESKTOP");
	static const QString mobile("MOBILE");

	switch (appType) {
	case Json::Report::AT_UNKNOWN:
		return unknown;
		break;
	case Json::Report::AT_TESTING:
		return testing;
		break;
	case Json::Report::AT_DESKTOP:
		return desktop;
		break;
	case Json::Report::AT_MOBILE:
		return mobile;
		break;
	default:
		return unknown;
		break;
	}
}

QString StatsReporter::constructVisitPath(enum Json::Report::AppType appType)
{
	QString currentCpuArch;
	QString productName;
	QString productVersion;
	QString buildCpuArch;

#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
	currentCpuArch = QSysInfo::currentCpuArchitecture();
	productName = QSysInfo::prettyProductName();
	productVersion = QSysInfo::productVersion();
	buildCpuArch = QSysInfo::buildCpuArchitecture();
#else /* < Qt-5.4 */
	/* Qt-5.4 is old enough in order not to bother with the implementation. */
	currentCpuArch = "unknown";
	productName = "unknown";
	productVersion = "unknown";
	buildCpuArch = "unknown";
#endif /* >= Qt-5.4 */

	/* Path consists of: CPUARCH-X/OS-X/APPBUILDARCH-X/APPVER-X */
	return QString("%1/CPUARCH-%2/OS-%3-%4/APPBUILDARCH-%5/APPVER-%6")
	    .arg(visitRoot(appType)) // 1
	    .arg(currentCpuArch) // 2
	    .arg(productName) // 3
	    .arg(productVersion) // 4
	    .arg(buildCpuArch) // 5
	    .arg(VERSION); // 6
}

void StatsReporter::reportVisit(enum Json::Report::AppType at,
    const QString &mui, const QString &path, Json::UsageReport usage)
{
	if (Q_UNLIKELY(mui.isEmpty())) {
		/* Don't send anything without unique id. */
		return;
	}

	const QString visitRootLowercase = visitRoot(at).toLower();

	m_reporter.setUserId(mui);

	{
		Json::BuildReport buildReport;
		buildReport.setAppType(at);
		buildReport.setUniqueId(mui);

		m_reporter.setCustomVisitVariables(
		    QString("%1_build_report").arg(visitRootLowercase),
		    StatsReporter::escapeQuotes(buildReport.toJsonData()));
	}

	{
		Json::ConfReport confReport;
		confReport.setAppType(at);
		confReport.setUniqueId(mui);

		m_reporter.setCustomVisitVariables(
		    QString("%1_conf_report").arg(visitRootLowercase),
		    StatsReporter::escapeQuotes(confReport.toJsonData()));
	}

	if (!usage.isNull()) {
		usage.setAppType(at);
		usage.setUniqueId(mui);

		m_reporter.setCustomVisitVariables(
		    QString("%1_usage_report").arg(visitRootLowercase),
		    StatsReporter::escapeQuotes(usage.toJsonData()));
	}

	m_reporter.sendVisit(path, "");
}
