// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
// SPDX-License-Identifier: GPL-3.0-or-later

#include "applicationsingleton.h"

#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QLocalServer>
#include <QLocalSocket>
#include <QLoggingCategory>
#include <QScopedPointer>
#include <QSharedMemory>

#ifdef Q_OS_UNIX
#include <errno.h>
#include <signal.h>
#endif

namespace Zeal::Core {

namespace {
Q_LOGGING_CATEGORY(log, "zeal.core.applicationsingleton")

struct SharedData
{
    qint64 primaryPid;
};
} // namespace

ApplicationSingleton::ApplicationSingleton(QObject *parent)
    : QObject(parent)
{
    if (QCoreApplication::instance() == nullptr) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
        qCFatal(log, "QCoreApplication (or derived type) must be created before ApplicationSingleton.");
#else
        qFatal("QCoreApplication (or derived type) must be created before ApplicationSingleton.");
#endif
    }

    m_id = computeId();
    qCDebug(log, "Singleton ID: %s", qPrintable(m_id));

    m_sharedMemory = new QSharedMemory(m_id, this);

    if (m_sharedMemory->create(sizeof(SharedData))) {
        setupPrimary();
        return;
    }

#ifdef Q_OS_UNIX
    // Check if the shared memory segment belongs to a running instance.
    // NOTE: There is a small race window where the primary could exit between
    // the kill() check and the subsequent sendMessage() call in the secondary
    // path. This is unlikely in practice and not currently handled.
    if (m_sharedMemory->attach(QSharedMemory::ReadOnly)) {
        qint64 stalePid = 0;
        if (m_sharedMemory->lock()) {
            auto sd = static_cast<const SharedData *>(m_sharedMemory->constData());
            stalePid = sd ? sd->primaryPid : 0;
            m_sharedMemory->unlock();
        } else {
            qCWarning(log) << "Cannot lock shared memory for PID probe:" << m_sharedMemory->errorString();
        }

        // kill() with EPERM means the process exists but we lack permission to signal it.
        errno = 0;
        const bool isAlive = (stalePid > 0 && (kill(static_cast<pid_t>(stalePid), 0) == 0 || errno == EPERM));

        if (isAlive) {
            setupSecondary();
            return;
        }

        m_sharedMemory->detach();

        qCInfo(log, "Detected stale shared memory from PID %lld, reclaiming.", stalePid);

        // On some platforms (e.g., Termux) Qt's POSIX shared memory backend
        // does not remove the backing file on detach. Remove it explicitly.
        // The native key is a POSIX shm name (e.g., /qipc_sharedmemory_...).
        // On Termux, the backing file is in QDir::tempPath() + nativeKey.
        const QString nativeKey = m_sharedMemory->nativeKey();
        if (!nativeKey.isEmpty()) {
            // Check existence first to avoid a spurious warning on platforms
            // where the backing file is in /dev/shm (not tempPath).
            const QString shmFile = QDir::tempPath() + nativeKey;
            if (QFile::exists(shmFile)) {
                if (!QFile::remove(shmFile)) {
                    qCWarning(log) << "Cannot remove stale shared memory file:" << shmFile;
                }
            }
        }

        if (m_sharedMemory->create(sizeof(SharedData))) {
            setupPrimary();
            return;
        }
    }
#endif

    // If both create() and attach() failed, the shared memory segment is in
    // a broken state (e.g., stale backing file after a crash). Clean up and
    // try to become primary one last time.
    if (!m_sharedMemory->attach(QSharedMemory::ReadOnly)) {
        qCWarning(log) << "Cannot attach to the shared memory segment:" << m_sharedMemory->errorString();

        if (m_sharedMemory->create(sizeof(SharedData))) {
            setupPrimary();
            return;
        }

        qCWarning(log) << "Cannot create shared memory segment:" << m_sharedMemory->errorString();
        return;
    }

    setupSecondary();
}

ApplicationSingleton::State ApplicationSingleton::state() const
{
    return m_state;
}

qint64 ApplicationSingleton::primaryPid() const
{
    return m_primaryPid;
}

bool ApplicationSingleton::sendMessage(QByteArray &data, int timeout)
{
    // No support for primary to secondary communication.
    if (m_state != State::Secondary) {
        return false;
    }

    QScopedPointer<QLocalSocket, QScopedPointerDeleteLater> socket(new QLocalSocket);
    socket->connectToServer(m_id);
    if (!socket->waitForConnected(timeout)) {
        qCWarning(log) << "Cannot connect to the local service:" << socket->errorString();
        return false;
    }

    return socket->write(data) == data.size() && socket->flush();
}

void ApplicationSingleton::setupPrimary()
{
    m_state = State::Primary;
    m_primaryPid = QCoreApplication::applicationPid();

    qCInfo(log, "Starting as a primary instance. (PID: %lld)", m_primaryPid);

    m_sharedMemory->lock();
    auto *sd = static_cast<SharedData *>(m_sharedMemory->data());
    sd->primaryPid = m_primaryPid;
    m_sharedMemory->unlock();

    QLocalServer::removeServer(m_id);

    m_localServer = new QLocalServer(this);
    m_localServer->setSocketOptions(QLocalServer::UserAccessOption);

    connect(m_localServer, &QLocalServer::newConnection, this, [this] {
        QLocalSocket *socket = m_localServer->nextPendingConnection();
        connect(socket, &QLocalSocket::readyRead, this, [this, socket] {
            QByteArray data = socket->readAll();
            emit messageReceived(data);
            socket->deleteLater();
        });
    });

    if (!m_localServer->listen(m_id)) {
        qCWarning(log) << "Cannot start the local service:" << m_localServer->errorString();
        return;
    }
}

void ApplicationSingleton::setupSecondary()
{
    m_state = State::Secondary;

    m_sharedMemory->lock();
    auto *sd = static_cast<SharedData *>(m_sharedMemory->data());
    m_primaryPid = sd->primaryPid;
    m_sharedMemory->unlock();

    qCInfo(log, "Starting as a secondary instance. (Primary PID: %lld)", m_primaryPid);
}

QString ApplicationSingleton::computeId()
{
    // Make sure the result can be used as a name for the local socket.
    static const QByteArray::Base64Options base64Options = QByteArray::Base64UrlEncoding
                                                         | QByteArray::OmitTrailingEquals;

    QCryptographicHash hash(QCryptographicHash::Sha256);
    hash.addData(QCoreApplication::applicationName().toUtf8());
    hash.addData(QCoreApplication::organizationName().toUtf8());
    hash.addData(QCoreApplication::organizationDomain().toUtf8());
    // Support multi-user setup.
    hash.addData(QDir::homePath().toUtf8());

    return QString::fromLatin1(hash.result().toBase64(base64Options));
}

} // namespace Zeal::Core
