/*
 * 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>
#ifdef QT_GUI_LIB
#  include <QGuiApplication>
#  include <QScreen>
#endif /* QT_GUI_LIB */
#include <QUrl>
#include <QUrlQuery>
#include <utility> /* ::std::move */

#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/compat_qt/random.h"
#include "src/datovka_shared/io/matomo_reporter.h"
#include "src/datovka_shared/log/log.h"

#ifndef MATOMO_REPORTER_DEBUG
#  define MATOMO_REPORTER_DEBUG 0
#endif /* !MATOMO_REPORTER_DEBUG */

//#define MATOMO_PHP "/piwik.php"
#define MATOMO_PHP "/matomo.php"

/* Null objects - for convenience. */
static const QHash<int, QString> nullHashIntStr;
static const QHash<QString, QString> nullHashStrStr;
static const QString nullString;
static const QUrl nullUrl;

#define DFLT_SITE_ID -1
#define DFLT_BOOL_VAL false

/*!
 * @brief PIMPL MatomoReporter class.
 */
class MatomoReporterPrivate {
public:
	MatomoReporterPrivate(void)
	    :  m_trackerUrl(), m_siteId(DFLT_SITE_ID), m_useRand(DFLT_BOOL_VAL),
	    m_cid(), m_uid(), m_res(), m_ua(), m_lang(), m_dimensions(),
	    m_cvar()
	{
	}

	MatomoReporterPrivate &operator=(const MatomoReporterPrivate &other) Q_DECL_NOTHROW
	{
		m_trackerUrl = other.m_trackerUrl;
		m_siteId = other.m_siteId;
		m_useRand = other.m_useRand;
		m_webAppName = other.m_webAppName;
		m_cid = other.m_cid;
		m_uid = other.m_uid;
		m_res = other.m_res;
		m_ua = other.m_ua;
		m_lang = other.m_lang;
		m_dimensions = other.m_dimensions;
		m_cvar = other.m_cvar;

		return *this;
	}

	bool operator==(const MatomoReporterPrivate &other) const
	{
		return (m_trackerUrl == other.m_trackerUrl) &&
		    (m_siteId == other.m_siteId) &&
		    (m_useRand == other.m_useRand) &&
		    (m_webAppName == other.m_webAppName) &&
		    (m_cid == other.m_cid) &&
		    (m_uid == other.m_uid) &&
		    (m_res == other.m_res) &&
		    (m_ua == other.m_ua) &&
		    (m_lang == other.m_lang) &&
		    (m_dimensions == other.m_dimensions) &&
		    (m_cvar == other.m_cvar);
	}

	QUrl m_trackerUrl; /* tracker URL */
	int m_siteId; /* site ID */
	bool m_useRand; /* whether to generate a rand value */
	QString m_webAppName; /* web application name */
	QString m_cid; /* client ID */
	QString m_uid; /* user ID */
	QString m_res; /* screen resolution */
	QString m_ua; /* user agent */
	QString m_lang; /* language */
	QHash<int, QString> m_dimensions; /* custom dimensions */
	QHash<QString, QString> m_cvar; /* custom variables */
};

MatomoReporter::MatomoReporter(QObject *parent)
    : QObject(parent),
    d_ptr(Q_NULLPTR),
    m_nam(this)
{
	connect(&m_nam, SIGNAL(finished(QNetworkReply *)),
	    this, SLOT(onReplyFinished(QNetworkReply *)));

	gatherAndSetInfo();
}

MatomoReporter::~MatomoReporter(void)
{
}

/*!
 * @brief Ensures MatomoReporterPrivate presence.
 *
 * @note Returns if MatomoReporterPrivate could not be allocated.
 */
#define ensureMatomoReporterPrivate(_x_) \
	do { \
		if (Q_UNLIKELY(d_ptr == Q_NULLPTR)) { \
			MatomoReporterPrivate *p = new (::std::nothrow) MatomoReporterPrivate; \
			if (Q_UNLIKELY(p == Q_NULLPTR)) { \
				Q_ASSERT(0); \
				return _x_; \
			} \
			d_ptr.reset(p); \
		} \
	} while (0)

const QUrl &MatomoReporter::trackerUrl(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullUrl;
	}

	return d->m_trackerUrl;
}

void MatomoReporter::setTrackerUrl(const QUrl &u)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_trackerUrl = u;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MatomoReporter::setTrackerUrl(QUrl &&u)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_trackerUrl = ::std::move(u);
}
#endif /* Q_COMPILER_RVALUE_REFS */

int MatomoReporter::siteId(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return DFLT_SITE_ID;
	}

	return d->m_siteId;
}
void MatomoReporter::setSiteId(int si)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_siteId = si;
}

bool MatomoReporter::useRand(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return DFLT_BOOL_VAL;
	}

	return d->m_useRand;
}

void MatomoReporter::setUseRand(bool ur)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_useRand = ur;
}

const QString &MatomoReporter::webAppName(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_webAppName;
}

void MatomoReporter::setWebAppName(const QString &an)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_webAppName = an;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MatomoReporter::setWebAppName(QString &&an)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_webAppName = ::std::move(an);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &MatomoReporter::clientId(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_cid;
}

void MatomoReporter::setClientId(const QString &cid)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_cid = cid;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MatomoReporter::setClientId(QString &&cid)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_cid = ::std::move(cid);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &MatomoReporter::userId(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_uid;
}

void MatomoReporter::setUserId(const QString &uid)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_uid = uid;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MatomoReporter::setUserId(QString &&uid)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_uid = ::std::move(uid);
}
#endif /* Q_COMPILER_RVALUE_REFS */

void MatomoReporter::sendPing(void)
{
	QUrl url(trackerUrl().toString() + MATOMO_PHP);
	QUrlQuery query = prepareUrlQuery("");

	query.addQueryItem("ping", "1");

	url.setQuery(query);

	sendGetRequest(url);
}

void MatomoReporter::sendVisit(const QString &path, const QString &actionName)
{
	QUrl url(trackerUrl().toString() + MATOMO_PHP);
	QUrlQuery query = prepareUrlQuery(path);
	QString visitVars = collectVisitVariables();

	if (!visitVars.isEmpty()) {
		query.addQueryItem("_cvar", visitVars);
	}
	if (!actionName.isEmpty()) {
		query.addQueryItem("action_name", actionName);
	}

	url.setQuery(query);

	sendGetRequest(url);
}

void MatomoReporter::sendEvent(const QString &path, const QString &eventCategory,
    const QString &eventAction, const QString &eventName, int eventValue)
{
	QUrl url(trackerUrl().toString() + MATOMO_PHP);
	QUrlQuery query = prepareUrlQuery(path);

	if (!eventCategory.isEmpty()) {
		query.addQueryItem("e_c", eventCategory);
	}

	if (!eventAction.isEmpty()) {
		query.addQueryItem("e_a", eventAction);
	}

	if (!eventName.isEmpty()) {
		query.addQueryItem("e_n", eventName);
	}

	query.addQueryItem("e_v", QString::number(eventValue));

	url.setQuery(query);

	sendGetRequest(url);
}

void MatomoReporter::setCustomDimension(int id, const QString &val)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_dimensions[id] = val;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MatomoReporter::setCustomDimension(int id, QString &&val)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_dimensions[id] = ::std::move(val);
}
#endif /* Q_COMPILER_RVALUE_REFS */

void MatomoReporter::setCustomVisitVariables(const QString &key,
    const QString &val)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_cvar[key] = val;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MatomoReporter::setCustomVisitVariables(const QString &key, QString &&val)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

	d->m_cvar[key] = ::std::move(val);
}
#endif /* Q_COMPILER_RVALUE_REFS */

void MatomoReporter::onReplyFinished(QNetworkReply *reply)
{
	if (Q_NULLPTR != reply) {
		const QUrl url = reply->url();
		emit requestFinished(url);
#if MATOMO_REPORTER_DEBUG
		logDebugLv1NL("Got reply from '%s'.",
		    url.toEncoded().constData());
#endif /* MATOMO_REPORTER_DEBUG */
	}
}

void MatomoReporter::onReplyError(QNetworkReply::NetworkError code)
{
#if MATOMO_REPORTER_DEBUG
	logErrorNL("network error code: %d", code);
#else /* !MATOMO_REPORTER_DEBUG */
	Q_UNUSED(code);
#endif /* MATOMO_REPORTER_DEBUG */
}

void MatomoReporter::gatherAndSetInfo(void)
{
	ensureMatomoReporterPrivate();
	Q_D(MatomoReporter);

#ifdef QT_GUI_LIB
	{
		/* Get resolution for GUI applications. */

		const QScreen *primaryScreen = QGuiApplication::primaryScreen();
		if (Q_NULLPTR != primaryScreen) {
			const QRect geometry = primaryScreen->geometry();
			d->m_res = QString("%1x%2")
			    .arg(geometry.width())
			    .arg(geometry.height());
		} else {
			d->m_res = macroStdMove(QString());
		}
	}
#endif /* QT_GUI_LIB */

	{
		/* Gather some OS information. */

		/* Try to identify the operating system. */
		QString os = "Other";

#ifdef Q_OS_LINUX
		os = "Linux";
#endif /* Q_OS_LINUX */

#ifdef Q_OS_MAC
		os = "Macintosh";
#endif /* Q_OS_MAC */

#ifdef Q_OS_WIN /* Q_OS_WIN32 amd Q_OS_WIN64 */
		os = "Windows";
#endif /* Q_OS_WIN */

	/* Matomo has troubles identifying macOS from Qt information. */
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
#  ifdef Q_OS_MAC
		os = "Macintosh " + QSysInfo::prettyProductName();
#  else /* !Q_OS_MAC */
		os = QSysInfo::prettyProductName() + ", " +
		    QSysInfo::currentCpuArchitecture();
#  endif /* Q_OS_MAC */
#endif /* >= Qt-5.4 */

		/* Locale. */
		QString locale = QLocale::system().name().toLower().replace("_", "-");

		/* Set the user agent. */
		d->m_ua = QString("Mozilla/5.0 (%1; %2) "
		    "MatomoReporter/0.0 (Qt/%3)")
		    .arg(os).arg(locale).arg(QT_VERSION_STR);

		// set the user language
		d->m_lang = macroStdMove(locale);
	}
}

const QString &MatomoReporter::screenResolution(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_res;
}

const QString &MatomoReporter::userAgent(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_ua;
}

const QString &MatomoReporter::language(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_lang;
}

const QHash<int, QString> &MatomoReporter::customDimentsions(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullHashIntStr;
	}

	return d->m_dimensions;
}

const QHash<QString, QString> &MatomoReporter::customVariables(void) const
{
	Q_D(const MatomoReporter);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullHashStrStr;
	}

	return d->m_cvar;
}

QUrlQuery MatomoReporter::prepareUrlQuery(const QString &path) const
{
	QUrlQuery query;
	query.addQueryItem("idsite", QString::number(siteId()));

	if (useRand()) {
		qint64 val;
		Compat::randFillRange((char *)&val, sizeof(val));
		QByteArray data((const char *)&val, sizeof(val));
		query.addQueryItem("rand", data.toBase64());
	}

	{
		const QString &cid = clientId();
		if (!cid.isEmpty()) {
			query.addQueryItem("_id", cid);
			query.addQueryItem("cid", cid);
		}
	}
	{
		const QString &uid = userId();
		if (!uid.isEmpty()) {
			query.addQueryItem("uid", uid);
		}
	}
	query.addQueryItem("url",
	    QString("http://%1/%2").arg(webAppName()).arg(path));

	/* To record the request. */
	query.addQueryItem("rec", "1");

	/* Matomo API version. */
	query.addQueryItem("apiv", "1");

	{
		const QString &res = screenResolution();
		if (!res.isEmpty()) {
			query.addQueryItem("res", res);
		}
	}

	{
		const QString &ua = userAgent();
		if (!ua.isEmpty()) {
			query.addQueryItem("ua", ua);
		}
	}

	{
		const QString &lang = language();
		if (!lang.isEmpty()) {
			query.addQueryItem("lang", lang);
		}
	}

	{
		const QHash<int, QString> &dims = customDimentsions();
		if (dims.count() > 0) {
			QHash<int, QString>::const_iterator i;
			for (i = dims.constBegin(); i != dims.constEnd(); ++i) {
				query.addQueryItem("dimension" + QString::number(i.key()), i.value());
			}
		}
	}

	return query;
}

QString MatomoReporter::collectVisitVariables(void) const
{
	QString varString;
	/*
	 * See https://github.com/piwik/piwik/issues/2165
	 * Need to pass in format {"1":["key1","value1"],"2":["key2","value2"]}
	 */
	const QHash<QString, QString> &vars = customVariables();
	if (vars.count() > 0) {
		QHash<QString, QString>::const_iterator i;
		varString.append("{");
		int num = 0;
		for (i = vars.constBegin(); i != vars.constEnd(); ++i) {
			if (num != 0) {
				varString.append(",");
			}
			QString thisvar = QString(R"("%1":["%2","%3"])")
			    .arg(num + 1)
			    .arg(i.key())
			    .arg(i.value());
			varString.append(thisvar);
			++num;
		}
		varString.append("}");
	}
	return varString;
}

void MatomoReporter::sendGetRequest(const QUrl &url)
{
{
	QNetworkReply *reply = m_nam.get(QNetworkRequest(url));

	if (Q_NULLPTR != reply) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
		connect(reply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)),
		    this, SLOT(onReplyError(QNetworkReply::NetworkError)));
#else /* < Qt-5.15 */
		connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
		    this, SLOT(onReplyError(QNetworkReply::NetworkError)));
#endif /* >= Qt-5.15 */

		/* Ignoring SSL errors. */
		connect(reply, SIGNAL(sslErrors(QList<QSslError>)),
		    reply, SLOT(ignoreSslErrors()));

#if MATOMO_REPORTER_DEBUG
		logDebugLv1NL("Sending get request to '%s'.", url.toEncoded().constData());
#endif /* MATOMO_REPORTER_DEBUG */
	}
}
}
