/*
 * Copyright (C) 2015-2026 Savoir-faire Linux Inc.
 *
 * 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/>.
 */

#include "mainapplication.h"

#include "global.h"
#include "qmlregister.h"
#include "appsettingsmanager.h"
#include "accountsettingsmanager.h"
#include "connectivitymonitor.h"
#include "systemtray.h"
#include "previewengine.h"
#include "crashreporter.h"
#include "apiserver.h"
#include "apitokenmanager.h"
#include "apitokenlistmodel.h"

#include <QWKQuick/qwkquickglobal.h>

#include <QAction>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QFontDatabase>
#include <QMenu>
#include <QQmlContext>
#include <QResource>
#include <QTimer>
#include <QTranslator>
#include <QLibraryInfo>
#include <QQuickWindow>

#if WITH_WEBENGINE
#include <QtWebEngineCore/QWebEngineProfile>
#include <QDir>
#include <QStandardPaths>
#endif

#ifdef Q_OS_WIN
#include <windows.h>
#endif

#ifdef Q_OS_UNIX
#include "globalinstances.h"
#include "dbuserrorhandler.h"
#endif

#ifdef Q_OS_MACOS
#include <os/macos/macutils.h>
#endif

#include <QWindow>

Q_LOGGING_CATEGORY(clientLog, "client")

static const QtMessageHandler QT_DEFAULT_MESSAGE_HANDLER = qInstallMessageHandler(0);

void
messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
    const static std::string fmt[5] = {"DBG", "WRN", "CRT", "FTL", "INF"};
    const QByteArray localMsg = msg.toUtf8();
    const auto ts = QString::number(QDateTime::currentMSecsSinceEpoch());
    const auto tid = QString::number(reinterpret_cast<quintptr>(QThread::currentThreadId()), 16);

    QString fileLineInfo = "";
    const auto isQml = QString(context.category) == QLatin1String("qml");
#ifdef QT_DEBUG
    // In debug mode, always include file URI (including line info).
    // Only do this when the level Info/Debug, as it is already included in the constructed
    // message for the other levels.
    if (type == QtDebugMsg || type == QtInfoMsg || !isQml) {
        auto fileName = isQml ? context.file : QUrl::fromLocalFile(context.file).toString();
        fileLineInfo = QString(" %1:%2").arg(!fileName.isEmpty() ? fileName : "unknown",
                                             context.line ? QString::number(context.line) : "0");
    }
#else
    // In release mode, include file and line info only for QML category which will always
    // be available and provide a link to the source code in QtCreator.
    if (isQml) {
        fileLineInfo = QString("[%1:%2]").arg(context.file ? context.file : "unknown",
                                              context.line ? QString::number(context.line) : "0");
    }
#endif

    const auto fmtMsg = QString("[%1][%2][%3]:%4 %5").arg(ts, fmt[type].c_str(), tid, fileLineInfo, localMsg.constData());

    (*QT_DEFAULT_MESSAGE_HANDLER)(type, context, fmtMsg);
}

static QString
getRenderInterfaceString()
{
    using GAPI = QSGRendererInterface::GraphicsApi;
    switch (QQuickWindow::graphicsApi()) {
    case GAPI::Direct3D11Rhi:
        return "Direct3D11Rhi";
    case GAPI::MetalRhi:
        return "MetalRhi";
    case GAPI::OpenGLRhi:
        return "OpenGLRhi";
    case GAPI::VulkanRhi:
        return "VulkanRhi";
    default:
        break;
    }
    return {};
}

void
ScreenInfo::setCurrentFocusWindow(QWindow* window)
{
    if (window && !currentFocusWindow_) {
        currentFocusWindow_ = window;
        set_devicePixelRatio(currentFocusWindow_->screen()->devicePixelRatio());

        QObject::connect(currentFocusWindow_,
                         &QWindow::screenChanged,
                         this,
                         &ScreenInfo::onScreenChanged,
                         Qt::UniqueConnection);
    }
}

void
ScreenInfo::onScreenChanged()
{
    currentFocusWindowScreen_ = currentFocusWindow_->screen();
    set_devicePixelRatio(currentFocusWindowScreen_->devicePixelRatio());

    QObject::connect(currentFocusWindowScreen_,
                     &QScreen::physicalDotsPerInchChanged,
                     this,
                     &ScreenInfo::onPhysicalDotsPerInchChanged,
                     Qt::UniqueConnection);
}

void
ScreenInfo::onPhysicalDotsPerInchChanged()
{
    set_devicePixelRatio(currentFocusWindowScreen_->devicePixelRatio());
}

MainApplication::MainApplication(int& argc, char** argv)
    : QApplication(argc, argv)
{
    const char* qtVersion = qVersion();

    parseArguments();

    // Adjust the log levels as needed (as logging categories are added).
    // Note: the following will cause detailed Qt logging and effectively spam the console
    // without using `qt.*=false`. It may be useful for debugging Qt/QtQuick issues.
    QLoggingCategory::setFilterRules("\n"
                                     "*.debug=true\n"
                                     "libclient.debug=true\n"
                                     "qt.*=false\n"
                                     "qml.debug=false\n"
                                     "default.debug=true\n"
                                     "client.debug=true\n"
                                     "spellcheck.debug=false\n"
                                     "\n");
    // These can be set in the environment as well.
    // e.g. QT_LOGGING_RULES="*.debug=false;qml.debug=true"

    // Tab align the log messages.
    qSetMessagePattern("%{category}\t%{message}");

    // Registration is done late here contrary to suggested practice in order to
    // allow for the arguments to be parsed first in case we want to influence
    // the logging features.
    qInstallMessageHandler(messageHandler);

    C_INFO << "Using Qt runtime version:" << qtVersion;
}

MainApplication::~MainApplication()
{
    // Tear the system tray down first. During shutdown, releasing certain
    // Windows COM objects (e.g. the network list manager owned by
    // ConnectivityMonitor) pumps a nested modal message loop that can dispatch
    // a still-pending tray context-menu request. Popping up the menu while the
    // application is being destroyed dereferences an invalid widget/window and
    // crashes (NULL_POINTER_READ in Qt6Widgets). Hide the icon and detach its
    // (parentless) context menu up front so no menu can be shown during teardown.
    if (systemTray_) {
        systemTray_->hide();
        QMenu* menu = systemTray_->contextMenu();
        systemTray_->setContextMenu(nullptr);
        delete menu;
    }

    engine_.reset();
    lrcInstance_.reset();

    // Allow the crash reporter to do implementation-specific cleanup before the application exits.
    delete crashReporter_;
}

bool
MainApplication::init()
{
    // Let's make sure we can provide postmortem debugging information prior
    // to any other initialization. This won't do anything if crashpad isn't
    // enabled.
    settingsManager_ = new AppSettingsManager(this);
    crashReporter_ = new CrashReporter(settingsManager_, this);
    accountSettingsManager_ = new AccountSettingsManager(this);

    // This 2-phase initialisation prevents ephemeral instances from
    // performing unnecessary tasks, like initializing the WebEngine.
    engine_.reset(new QQmlApplicationEngine(this));

#if WITH_WEBENGINE
    // Configure the single shared default WebEngine profile exactly once, here,
    // instead of letting each WebEngineView mutate it from its own
    // Component.onCompleted. In --single-process mode Chromium supports only a
    // single profile, so every view (map, emoji picker, media/video previews,
    // general views) must share the default one. Centralising the setup keeps a
    // consistent configuration regardless of which view is created first, and
    // guarantees the map always gets the descriptive User-Agent required by the
    // OpenStreetMap tile usage policy even when it is the first view opened.
    if (auto* profile = QWebEngineProfile::defaultProfile()) {
        QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
        dataDir.cdUp();
        const QString dataPath = dataDir.absolutePath() + "/jami";
        profile->setCachePath(dataPath);
        profile->setPersistentStoragePath(dataPath);
        profile->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
        profile->setHttpCacheType(QWebEngineProfile::NoCache);
        profile->setHttpUserAgent(QStringLiteral("JamiDesktop/") + QCoreApplication::applicationVersion());
    }
#endif

    QWK::registerTypes(engine_.get());

    connectivityMonitor_ = new ConnectivityMonitor(this);
    systemTray_ = new SystemTray(settingsManager_, this);
    previewEngine_ = new PreviewEngine(connectivityMonitor_, this);

    // These should should be QueuedConnection to ensure that the
    // they are executed after the QML engine has been initialized,
    // and after the QSystemTrayIcon has been created and shown.
    QObject::connect(settingsManager_,
                     &AppSettingsManager::retranslate,
                     engine_.get(),
                     &QQmlApplicationEngine::retranslate,
                     Qt::QueuedConnection);
    QObject::connect(settingsManager_,
                     &AppSettingsManager::retranslate,
                     this,
                     &MainApplication::initSystray,
                     Qt::QueuedConnection);

#ifndef Q_OS_MACOS
    setWindowIcon(QIcon(":/images/jami.ico"));
#endif

    Utils::removeOldVersions();
    qputenv("JAMI_LANG", settingsManager_->getLanguage().toUtf8());
    settingsManager_->loadTranslations();
    setApplicationFont();

    initLrc(runOptions_[Option::UpdateUrl].toString(),
            connectivityMonitor_,
            runOptions_[Option::Debug].toBool(),
            runOptions_[Option::MuteDaemon].toBool());

#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
    using namespace Interfaces;
    GlobalInstances::setDBusErrorHandler(std::make_unique<DBusErrorHandler>());
    auto dBusErrorHandlerQObject = dynamic_cast<QObject*>(&GlobalInstances::dBusErrorHandler());
    QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, DBusErrorHandler, dBusErrorHandlerQObject);
    if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) {
        engine_->load(QUrl(QStringLiteral("qrc:/DaemonReconnectWindow.qml")));
        exec();

        if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) {
            qWarning() << "Can't connect to the daemon via D-Bus.";
            return false;
        } else {
            engine_.reset(new QQmlApplicationEngine());
        }
    }
#endif

    connect(connectivityMonitor_, &ConnectivityMonitor::connectivityChanged, this, [this] {
        QTimer::singleShot(500, this, [&]() { lrcInstance_->connectivityChanged(); });
    });

    connect(this, &QGuiApplication::focusWindowChanged, [this] {
        screenInfo_.setCurrentFocusWindow(this->focusWindow());
    });

    auto downloadPath = settingsManager_->getValue(Settings::Key::DownloadPath);
    auto screenshotPath = settingsManager_->getValue(Settings::Key::ScreenshotPath);
    auto allowTransferFromTrusted = settingsManager_->getValue(Settings::Key::AutoAcceptFiles).toBool();
    auto acceptTransferBelow = settingsManager_->getValue(Settings::Key::AcceptTransferBelow).toInt();
    lrcInstance_->accountModel().downloadDirectory = downloadPath.toString() + "/";
    lrcInstance_->accountModel().screenshotDirectory = screenshotPath.toString();
    lrcInstance_->accountModel().autoTransferFromTrusted = allowTransferFromTrusted;
    lrcInstance_->accountModel().autoTransferSizeThreshold = acceptTransferBelow;

    // A start URI overrides --minimized (window must be visible to handle the URI).
    set_startMinimized(runOptions_[Option::StartMinimized].toBool() && runOptions_[Option::StartUri].isNull());

    initQmlLayer();

    // Start the local API server if enabled.
    if (settingsManager_->getValue(Settings::Key::EnableApi).toBool()) {
        auto apiPort = settingsManager_->getValue(Settings::Key::ApiPort).value<quint16>();
        if (apiServer_->start(apiPort)) {
            C_INFO << "API server started on port" << apiServer_->port();
        }
    }

    accountSettingsManager_->initalizeAccountSettings();

    initSystray();

    return true;
}

void
MainApplication::restoreApp()
{
    Q_EMIT lrcInstance_->restoreAppRequested();
}

void
MainApplication::handleUriAction(const QString& arg)
{
    QString uri {};
    if (arg.isEmpty() && !runOptions_[Option::StartUri].isNull()) {
        uri = runOptions_[Option::StartUri].toString();
        C_DBG << "URI action invoked by run option" << uri;
        Q_EMIT searchAndSelect(uri.replace("jami:", ""));
    } else if (!arg.isEmpty()) {
        uri = arg;
        C_DBG << "URI action invoked by secondary instance" << uri;
        Q_EMIT searchAndSelect(uri.replace("jami:", ""));
    }
}

void
MainApplication::initLrc(const QString& downloadUrl, ConnectivityMonitor* cm, bool debugMode, bool muteDaemon)
{
    lrcInstance_.reset(new LRCInstance(downloadUrl, cm, debugMode, muteDaemon));
    lrcInstance_->subscribeToDebugReceived();
}

void
MainApplication::parseArguments()
{
    // See if the app is being started with a URI.
    for (const auto& arg : QApplication::arguments()) {
        if (arg.startsWith("jami:")) {
            runOptions_[Option::StartUri] = arg;
        }
    }

    parser_.addHelpOption();
    parser_.addVersionOption();

    QCommandLineOption webDebugOption(QStringList() << "remote-debugging-port", "Web debugging port.", "port");
    parser_.addOption(webDebugOption);

    QCommandLineOption minimizedOption({"m", "minimized"}, "Start minimized.");
    parser_.addOption(minimizedOption);

    QCommandLineOption debugOption({"d", "debug"}, "Debug out.");
    parser_.addOption(debugOption);

    QCommandLineOption logFileOption({"f", "file"}, "Debug to <file>.", "file");
    parser_.addOption(logFileOption);

#ifdef Q_OS_WINDOWS
    QCommandLineOption updateUrlOption({"u", "url"}, "<url> for debugging version queries.", "url");
    parser_.addOption(updateUrlOption);

#endif
    QCommandLineOption terminateOption({"t", "term"}, "Terminate all instances.");
    parser_.addOption(terminateOption);

    QCommandLineOption muteDaemonOption({"q", "quiet"}, "Mute daemon logging. (only if debug)");
    parser_.addOption(muteDaemonOption);

#ifdef QT_DEBUG
    //  In debug mode, add an option to test a specific QML component via its name.
    //  e.g. ./jami --test AccountComboBox
    parser_.addOption(QCommandLineOption("test", "Test a QML component via its name.", "uri"));
    // We may need to force the test window dimensions in the case that the component to test
    // does not specify its own dimensions and is dependent on parent/sibling dimensions.
    // e.g. ./jami --test AccountComboBox -w 200
    parser_.addOption(QCommandLineOption("width", "Width for the test window.", "width"));
    parser_.addOption(QCommandLineOption("height", "Height for the test window.", "height"));
#endif

    parser_.process(*this);

    runOptions_[Option::StartMinimized] = parser_.isSet(minimizedOption);
    runOptions_[Option::Debug] = parser_.isSet(debugOption);
    if (parser_.isSet(logFileOption)) {
        auto logFileValue = parser_.value(logFileOption);
        auto logFile = logFileValue.isEmpty() ? Utils::getDebugFilePath() : logFileValue;
        qputenv("JAMI_LOG_FILE", logFile.toStdString().c_str());
    }
#ifdef Q_OS_WINDOWS
    runOptions_[Option::UpdateUrl] = parser_.value(updateUrlOption);
#endif
    runOptions_[Option::TerminationRequested] = parser_.isSet(terminateOption);
    runOptions_[Option::MuteDaemon] = parser_.isSet(muteDaemonOption);
}

void
MainApplication::setApplicationFont()
{
    QStringList fontFamilies {"Ubuntu"};
#ifdef Q_OS_LINUX
    QFontDatabase::addApplicationFont(":/fonts/NotoColorEmoji.ttf");
    fontFamilies += "NotoColorEmoji";
#endif

    QFontDatabase::addApplicationFont(":/fonts/UbuntuMono-Regular.ttf");
    QFont font;
    font.setFamilies(fontFamilies);
    setFont(font);
}

QString
findResource(const QString& targetBasename, const QString& basePath = ":/")
{
    QDir dir(basePath);
    // List all entries in the directory excluding special entries '.' and '..'
    QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDir::DirsFirst);

    Q_FOREACH (const QString& entry, entries) {
        QString fullPath = basePath + "/" + entry;
        QFileInfo fileInfo(fullPath);

        if (fileInfo.isDir()) {
            // Recursively search in subdirectories
            QString found = findResource(targetBasename, fullPath);
            if (!found.isEmpty()) {
                return found; // Return the first match found in any subdirectory
            }
        } else if (fileInfo.isFile() && fileInfo.fileName().contains(targetBasename, Qt::CaseInsensitive)) {
            // Match found, return the full path but remove the leading ":/".
            return fileInfo.absoluteFilePath().mid(2);
        }
    }

    // No match found in this directory or its subdirectories
    return QString();
}

void
MainApplication::initQmlLayer()
{
    // Expose custom types to the QML engine.
    Utils::registerTypes(engine_.get(),
                         lrcInstance_.get(),
                         systemTray_,
                         settingsManager_,
                         accountSettingsManager_,
                         connectivityMonitor_,
                         previewEngine_,
                         &screenInfo_,
                         this);

    // Register the crash reporter as a context property in the QML engine.
    engine_->rootContext()->setContextProperty("crashReporter", crashReporter_);

    // Expose API token manager and list model to QML.
    apiTokenManager_ = new ApiTokenManager(this);
    qApp->setProperty("ApiTokenManager", QVariant::fromValue(static_cast<QObject*>(apiTokenManager_)));
    apiTokenListModel_ = new ApiTokenListModel(apiTokenManager_, this);
    engine_->rootContext()->setContextProperty("ApiTokenListModel", apiTokenListModel_);

    // Always create the API server so it can be controlled from QML settings.
    apiServer_ = new ApiServer(lrcInstance_.get(), this);
    apiServer_->setTokenManager(apiTokenManager_);
    engine_->rootContext()->setContextProperty("ApiServer", apiServer_);

    QUrl url = QStringLiteral("qrc:/MainApplicationWindow.qml");
#ifdef QT_DEBUG
    if (parser_.isSet("test")) {
        // List the QML files in the project source tree.
        const auto targetTestComponent = findResource(parser_.value("test"));
        if (targetTestComponent.isEmpty()) {
            C_FATAL << "Failed to find QML component:" << parser_.value("test");
        }
        engine_->rootContext()->setContextProperty("testComponentURI", targetTestComponent);
        // Log the width and height values for the test window.
        const auto testWidth = parser_.isSet("width") ? parser_.value("width").toInt() : 0;
        const auto testHeight = parser_.isSet("height") ? parser_.value("height").toInt() : 0;
        engine_->rootContext()->setContextProperty("testWidth", testWidth);
        engine_->rootContext()->setContextProperty("testHeight", testHeight);
        url = QStringLiteral("qrc:/ComponentTestWindow.qml");
    }
#endif
    QObject::connect(
        engine_.get(),
        &QQmlApplicationEngine::objectCreationFailed,
        this,
        [url]() { C_FATAL << "Failed to load QML component:" << url; },
        Qt::QueuedConnection);
    engine_->load(url);

    // Report the render interface used.
    C_INFO << "Main window loaded using" << getRenderInterfaceString();
}

void
MainApplication::initSystray()
{
    systemTray_->setIcon(QIcon(":/images/net.jami.Jami.svg"));

    QMenu* menu {nullptr};
    // If there was a previous menu, reuse it, otherwise create a new one.
    if ((menu = systemTray_->contextMenu())) {
        menu->clear();
    } else {
        menu = new QMenu;
    }

    QString quitString;
#ifdef Q_OS_WINDOWS
    quitString = tr("E&xit");
#else
    quitString = tr("&Quit");
#endif

    QAction* quitAction = new QAction(quitString, this);
    connect(quitAction, &QAction::triggered, this, &MainApplication::closeRequested);

    QAction* restoreAction = new QAction(tr("&Show Jami"), this);
    connect(restoreAction, &QAction::triggered, this, &MainApplication::restoreApp);

    connect(systemTray_, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
        if (reason != QSystemTrayIcon::ActivationReason::Context) {
#ifdef Q_OS_WINDOWS
            restoreApp();
#elif !defined(Q_OS_MACOS)
                    QWindow* window = focusWindow();
                    if (window)
                        window->close();
                    else
                        restoreApp();
#endif
        }
    });

    menu->addAction(restoreAction);
    menu->addAction(quitAction);
    systemTray_->setContextMenu(menu);

    systemTray_->show();
}

void
MainApplication::setEventFilter()
{
    installEventFilter(this);
}

void
MainApplication::startSystemMove(QWindow* window)
{
#ifdef Q_OS_MACOS
    macutils::startSystemMove(window);
#endif
}

void
MainApplication::setToolBar(QWindow* window)
{
#ifdef Q_OS_MACOS
    macutils::setToolBar(window);
#endif
}

void
MainApplication::setupPipWindow(QWindow* window)
{
#ifdef Q_OS_MACOS
    macutils::setupPipWindow(window);
#endif
}
