/************************************************************************
 **
 **  @file   vganalytics.cpp
 **  @author Roman Telezhynskyi <dismine(at)gmail.com>
 **  @date   26 6, 2023
 **
 **  @brief
 **  @copyright
 **  This source code is part of the Valentina project, a pattern making
 **  program, whose allow create and modeling patterns of clothing.
 **  Copyright (C) 2023 Valentina project
 **  <https://gitlab.com/smart-pattern/valentina> All Rights Reserved.
 **
 **  Valentina 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.
 **
 **  Valentina 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 Valentina.  If not, see <http://www.gnu.org/licenses/>.
 **
 *************************************************************************/
#include "vganalytics.h"
#include "vganalyticsworker.h"

#include <vcsRepoState.h>
#include <atomic>
#include <functional>
#include <memory>
#include <QDataStream>
#include <QDebug>
#include <QGuiApplication>
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocale>
#include <QLoggingCategory>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QQueue>
#include <QScreen>
#include <QSettings>
#include <QString>
#include <QTimer>
#include <QUrlQuery>
#include <QUuid>

#ifdef Q_OS_WIN
#include <windows.h>
#elif defined(Q_OS_MACOS)
#include <mach/mach.h>
#include <mach/mach_host.h>
#elif defined(Q_OS_LINUX)
#include <sys/sysinfo.h>
#endif

#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
#include "../vmisc/compatibility.h"
#endif

#include "../vmisc/vcommonsettings.h"
#include "def.h"

using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;

namespace
{

//---------------------------------------------------------------------------------------------------------------------
auto GetSystemMemorySize() -> qint64
{
#ifdef Q_OS_WIN
    MEMORYSTATUSEX memory_status;
    ZeroMemory(&memory_status, sizeof(MEMORYSTATUSEX));
    memory_status.dwLength = sizeof(MEMORYSTATUSEX);
    if (GlobalMemoryStatusEx(&memory_status))
    {
        return static_cast<qint64>(memory_status.ullTotalPhys);
    }

    return -1;
#elif defined(Q_OS_MACOS)
    vm_size_t pageSize;
    vm_statistics64_data_t vmStats;

    mach_port_t machPort = mach_host_self();
    mach_msg_type_number_t count = sizeof(vmStats) / sizeof(natural_t);
    host_page_size(machPort, &pageSize);

    host_statistics64(machPort, HOST_VM_INFO, reinterpret_cast<host_info64_t>(&vmStats), &count);

    auto freeMemory = static_cast<qulonglong>(vmStats.free_count) * static_cast<qulonglong>(pageSize);
    qulonglong totalMemoryUsed =
        (static_cast<qulonglong>(vmStats.active_count) + static_cast<qulonglong>(vmStats.inactive_count) +
         static_cast<qulonglong>(vmStats.wire_count)) *
        static_cast<qulonglong>(pageSize);
    return static_cast<qint64>(freeMemory + totalMemoryUsed);
#elif defined(Q_OS_LINUX)
    struct sysinfo info; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init)
    sysinfo(&info);
    return static_cast<qint64>(info.totalram) * static_cast<qint64>(info.mem_unit);
#else
    // Unsupported platform
    return -1;
#endif
}

//---------------------------------------------------------------------------------------------------------------------
auto GetSystemMemorySizeGB() -> double
{
    if (qint64 const totalMemoryBytes = GetSystemMemorySize(); totalMemoryBytes != -1)
    {
        return static_cast<double>(totalMemoryBytes) / (1024 * 1024 * 1024); // Convert bytes to gigabytes
    }

    return -1.0; // Unsupported platform or error
}

//---------------------------------------------------------------------------------------------------------------------
auto TotalMemory() -> QString
{
    double const size = qRound(GetSystemMemorySizeGB() * 10.0) / 10.0;
    return !qFuzzyCompare(size, -1.0) ? QStringLiteral("%1 GB").arg(size) : QStringLiteral("Unknown RAM");
}
} // namespace

//---------------------------------------------------------------------------------------------------------------------
VGAnalytics::VGAnalytics(QObject *parent)
  : QObject{parent},
    d(new VGAnalyticsWorker(this))
{
}

//---------------------------------------------------------------------------------------------------------------------
VGAnalytics::~VGAnalytics()
{
    delete d;
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::Instance() -> VGAnalytics *
{
    static VGAnalytics *instance = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
    if (instance == nullptr)
    {
        instance = new VGAnalytics();
    }

    return instance;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::Init(const VCommonSettings *settings)
{
    SCASSERT(settings != nullptr)

    auto *statistic = VGAnalytics::Instance();
    QString clientID = settings->GetClientID();
    if (clientID.isEmpty())
    {
        clientID = QUuid::createUuid().toString();
        settings->SetClientID(clientID);
    }
    statistic->SetClientID(clientID);
    statistic->SetGUILanguage(settings->GetLocale());
    statistic->SetMeasurementId(GA_MEASUREMENT_ID);
    statistic->SetApiSecret(GA_API_SECRET);
    statistic->SetRepoRevision(QLatin1String(VCS_REPO_STATE_REVISION));
    statistic->Enable(settings->IsCollectStatistic());
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetLogLevel(enum VGAnalytics::LogLevel logLevel)
{
    d->m_logLevel = logLevel;
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::LogLevel() const -> enum VGAnalytics::LogLevel { return d->m_logLevel; }

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetRepoRevision(QString rev)
{
    d->m_repoRevision = !rev.isEmpty() ? rev.remove(QStringLiteral("Git:")) : QStringLiteral("Unknown");
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::RepoRevision() const -> QString
{
    return d->m_repoRevision;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetGUILanguage(const QString &language)
{
    d->m_guiLanguage = language.toLower().replace('_'_L1, '-'_L1);
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::GUILanguage() const -> QString
{
    return d->m_guiLanguage;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetSendInterval(int milliseconds)
{
    d->m_timer.setInterval(milliseconds);
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::SendInterval() const -> int
{
    return (d->m_timer.interval());
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::IsEnabled() -> bool
{
    return d->m_isEnabled;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetMeasurementId(const QString &measurementId)
{
    d->m_measurementId = measurementId;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetApiSecret(const QString &apiSecret)
{
    d->m_apiSecret = apiSecret;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetClientID(const QString &clientID)
{
    d->m_clientID = clientID;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::Enable(bool state)
{
    d->Enable(state);
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SetNetworkAccessManager(QNetworkAccessManager *networkAccessManager)
{
    if (d->networkManager != networkAccessManager)
    {
        // Delete the old network manager if it was our child
        if (d->networkManager && d->networkManager->parent() == this)
        {
            d->networkManager->deleteLater();
        }

        d->networkManager = networkAccessManager;
    }
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::NetworkAccessManager() const -> QNetworkAccessManager *
{
    return d->networkManager;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendAppFreshInstallEvent(qint64 engagementTimeMsec)
{
    SendEvent(QStringLiteral("vapp_fresh_install"), InitAppStartEventParams(engagementTimeMsec));
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendAppStartEvent(qint64 engagementTimeMsec)
{
    SendEvent(QStringLiteral("vapp_start"), InitAppStartEventParams(engagementTimeMsec));
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendAppCloseEvent(qint64 engagementTimeMsec)
{
    QHash<QString, QJsonValue> const params{
        // {QStringLiteral("category"), ""},
        // In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id
        // must be supplied as part of the params for an event.
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
        {QStringLiteral("engagement_time_msec"), engagementTimeMsec},
    };

    SendEvent(QStringLiteral("vapp_close"), params);

    qCDebug(vAnalytics) << "Flushing close event before shutdown.";

    if (QNetworkReply *reply = d->SendAnalytics())
    {
        QTimer timer;
        timer.setSingleShot(true);
        timer.start(3s);

        QEventLoop loop;
        connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
        QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
        loop.exec(); // wait for finished

        qCDebug(vAnalytics) << "Close event flush finished" << (timer.isActive() ? "(reply received)." : "(timed out).");
    }
    else
    {
        qCDebug(vAnalytics) << "Nothing to flush for close event.";
    }
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendPatternToolUsedEvent(qint64 engagementTimeMsec, const QString &toolName)
{
    QHash<QString, QJsonValue> const params{
        // {QStringLiteral("category"), ""},
        // In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id
        // must be supplied as part of the params for an event.
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
        {QStringLiteral("engagement_time_msec"), engagementTimeMsec},
        {QStringLiteral("tool_name"), toolName},
    };

    SendEvent(QStringLiteral("vapp_pattern_tool_used"), params);
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendPatternFormatVersion(qint64 engagementTimeMsec, const QString &version)
{
    QHash<QString, QJsonValue> const params{
        // In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id
        // must be supplied as part of the params for an event.
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
        {QStringLiteral("engagement_time_msec"), engagementTimeMsec},
        {QStringLiteral("format_version"), version},
    };

    SendEvent(QStringLiteral("vapp_pattern_format_version"), params);
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendIndividualMeasurementsFormatVersion(qint64 engagementTimeMsec, const QString &version)
{
    QHash<QString, QJsonValue> const params{
        // In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id
        // must be supplied as part of the params for an event.
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
        {QStringLiteral("engagement_time_msec"), engagementTimeMsec},
        {QStringLiteral("format_version"), version},
    };

    SendEvent(QStringLiteral("vapp_individual_measurements_format_version"), params);
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendMultisizeMeasurementsFormatVersion(qint64 engagementTimeMsec, const QString &version)
{
    QHash<QString, QJsonValue> const params{
        // In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id
        // must be supplied as part of the params for an event.
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
        {QStringLiteral("engagement_time_msec"), engagementTimeMsec},
        {QStringLiteral("format_version"), version},
    };

    SendEvent(QStringLiteral("vapp_multisize_measurements_format_version"), params);
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::SendLayoutFormatVersion(qint64 engagementTimeMsec, const QString &version)
{
    QHash<QString, QJsonValue> const params{
        // In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id
        // must be supplied as part of the params for an event.
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
        {QStringLiteral("engagement_time_msec"), engagementTimeMsec},
        {QStringLiteral("format_version"), version},
    };

    SendEvent(QStringLiteral("vapp_layout_format_version"), params);
}

//---------------------------------------------------------------------------------------------------------------------
/**
 * A query for a POST message will be created to report this event. The
 * created query will be stored in a message queue.
 */
void VGAnalytics::SendEvent(const QString &eventName, const QHash<QString, QJsonValue> &params)
{
    /*
    // event body example
    {
        "client_id": "XXXXXXXXXX.YYYYYYYYYY",
        "events": [{
            "name": "refund",
            "params": {
                "currency": "USD",
                "value": "9.99",
                "transaction_id": "ABC-123"
            }
        }]
    }
    */

    QJsonObject root;
    root[QStringLiteral("client_id")] = d->m_clientID;

    QJsonObject event;
    event[QStringLiteral("name")] = eventName;

    QJsonObject eventParams;

    auto i = params.constBegin();
    while (i != params.constEnd())
    {
        eventParams[i.key()] = i.value();
        ++i;
    }

    // In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id must
    // be supplied as part of the params for an event.
    // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast)
    eventParams[QStringLiteral("session_id")] = qApp->sessionId();
    event[QStringLiteral("params")] = eventParams;

    QJsonArray eventArray;
    eventArray.append(event);
    root[QStringLiteral("events")] = eventArray;

    d->EnqueQueryWithCurrentTime(root);
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::InitAppStartEventParams(qint64 engagementTimeMsec) const -> QHash<QString, QJsonValue>
{
    QHash<QString, QJsonValue> params{
        // {QStringLiteral("category"), ""},
        {QStringLiteral("qt_version"), QT_VERSION_STR},
        {QStringLiteral("app_version"), d->m_appVersion},
        {QStringLiteral("word_size"), QStringLiteral("%1 bit").arg(QSysInfo::WordSize)},
        {QStringLiteral("cpu_architecture"), QSysInfo::currentCpuArchitecture()},
        {QStringLiteral("revision"), d->m_repoRevision},
        {QStringLiteral("os_version"), QSysInfo::prettyProductName()},
        {QStringLiteral("screens_number"), d->m_screensNumber},
        {QStringLiteral("screen_pixel_ratio"), d->m_screenPixelRatio},
        {QStringLiteral("screen_size"), d->m_screenResolution},
        {QStringLiteral("screen_scale_factor"), d->m_screenScaleFactor},
        // In order for user activity to display in standard reports like Realtime, engagement_time_msec and
        // session_id must be supplied as part of the params for an event.
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports
        {QStringLiteral("engagement_time_msec"), engagementTimeMsec},
        {QStringLiteral("gui_language"), d->m_guiLanguage},
        {QStringLiteral("country_code"), CountryCode()},
        {QStringLiteral("kernel_type"), QSysInfo::kernelType()},
        {QStringLiteral("total_memory"), TotalMemory()},
    };
    return params;
}

//---------------------------------------------------------------------------------------------------------------------
auto VGAnalytics::CountryCode() -> QString
{
    QNetworkAccessManager manager;
    QNetworkRequest const request(QUrl(QStringLiteral("https://api.country.is")));
    QNetworkReply *reply = manager.get(request);

    QTimer timer;
    timer.setSingleShot(true);
    timer.start(5s); // Set the timeout to 5 seconds

    QEventLoop eventLoop;

    QObject::connect(&timer, &QTimer::timeout, &eventLoop, &QEventLoop::quit);
    QObject::connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);

    eventLoop.exec();

    auto country = QStringLiteral("Unknown");

    if (timer.isActive())
    {
        // The API response was received before the timeout
        if (reply->error() == QNetworkReply::NoError)
        {
            QByteArray const responseData = reply->readAll();
            QJsonParseError error;
            QJsonDocument const jsonDoc = QJsonDocument::fromJson(responseData, &error);

            if (error.error == QJsonParseError::NoError && jsonDoc.isObject())
            {
                QJsonObject jsonObj = jsonDoc.object();
                if (jsonObj.contains(QStringLiteral("country")))
                {
                    country = jsonObj[QStringLiteral("country")].toString().toLower();
                }
            }
        }
        else
        {
            qDebug() << "Error:" << reply->errorString();
        }
    }
    else
    {
        // Timeout occurred
        qDebug() << "Request timed out";
        reply->abort();
    }

    // Clean up the reply
    reply->deleteLater();

    return country;
}

//---------------------------------------------------------------------------------------------------------------------
void VGAnalytics::CheckCountryCodeAsync(std::function<void(const QString &)> callback)
{
    struct RequestState
    {
        QNetworkAccessManager *manager = nullptr;
        QNetworkReply *reply = nullptr;
        QTimer *timer = nullptr;
        std::function<void(const QString &)> callback{};
        std::atomic_bool done{false};
    };

    auto state = std::make_shared<RequestState>();
    state->manager = new QNetworkAccessManager;
    state->callback = std::move(callback);

    QNetworkRequest const request(QUrl(QStringLiteral("https://api.country.is")));
    state->reply = state->manager->get(request);

    state->timer = new QTimer;
    state->timer->setSingleShot(true);

    auto finish = [state](const QString &country) -> void
    {
        bool expected = false;
        if (!state->done.compare_exchange_strong(expected, true))
        {
            return;
        }

        state->timer->stop();
        state->timer->deleteLater();
        state->reply->deleteLater();
        state->manager->deleteLater();
        state->callback(country);
    };

    QObject::connect(state->timer,
                     &QTimer::timeout,
                     [state, finish]() -> void
                     {
                         state->reply->abort();
                         finish(QStringLiteral("Unknown"));
                     });

    QObject::connect(state->reply,
                     &QNetworkReply::finished,
                     [state, finish]() -> void
                     {
                         auto country = QStringLiteral("Unknown");
                         if (state->reply->error() == QNetworkReply::NoError)
                         {
                             const QByteArray responseData = state->reply->readAll();
                             QJsonParseError error;
                             if (const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &error);
                                 error.error == QJsonParseError::NoError && jsonDoc.isObject())
                             {
                                 if (const QJsonObject jsonObj = jsonDoc.object();
                                     jsonObj.contains(QStringLiteral("country")))
                                 {
                                     country = jsonObj[QStringLiteral("country")].toString().toLower();
                                 }
                             }
                         }
                         finish(country);
                     });

    // Abort the pending request on app shutdown so the SSL thread can clean up before
    // QApplication destructs. Without this, the SSL backend thread may fire events into
    // freed Qt objects, causing EXC_BAD_ACCESS.
    QObject::connect(qApp, &QCoreApplication::aboutToQuit, state->reply,
                     [state]() { state->reply->abort(); });

    state->timer->start(5s);
}

//---------------------------------------------------------------------------------------------------------------------
/**
 * Qut stream to persist class GAnalytics.
 */
auto operator<<(QDataStream &outStream, const VGAnalytics &analytics) -> QDataStream &
{
    outStream << analytics.d->PersistMessageQueue();

    return outStream;
}

//---------------------------------------------------------------------------------------------------------------------
/**
 * In stream to read GAnalytics from file.
 */
auto operator>>(QDataStream &inStream, VGAnalytics &analytics) -> QDataStream &
{
    QList<QString> dataList;
    inStream >> dataList;
    analytics.d->ReadMessagesFromFile(dataList);

    return inStream;
}
