// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "err_notifier_service.h"

#include <qlatin1stringview.h>

extern "C" {
#include <sys/pidfd.h>

#include <unistd.h>
}

#include <DDBusInterface>

#include <QByteArray>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusError>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusReply>
#include <QDBusUnixFileDescriptor>
#include <QDBusVariant>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QLocale>
#include <QLoggingCategory>
#include <QMetaType>
#include <QProcess>
#include <QSaveFile>
#include <QStandardPaths>
#include <QStringList>
#include <QUrl>

#include <cstring>

using DTK_CORE_NAMESPACE::DDBusInterface;

Q_LOGGING_CATEGORY(lc_err_notifier, "deepin.immutable.err-notifier")

// Type definition for ApplicationManager1 Identify return type
using ObjectInterfaceMap = QMap<QString, QVariantMap>;
Q_DECLARE_METATYPE(ObjectInterfaceMap)

namespace {

const QString USER_SERVICE_NAME = QStringLiteral("org.deepin.immutable.ErrNotifier1");
const QString USER_OBJECT_PATH = QStringLiteral("/org/deepin/immutable/ErrNotifier1");
const QString USER_INTERFACE_NAME = QStringLiteral("org.deepin.immutable.ErrNotifier1");

const QString SYSTEM_SERVICE_NAME = QStringLiteral("org.deepin.immutable.ReadOnlyAuditor1");
const QString SYSTEM_OBJECT_PATH = QStringLiteral("/org/deepin/immutable/ReadOnlyAuditor1");
const QString SYSTEM_INTERFACE_NAME = QStringLiteral("org.deepin.immutable.ReadOnlyAuditor1");

const QString NOTIFICATION_SERVICE = QStringLiteral("org.freedesktop.Notifications");
const QString NOTIFICATION_PATH = QStringLiteral("/org/freedesktop/Notifications");
const QString NOTIFICATION_INTERFACE = QStringLiteral("org.freedesktop.Notifications");

const QString PORTAL_SERVICE = QStringLiteral("org.freedesktop.portal.Desktop");
const QString PORTAL_PATH = QStringLiteral("/org/freedesktop/portal/desktop");
const QString PORTAL_FILE_CHOOSER_INTERFACE = QStringLiteral("org.freedesktop.portal.FileChooser");
const QString PORTAL_REQUEST_INTERFACE = QStringLiteral("org.freedesktop.portal.Request");

const QString APP_MANAGER_SERVICE = QStringLiteral("org.desktopspec.ApplicationManager1");
const QString APP_MANAGER_PATH = QStringLiteral("/org/desktopspec/ApplicationManager1");
const QString APP_MANAGER_INTERFACE = QStringLiteral("org.desktopspec.ApplicationManager1");
const QString APP_MANAGER_APPLICATION_INTERFACE =
        QStringLiteral("org.desktopspec.ApplicationManager1.Application");

const QString DBUS_PROPERTIES_INTERFACE = QStringLiteral("org.freedesktop.DBus.Properties");
const QString DBUS_PROPERTIES_CHANGED_SIGNAL = QStringLiteral("PropertiesChanged");

const QString DCONFIG_APP_ID = QStringLiteral("org.deepin.immutable.eventd");
const QString DCONFIG_RESOURCE_ID = QStringLiteral("org.deepin.immutable.err-notifier");

const QString CONTROL_CENTER_DCONFIG_APP_ID = QStringLiteral("org.deepin.dde.control-center");
const QString CONTROL_CENTER_DCONFIG_RESOURCE_ID =
        QStringLiteral("org.deepin.dde.control-center.commoninfo");

const QString NOTIFICATION_ENABLED_KEY = QStringLiteral("notificationEnabled");
const QString NOTIFICATION_ENABLED_PROPERTY = QStringLiteral("NotificationEnabled");

} // namespace

ErrNotifierService::ErrNotifierService(const QDBusConnection &connection, QObject *parent)
    : QObject(parent)
    , m_connection(connection)
{
    m_config = DConfig::create(DCONFIG_APP_ID, DCONFIG_RESOURCE_ID, "", this);
    if (!m_config->isValid()) {
        qWarning(lc_err_notifier) << "DConfig is invalid, using default notification settings";
        m_notification_enabled = false;
    } else {
        m_notification_enabled = m_config->value(NOTIFICATION_ENABLED_KEY, false).toBool();
        // Connect to valueChanged signal to monitor configuration changes
        connect(m_config,
                &DConfig::valueChanged,
                this,
                &ErrNotifierService::handleConfigValueChanged);
    }

    loadShowReadOnlyProtectionConfig();
    initErrorSignalSubscription();
}

ErrNotifierService::~ErrNotifierService()
{
    cleanupPortalRequest();
    cleanupCachedLogFd();

    if (m_service_registered) {
        m_connection.unregisterService(USER_SERVICE_NAME);
        m_service_registered = false;
    }

    if (m_object_registered) {
        m_connection.unregisterObject(USER_OBJECT_PATH);
        m_object_registered = false;
    }
}

bool ErrNotifierService::initialize()
{
    if (!m_connection.isConnected()) {
        qWarning(lc_err_notifier) << "DBus connection unavailable for user plugin";
        return false;
    }

    // Create and attach the D-Bus adaptor
    new ErrNotifierAdaptor(this);

    // Register object - adaptors are automatically exported
    if (!m_connection.registerObject(USER_OBJECT_PATH, this)) {
        qWarning(lc_err_notifier) << "Failed to register DBus object" << USER_OBJECT_PATH;
        return false;
    }
    m_object_registered = true;

    if (!m_connection.registerService(USER_SERVICE_NAME)) {
        const QDBusError err = m_connection.lastError();
        qWarning(lc_err_notifier) << "Failed to register service" << USER_SERVICE_NAME
                                  << err.message();
        m_connection.unregisterObject(USER_OBJECT_PATH);
        m_object_registered = false;
        return false;
    }
    m_service_registered = true;

    qInfo(lc_err_notifier) << "Notification" << (m_notification_enabled ? "enabled" : "disabled");

    return true;
}

bool ErrNotifierService::notificationEnabled() const
{
    return m_notification_enabled;
}

void ErrNotifierService::setNotificationEnabled(bool enabled)
{
    if (m_notification_enabled == enabled) {
        return;
    }

    m_notification_enabled = enabled;
    m_config->setValue(NOTIFICATION_ENABLED_KEY, enabled);
    qInfo(lc_err_notifier) << "Notification" << (enabled ? "enabled" : "disabled");
    emitPropertiesChanged(NOTIFICATION_ENABLED_PROPERTY, enabled);
}

void ErrNotifierService::emitPropertiesChanged(const QString &propertyName, const QVariant &value)
{
    QVariantMap changedProperties;
    changedProperties.insert(propertyName, value);
    QStringList invalidatedProperties;

    QDBusMessage signal = QDBusMessage::createSignal(USER_OBJECT_PATH,
                                                     DBUS_PROPERTIES_INTERFACE,
                                                     DBUS_PROPERTIES_CHANGED_SIGNAL);
    signal << USER_INTERFACE_NAME << changedProperties << invalidatedProperties;

    if (!m_connection.send(signal)) {
        qWarning(lc_err_notifier) << "Failed to emit PropertiesChanged signal for" << propertyName;
    }
}

ExportLogError ErrNotifierService::doExportLog()
{
    if (m_export_in_progress) {
        qWarning(lc_err_notifier) << "Export operation already in progress";
        return ExportLogError::InProgress;
    }

    m_export_in_progress = true;

    // First, request log from system service (triggers polkit authentication)
    ExportLogError requestError = requestLogFromSystemService();
    if (requestError != ExportLogError::None) {
        qWarning(lc_err_notifier) << "Failed to request log from system service";
        m_export_in_progress = false;
        return requestError;
    }

    // Authentication succeeded, now open file selection dialog
    openExportDialog();
    return ExportLogError::None;
}

void ErrNotifierService::handleErrorOccurred(const QVariantMap &payload)
{
    // Check if notifications are enabled
    if (!m_notification_enabled) {
        qDebug(lc_err_notifier) << "Notification disabled, skipping error notification";
        return;
    }

    QString label = applicationLabelFromPayload(payload);
    QString body = tr("The %1 app is attempting to modify the system's read-only directory "
                      "and has been blocked by the immutable system.")
                           .arg(label);

    // Add the second sentence only if showReadOnlyProtection is true
    if (m_show_readonly_protection) {
        body += QLatin1Char(' ')
                + tr("To allow modifications, go to Control Center - System - Developer "
                     "Options and disable read-only protection");
    }

    showNotification(tr("Immutable System"), body, 5000, true);
}

void ErrNotifierService::initErrorSignalSubscription()
{
    QDBusConnection system_bus = QDBusConnection::systemBus();
    if (!system_bus.isConnected()) {
        qWarning(lc_err_notifier) << "Failed to connect to system bus for signals";
        return;
    }

    const bool ok = system_bus.connect(SYSTEM_SERVICE_NAME,
                                       SYSTEM_OBJECT_PATH,
                                       SYSTEM_INTERFACE_NAME,
                                       QStringLiteral("ErrorOccurred"),
                                       this,
                                       SLOT(handleErrorOccurred(QVariantMap)));

    if (!ok) {
        qWarning(lc_err_notifier) << "Failed to subscribe ErrorOccurred signal";
    }
}

void ErrNotifierService::loadShowReadOnlyProtectionConfig()
{
    // Initialize control center config to get showReadOnlyProtection
    DConfig *control_center_config = DConfig::create(CONTROL_CENTER_DCONFIG_APP_ID,
                                                     CONTROL_CENTER_DCONFIG_RESOURCE_ID,
                                                     "",
                                                     nullptr);
    if (!control_center_config->isValid()) {
        qWarning(lc_err_notifier) << "Control center DConfig is invalid, using default value, "
                                     "showReadOnlyProtection = false";
        m_show_readonly_protection = false;
    } else {
        m_show_readonly_protection =
                control_center_config->value(QStringLiteral("showReadOnlyProtection"), true)
                        .toBool();
        qDebug(lc_err_notifier) << "showReadOnlyProtection:" << m_show_readonly_protection;
    }
    // Destroy control center config after reading the value
    delete control_center_config;
}

void ErrNotifierService::handleConfigValueChanged(const QString &key)
{
    if (key != NOTIFICATION_ENABLED_KEY) {
        return;
    }
    if (!m_config || !m_config->isValid()) {
        qWarning(lc_err_notifier) << "DConfig invalid when handling value change for key:" << key;
        return;
    }
    const bool enabled = m_config->value(key, false).toBool();
    if (m_notification_enabled == enabled) {
        return;
    }
    m_notification_enabled = enabled;
    qInfo(lc_err_notifier) << "Configuration changed: Notification"
                           << (enabled ? "enabled" : "disabled");
    emitPropertiesChanged(NOTIFICATION_ENABLED_PROPERTY, enabled);
}

ExportLogError ErrNotifierService::requestLogFromSystemService()
{
    // Clean up any existing cached fd first
    cleanupCachedLogFd();

    QDBusInterface interface(SYSTEM_SERVICE_NAME,
                             SYSTEM_OBJECT_PATH,
                             SYSTEM_INTERFACE_NAME,
                             QDBusConnection::systemBus());
    if (!interface.isValid()) {
        qWarning(lc_err_notifier) << "Failed to connect to system read-only auditor service:"
                                  << interface.lastError().message();
        return ExportLogError::SystemServiceUnavailable;
    }

    QDBusReply<QDBusUnixFileDescriptor> reply = interface.call(QLatin1String("ExportLog"));
    if (!reply.isValid()) {
        const QDBusError error = reply.error();
        qWarning(lc_err_notifier) << "ExportLog call failed:" << error.name() << error.message();

        // Check if it's an authentication/authorization error
        if (error.type() == QDBusError::AccessDenied
            || error.name().contains("NotAuthorized", Qt::CaseInsensitive)
            || error.name().contains("AuthFailed", Qt::CaseInsensitive)) {
            return ExportLogError::AuthenticationFailed;
        }

        return ExportLogError::SystemServiceUnavailable;
    }

    const QDBusUnixFileDescriptor descriptor = reply.value();
    if (!descriptor.isValid()) {
        qWarning(lc_err_notifier) << "Log file descriptor not received";
        return ExportLogError::SystemServiceUnavailable;
    }

    // Duplicate the file descriptor to keep it alive
    m_cached_log_fd = ::dup(descriptor.fileDescriptor());
    if (m_cached_log_fd == -1) {
        qWarning(lc_err_notifier) << "Failed to duplicate file descriptor";
        return ExportLogError::SystemServiceUnavailable;
    }

    qDebug(lc_err_notifier) << "Log file descriptor cached successfully";
    return ExportLogError::None;
}

void ErrNotifierService::cleanupCachedLogFd()
{
    if (m_cached_log_fd >= 0) {
        ::close(m_cached_log_fd);
        m_cached_log_fd = -1;
        qDebug(lc_err_notifier) << "Cached log file descriptor cleaned up";
    }
}

void ErrNotifierService::openExportDialog()
{
    if (!m_portal_request_path.path().isEmpty()) {
        qDebug(lc_err_notifier) << "File selection request already in progress";
        return;
    }

    if (startPortalSaveDialog()) {
        return;
    }

    const QString directory = defaultDirectory();
    const QString fallback_path = QDir(directory).filePath(defaultFileName());
    qWarning(lc_err_notifier) << "Failed to open file chooser, using default path" << fallback_path;
    handleExportPath(fallback_path);
}

bool ErrNotifierService::startPortalSaveDialog()
{
    QDBusInterface portal(PORTAL_SERVICE,
                          PORTAL_PATH,
                          PORTAL_FILE_CHOOSER_INTERFACE,
                          QDBusConnection::sessionBus());
    if (!portal.isValid()) {
        qWarning(lc_err_notifier) << "xdg-desktop-portal unavailable for save dialog";
        return false;
    }

    const QString parent_window;

    const QString title = tr("Export Log");
    QVariantMap options;
    options.insert(QLatin1String("app_id"), USER_SERVICE_NAME);
    options.insert(QLatin1String("accept_label"), QLatin1String("Save"));
    options.insert(QLatin1String("current_name"), defaultFileName());

    // The portal expects current_folder to be a nul-terminated byte array
    // in the same encoding as the file system
    QByteArray folder_bytes = QFile::encodeName(defaultDirectory());
    folder_bytes.append('\0'); // Append nul byte terminator
    options.insert(QLatin1String("current_folder"), QVariant::fromValue(folder_bytes));

    QList<QVariant> args;
    args << parent_window << title << QVariant::fromValue(options);

    QDBusMessage reply =
            portal.callWithArgumentList(QDBus::Block, QStringLiteral("SaveFile"), args);
    if (reply.type() != QDBusMessage::ReplyMessage || reply.arguments().isEmpty()) {
        qWarning(lc_err_notifier) << "Failed to create save dialog request" << reply.errorMessage();
        return false;
    }

    const QDBusObjectPath request_path = qvariant_cast<QDBusObjectPath>(reply.arguments().first());
    if (request_path.path().isEmpty()) {
        qWarning(lc_err_notifier) << "Portal request path is empty";
        return false;
    }

    const bool connected =
            QDBusConnection::sessionBus().connect(PORTAL_SERVICE,
                                                  request_path.path(),
                                                  PORTAL_REQUEST_INTERFACE,
                                                  QStringLiteral("Response"),
                                                  this,
                                                  SLOT(handlePortalResponse(uint, QVariantMap)));
    if (!connected) {
        qWarning(lc_err_notifier) << "Failed to subscribe portal response for"
                                  << request_path.path();
        return false;
    }

    m_portal_request_path = request_path;
    qDebug(lc_err_notifier) << "Portal save dialog request started at" << request_path.path();
    return true;
}

void ErrNotifierService::handlePortalResponse(uint response, const QVariantMap &results)
{
    qDebug(lc_err_notifier) << "Received portal response, code:" << response;

    cleanupPortalRequest();

    if (response != 0) {
        qDebug(lc_err_notifier) << "User cancelled export operation";
        goto cleanup;
    }

    {
        const QVariant uris_var = results.value(QStringLiteral("uris"));
        const QStringList uris = uris_var.toStringList();
        if (uris.isEmpty()) {
            qWarning(lc_err_notifier) << "Portal response missing path information";
            goto cleanup;
        }

        const QUrl url(uris.first());
        QString path;
        if (url.isLocalFile()) {
            path = url.toLocalFile();
        } else if (url.scheme().isEmpty()) {
            path = url.path();
        } else {
            qWarning(lc_err_notifier) << "Unsupported url scheme for portal response" << url;
            goto cleanup;
        }

        if (path.isEmpty()) {
            qWarning(lc_err_notifier) << "Portal response returned empty path";
            goto cleanup;
        }

        qDebug(lc_err_notifier) << "Start handling export path returned by portal:" << path;
        handleExportPath(path);
        return;
    }

cleanup:
    cleanupCachedLogFd();
    m_export_in_progress = false;
}

void ErrNotifierService::cleanupPortalRequest()
{
    if (m_portal_request_path.path().isEmpty()) {
        return;
    }

    QDBusConnection::sessionBus().disconnect(PORTAL_SERVICE,
                                             m_portal_request_path.path(),
                                             PORTAL_REQUEST_INTERFACE,
                                             QStringLiteral("Response"),
                                             this,
                                             SLOT(handlePortalResponse(uint, QVariantMap)));

    m_portal_request_path = QDBusObjectPath();
    qDebug(lc_err_notifier) << "Portal request state cleaned up";
}

void ErrNotifierService::handleExportPath(const QString &path)
{
    if (path.isEmpty()) {
        cleanupCachedLogFd();
        m_export_in_progress = false;
        return;
    }

    const bool ok = exportLogToPath(path);
    m_export_in_progress = false;

    if (ok) {
        showNotification(tr("Immutable System"), tr("Log exported to %1").arg(path));
    }
}

/**
 * @brief Exports the read-only filesystem error log to a specified file path.
 *
 * This function reads the log content from the cached file descriptor (obtained from
 * the system-level read-only auditor service) and copies it to the specified destination
 * path. The target directory will be created automatically if it doesn't exist.
 *
 * Note: The cached file descriptor must be valid before calling this method.
 * It will be cleaned up after successful or failed export.
 *
 * @param path The destination file path where the log should be exported
 * @return true if the log was successfully exported, false otherwise
 */
bool ErrNotifierService::exportLogToPath(const QString &path)
{
    bool success = false;

    if (m_cached_log_fd < 0) {
        qWarning(lc_err_notifier) << "No cached log file descriptor available";
        return false;
    }

    // Reset file position to beginning
    if (::lseek(m_cached_log_fd, 0, SEEK_SET) == -1) {
        qWarning(lc_err_notifier) << "Failed to seek to beginning of log file";
        goto cleanup;
    }

    {
        QFile source;
        if (!source.open(m_cached_log_fd, QIODevice::ReadOnly)) {
            qWarning(lc_err_notifier) << source.errorString();
            goto cleanup;
        }

        QFileInfo file_info(path);
        QDir target_dir = file_info.dir();
        if (!target_dir.exists()) {
            if (!target_dir.mkpath(QLatin1String("."))) {
                qWarning(lc_err_notifier)
                        << "Failed to create target directory:" << target_dir.path();
                goto cleanup;
            }
        }

        QSaveFile target(path);
        if (!target.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
            qWarning(lc_err_notifier) << target.errorString();
            goto cleanup;
        }

        char buffer[64 * 1024];
        while (true) {
            const qint64 read_bytes = source.read(buffer, sizeof(buffer));
            if (read_bytes == 0) {
                break;
            }
            if (read_bytes < 0) {
                qWarning(lc_err_notifier) << source.errorString();
                goto cleanup;
            }
            if (target.write(buffer, read_bytes) != read_bytes) {
                qWarning(lc_err_notifier) << target.errorString();
                goto cleanup;
            }
        }

        if (!target.commit()) {
            qWarning(lc_err_notifier) << target.errorString();
            goto cleanup;
        }
    }

    success = true;

cleanup:
    cleanupCachedLogFd();
    return success;
}

void ErrNotifierService::showNotification(const QString &summary,
                                          const QString &body,
                                          int timeout_ms,
                                          bool add_action)
{
    QDBusInterface notifier(NOTIFICATION_SERVICE,
                            NOTIFICATION_PATH,
                            NOTIFICATION_INTERFACE,
                            QDBusConnection::sessionBus());
    if (!notifier.isValid()) {
        qWarning(lc_err_notifier) << "Notification service unavailable";
        return;
    }

    QStringList actions;
    QVariantMap hints;
    if (add_action) {
        actions << QLatin1String("ExportLog") << tr("Export Log")
                << QLatin1String("DisableNotification")

                << tr("Don't notify");
        hints.insert(QLatin1String("x-deepin-action-ExportLog"),
                     QLatin1String("busctl,--user,call,%1,%2,%3,ExportLog")
                             .arg(USER_SERVICE_NAME)
                             .arg(USER_OBJECT_PATH)
                             .arg(USER_INTERFACE_NAME));
        hints.insert(
                QLatin1String("x-deepin-action-DisableNotification"),
                QLatin1String("busctl,--user,set-property,%1,%2,%3,NotificationEnabled,b,false")
                        .arg(USER_SERVICE_NAME)
                        .arg(USER_OBJECT_PATH)
                        .arg(USER_INTERFACE_NAME));
    }

    QList<QVariant> args;
    args << tr("Control Center") << static_cast<uint>(m_last_notification_id)
         << QLatin1String("preferences-system") << summary << body << QVariant::fromValue(actions)
         << QVariant::fromValue(hints) << timeout_ms;

    QDBusMessage reply = notifier.callWithArgumentList(QDBus::Block, QLatin1String("Notify"), args);

    if (reply.type() == QDBusMessage::ReplyMessage && !reply.arguments().isEmpty()) {
        m_last_notification_id = reply.arguments().first().toUInt();
    }
}

QString ErrNotifierService::applicationLabelFromPayload(const QVariantMap &payload) const
{
    const QString cgroup = variantToString(payload.value(QStringLiteral("cgroup")));
    const quint32 pid = variantToUint(payload.value(QStringLiteral("pid")));
    if (pid > 0) {
        const QString label = identifyApplication(pid, cgroup);
        if (!label.isEmpty()) {
            return label;
        }
    }

    const QString cmdline = variantToString(payload.value(QStringLiteral("cmdline")));
    if (!cmdline.isEmpty()) {
        QStringList parts = cmdline.split(QLatin1Char(' '), Qt::SkipEmptyParts);
        QString candidate = parts.isEmpty() ? cmdline : parts.first();
        const int slash = candidate.lastIndexOf(QLatin1Char('/'));
        if (slash >= 0) {
            candidate = candidate.mid(slash + 1);
        }
        return candidate.trimmed();
    }

    return QString();
}

QString ErrNotifierService::parseAppIdentifierFromCgroup(const QString &cgroup,
                                                         AppIdentifierType &outType) const
{
    if (cgroup.isEmpty()) {
        return QString();
    }

    // Rule 1: Looking for pattern: app-DDE-<appname>@<instance-id>.service
    // Example: app-DDE-deepin\\x2dterminal@5d166934941e40f98659bd1e86529696.service
    const QString prefix = QStringLiteral("app-DDE-");
    const int prefixPos = cgroup.indexOf(prefix);
    if (prefixPos >= 0) {
        // Extract the app name part (from prefix to '@')
        const int startPos = prefixPos + prefix.length();
        const int atPos = cgroup.indexOf(QLatin1Char('@'), startPos);
        if (atPos < 0) {
            qDebug(lc_err_notifier) << "@ separator not found after app-DDE- in cgroup:" << cgroup;
            return QString();
        }

        QString appName = cgroup.mid(startPos, atPos - startPos);
        qDebug(lc_err_notifier) << "Extracted app name from cgroup:" << appName;

        // Convert \xHH to _HH
        // Note: In the cgroup string, \x appears as two characters: backslash and 'x'
        appName.replace(QLatin1String("\\x"), QLatin1String("_"));
        qDebug(lc_err_notifier) << "Converted app name:" << appName;

        // Construct the D-Bus object path
        const QString appPath = APP_MANAGER_PATH + QStringLiteral("/") + appName;
        qDebug(lc_err_notifier) << "Constructed app path from cgroup:" << appPath;

        outType = AppIdentifierType::AM;
        return appPath;
    }

    // Rule 2: systemd service format: /<slice-name>/<service-name>.service
    // Example: /system.slice/disable-deepin-log-viewer.service
    if (cgroup.endsWith(QLatin1String(".service"))) {
        // Find the last '/' to extract service file name
        const int lastSlashPos = cgroup.lastIndexOf(QLatin1Char('/'));
        if (lastSlashPos < 0) {
            qDebug(lc_err_notifier) << "Invalid service cgroup format (no slash):" << cgroup;
            return QString();
        }

        // Extract service file name (after last '/')
        const QString serviceFile = cgroup.mid(lastSlashPos + 1);

        // Remove .service suffix (8 characters: ".service")
        const QString serviceName = serviceFile.left(serviceFile.length() - 8);

        qDebug(lc_err_notifier) << "Extracted service name from cgroup:" << serviceName;

        // Determine service type based on slice prefix
        // User services: /user.slice/*
        // System services: /system.slice/*, /sysbackground.slice/*,
        //                  /syscustom.slice/*, /syssecurity.slice/*, etc.
        if (cgroup.startsWith(QLatin1String("/user.slice/"))) {
            outType = AppIdentifierType::USER_SERVICE;
            qDebug(lc_err_notifier) << "Identified as user service";
        } else {
            // All other slices are considered system services
            outType = AppIdentifierType::SYSTEM_SERVICE;
            qDebug(lc_err_notifier) << "Identified as system service";
        }

        return serviceName;
    }

    // Unrecognized format
    qDebug(lc_err_notifier) << "Unrecognized cgroup format:" << cgroup;
    return QString();
}

QString ErrNotifierService::identifyFromCgroupPath(const QString &cgroup) const
{
    if (cgroup.isEmpty()) {
        return QString();
    }

    qDebug(lc_err_notifier) << "Attempting to identify from cgroup:" << cgroup;

    AppIdentifierType identifierType;
    const QString identifier = parseAppIdentifierFromCgroup(cgroup, identifierType);

    if (identifier.isEmpty()) {
        return QString();
    }

    if (identifierType == AppIdentifierType::AM) {
        // D-Bus path: query ApplicationManager
        const QString localizedName = getLocalizedName(identifier, false);
        if (!localizedName.isEmpty()) {
            return localizedName;
        }
    } else if (identifierType == AppIdentifierType::SYSTEM_SERVICE
               || identifierType == AppIdentifierType::USER_SERVICE) {
        // Try to get package name for the service
        const QString packageName = getPackageNameFromService(identifier, identifierType);
        if (!packageName.isEmpty()) {
            qDebug(lc_err_notifier)
                    << "Using package name for service:" << identifier << "-> " << packageName;
            return packageName;
        }

        // Fall back to service name if package lookup fails
        const QString serviceType =
                (identifierType == AppIdentifierType::USER_SERVICE) ? "user" : "system";
        qDebug(lc_err_notifier) << "Using" << serviceType
                                << "service name as application label:" << identifier;
        return identifier;
    }

    return QString();
}

QString ErrNotifierService::identifyApplication(quint32 pid, const QString &cgroup) const
{
    // Call ApplicationManager1.Identify to get application info
    const int pidfd = pidfd_open(static_cast<pid_t>(pid), 0);
    if (pidfd < 0) {
        qWarning(lc_err_notifier) << "pidfd_open failed for PID" << pid
                                  << "error: " << std::strerror(errno);

        // Try to extract app path from cgroup when pidfd_open fails
        const QString appName = identifyFromCgroupPath(cgroup);
        if (!appName.isEmpty()) {
            return appName;
        }

        return QString();
    }
    qDebug(lc_err_notifier) << "Identifying application by PID:" << pid;

    QDBusInterface app_manager(APP_MANAGER_SERVICE,
                               APP_MANAGER_PATH,
                               APP_MANAGER_INTERFACE,
                               QDBusConnection::sessionBus());

    if (!app_manager.isValid()) {
        qWarning(lc_err_notifier) << "Failed to connect to ApplicationManager1:"
                                  << app_manager.lastError().message();
        close(pidfd);
        return QString();
    }

    // Register the custom type for DBus
    qDBusRegisterMetaType<ObjectInterfaceMap>();

    QDBusUnixFileDescriptor fd_descriptor(pidfd);
    close(pidfd); // Close original fd immediately, fd_descriptor holds a dup'd copy
    QDBusMessage reply =
            app_manager.call(QStringLiteral("Identify"), QVariant::fromValue(fd_descriptor));

    if (reply.type() == QDBusMessage::ReplyMessage) {
        const QList<QVariant> args = reply.arguments();
        if (args.size() >= 3) {
            const QString id = args[0].toString();
            const QDBusObjectPath instance = qvariant_cast<QDBusObjectPath>(args[1]);

            qDebug(lc_err_notifier) << "ApplicationManager1.Identify result:";
            qDebug(lc_err_notifier) << "  id:" << id;
            qDebug(lc_err_notifier) << "  instance:" << instance.path();

            // Try to get localized application name from the Application interface
            const QString localizedName = getLocalizedName(instance.path());
            if (!localizedName.isEmpty()) {
                return localizedName;
            }

            // Fall back to application id if available
            if (!id.isEmpty()) {
                return id;
            }
        }
    } else if (reply.type() == QDBusMessage::ErrorMessage) {
        qWarning(lc_err_notifier) << "ApplicationManager1.Identify failed:" << reply.errorName()
                                  << reply.errorMessage();

        // Try to extract app path from cgroup
        const QString appName = identifyFromCgroupPath(cgroup);
        if (!appName.isEmpty()) {
            return appName;
        }
    }

    return QString();
}

QString ErrNotifierService::extractLocalizedValue(const QMap<QString, QString> &localeMap) const
{
    if (localeMap.isEmpty()) {
        return QString();
    }

    // Get current locale
    const QLocale locale = QLocale::system();
    const QString localeName = locale.name(); // e.g., "zh_CN", "en_US"

    // Try exact match first (e.g., "zh_CN")
    if (localeMap.contains(localeName)) {
        return localeMap.value(localeName);
    }

    // Try language code only (e.g., "zh" from "zh_CN")
    const QString languageCode = localeName.split(QLatin1Char('_')).first();
    if (localeMap.contains(languageCode)) {
        return localeMap.value(languageCode);
    }

    // Try default
    if (localeMap.contains(QLatin1String("default"))) {
        return localeMap.value(QLatin1String("default"));
    }

    // Return any non-empty value as last resort
    for (auto it = localeMap.begin(); it != localeMap.end(); ++it) {
        const QString &value = it.value();
        if (!value.isEmpty()) {
            return value;
        }
    }

    return QString();
}

/**
 * @brief Retrieves the localized application name from ApplicationManager1 D-Bus interface
 *
 * This function queries the org.desktopspec.ApplicationManager1 service to obtain
 * the localized name of an application. It first attempts to retrieve the "Name"
 * property, and falls back to "GenericName" if "Name" is unavailable or empty.
 *
 * @param appInstancePath The D-Bus object path to the application. If truncateLeft
 *                        is true, this should be an instance path (e.g.,
 *                        "/org/desktopspec/ApplicationManager1/org_2egnome_2edfeet/instance_id").
 *                        If truncateLeft is false, this should be a app path.
 * @param truncateLeft If true, removes the last path component to get the parent
 *                     application path. If false, uses the path as-is.
 *
 * @return QString The localized application name based on current system locale,
 *                 or an empty QString if the name cannot be retrieved or the path
 *                 is invalid.
 */
QString ErrNotifierService::getLocalizedName(const QString &appInstancePath,
                                             bool truncateLeft) const
{
    if (appInstancePath.isEmpty()) {
        return QString();
    }

    QString appPath;

    if (truncateLeft) {
        // Get parent path by removing the last component
        // e.g., "/org/desktopspec/ApplicationManager1/org_2egnome_2edfeet/instance_id"
        // -> "/org/desktopspec/ApplicationManager1/org_2egnome_2edfeet"
        const int lastSlash = appInstancePath.lastIndexOf(QLatin1Char('/'));
        if (lastSlash <= 0) {
            return QString();
        }
        appPath = appInstancePath.left(lastSlash);
    } else {
        // Use the path as-is (already complete app path)
        appPath = appInstancePath;
    }

    qDebug(lc_err_notifier) << "Application object path:" << appPath;

    // Query the Application interface for Name and GenericName properties
    DDBusInterface app_interface(APP_MANAGER_SERVICE,
                                 appPath,
                                 APP_MANAGER_APPLICATION_INTERFACE,
                                 QDBusConnection::sessionBus(),
                                 const_cast<ErrNotifierService *>(this));

    if (!app_interface.isValid()) {
        qWarning(lc_err_notifier) << "Failed to connect to Application interface:"
                                  << app_interface.lastError().message();
        return QString();
    }

    // Try to get Name property first
    QVariant nameVariant = app_interface.property("Name");
    if (nameVariant.isValid()) {
        const QMap<QString, QString> nameMap = qdbus_cast<QMap<QString, QString> >(nameVariant);
        qDebug(lc_err_notifier) << "Name property:" << nameMap;
        const QString name = extractLocalizedValue(nameMap);
        if (!name.isEmpty()) {
            return name;
        }
    }

    // Fall back to GenericName
    QVariant genericNameVariant = app_interface.property("GenericName");
    if (genericNameVariant.isValid()) {
        const QMap<QString, QString> genericNameMap =
                qdbus_cast<QMap<QString, QString> >(genericNameVariant);
        qDebug(lc_err_notifier) << "GenericName property:" << genericNameMap;
        const QString genericName = extractLocalizedValue(genericNameMap);
        if (!genericName.isEmpty()) {
            return genericName;
        }
    }

    return QString();
}

QString ErrNotifierService::variantToString(const QVariant &value) const
{
    if (!value.isValid()) {
        return QString();
    }

    if (value.userType() == qMetaTypeId<QDBusVariant>()) {
        return variantToString(value.value<QDBusVariant>().variant());
    }

    return value.toString().trimmed();
}

quint32 ErrNotifierService::variantToUint(const QVariant &value) const
{
    if (!value.isValid()) {
        return 0;
    }

    if (value.userType() == qMetaTypeId<QDBusVariant>()) {
        return variantToUint(value.value<QDBusVariant>().variant());
    }

    bool ok = false;
    const quint32 number = value.toUInt(&ok);
    return ok ? number : 0;
}

QString ErrNotifierService::defaultDirectory() const
{
    QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
    if (path.isEmpty()) {
        path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
    }
    if (path.isEmpty()) {
        path = QDir::homePath();
    }
    return path;
}

QString ErrNotifierService::defaultFileName() const
{
    const QString dateTime =
            QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss"));
    return tr("write_denied_log_%1").arg(dateTime) + QStringLiteral(".txt");
}

/**
 * @brief Get the package name that provides a systemd service
 *
 * This function queries systemd to find the service file path, then uses dpkg
 * to determine which package provides that file.
 *
 * @param serviceName The name of the service (without .service suffix)
 * @param serviceType Either SYSTEM_SERVICE or USER_SERVICE
 * @return The package name if found, empty string otherwise
 */
QString ErrNotifierService::getPackageNameFromService(const QString &serviceName,
                                                      AppIdentifierType serviceType) const
{
    if (serviceName.isEmpty()) {
        return QString();
    }

    // Step 1: Execute systemctl cat to get service file path
    QProcess process;
    QString program = QStringLiteral("systemctl");
    QStringList arguments;

    if (serviceType == AppIdentifierType::USER_SERVICE) {
        arguments << QStringLiteral("--user");
    }
    arguments << QStringLiteral("cat") << (serviceName + QStringLiteral(".service"));

    qDebug(lc_err_notifier) << "Executing:" << program << arguments.join(' ');

    process.start(program, arguments);
    if (!process.waitForFinished(3000)) {
        qWarning(lc_err_notifier) << "systemctl cat command timed out for service:" << serviceName;
        process.kill();
        return QString();
    }

    if (process.exitCode() != 0) {
        qWarning(lc_err_notifier) << "systemctl cat failed for service:" << serviceName
                                  << "Error:" << process.readAllStandardError();
        return QString();
    }

    const QString output = QString::fromUtf8(process.readAllStandardOutput());

    // Step 2: Extract service file path from output
    const QString servicePath = extractServiceFilePath(output);
    if (servicePath.isEmpty()) {
        qWarning(lc_err_notifier) << "Failed to extract service file path for:" << serviceName;
        return QString();
    }

    qDebug(lc_err_notifier) << "Service file path:" << servicePath;

    // Step 3: Query package using dpkg
    QString packageName = queryPackageByPath(servicePath);

    // Step 4: If failed and path starts with /usr/, try without /usr prefix
    if (packageName.isEmpty() && servicePath.startsWith(QLatin1String("/usr/"))) {
        const QString alternativePath = servicePath.mid(4); // Remove "/usr"
        qDebug(lc_err_notifier) << "Retrying with alternative path:" << alternativePath;
        packageName = queryPackageByPath(alternativePath);
    }

    if (!packageName.isEmpty()) {
        qDebug(lc_err_notifier) << "Found package:" << packageName << "for service:" << serviceName;
    } else {
        qDebug(lc_err_notifier) << "No package found for service:" << serviceName;
    }

    return packageName;
}

/**
 * @brief Extract service file path from systemctl cat output
 *
 * Parses the first line of systemctl cat output to extract the file path.
 * Expected format: "# /path/to/service.service"
 *
 * @param systemctlOutput The output from systemctl cat command
 * @return The extracted file path, or empty string if not found
 */
QString ErrNotifierService::extractServiceFilePath(const QString &systemctlOutput) const
{
    if (systemctlOutput.isEmpty()) {
        return QString();
    }

    // Get the first line
    const int newlinePos = systemctlOutput.indexOf(QLatin1Char('\n'));
    const QString firstLine =
            (newlinePos >= 0) ? systemctlOutput.left(newlinePos) : systemctlOutput;

    // Look for a path starting with '/'
    // Format is typically: "# /path/to/file.service"
    const int slashPos = firstLine.indexOf(QLatin1Char('/'));
    if (slashPos < 0) {
        return QString();
    }

    // Extract from '/' to the end of the line
    QString path = firstLine.mid(slashPos).trimmed();
    return path;
}

/**
 * @brief Query which package provides a file using dpkg
 *
 * @param filePath The file path to query
 * @return The package name if found, empty string otherwise
 */
QString ErrNotifierService::queryPackageByPath(const QString &filePath) const
{
    if (filePath.isEmpty()) {
        return QString();
    }

    QProcess process;
    const QString program = QStringLiteral("dpkg");
    const QStringList arguments = { QStringLiteral("-S"), filePath };

    qDebug(lc_err_notifier) << "Executing: dpkg -S" << filePath;

    process.start(program, arguments);
    // 1 minute timeout
    if (!process.waitForFinished(60 * 1000)) {
        qWarning(lc_err_notifier) << "dpkg -S command timed out";
        process.kill();
        return QString();
    }

    if (process.exitCode() != 0) {
        qDebug(lc_err_notifier) << "dpkg -S failed for path:" << filePath
                                << "Error:" << process.readAllStandardError();
        return QString();
    }

    const QString output = QString::fromUtf8(process.readAllStandardOutput()).trimmed();

    // Parse output format: "package-name: /path/to/file"
    const int colonPos = output.indexOf(QLatin1Char(':'));
    if (colonPos < 0) {
        qWarning(lc_err_notifier) << "Unexpected dpkg output format:" << output;
        return QString();
    }

    const QString packageName = output.left(colonPos).trimmed();
    return packageName;
}

// ErrNotifierAdaptor implementation
ErrNotifierAdaptor::ErrNotifierAdaptor(ErrNotifierService *service)
    : QDBusAbstractAdaptor(service)
    , m_service(service)
{
}

bool ErrNotifierAdaptor::notificationEnabled() const
{
    return m_service->notificationEnabled();
}

void ErrNotifierAdaptor::setNotificationEnabled(bool enabled)
{
    m_service->setNotificationEnabled(enabled);
}

void ErrNotifierAdaptor::ExportLog(const QDBusMessage &message)
{
    ExportLogError error = m_service->doExportLog();
    if (error != ExportLogError::None) {
        QString errorMsg;
        QString errorName;

        switch (error) {
        case ExportLogError::InProgress:
            errorMsg = "Export operation already in progress";
            errorName = "org.deepin.immutable.Error.InProgress";
            break;
        case ExportLogError::AuthenticationFailed:
            errorMsg = "Failed to request log from system service or authentication denied";
            errorName = QDBusError::errorString(QDBusError::AccessDenied);
            break;
        case ExportLogError::SystemServiceUnavailable:
            errorMsg = "System service unavailable";
            errorName = QDBusError::errorString(QDBusError::Failed);
            break;
        default:
            errorMsg = "Unknown error";
            errorName = QDBusError::errorString(QDBusError::Failed);
            break;
        }

        qWarning(lc_err_notifier) << "ExportLog failed:" << errorMsg;

        // Create and send error reply manually
        QDBusMessage errorReply = message.createErrorReply(errorName, errorMsg);
        QDBusConnection::sessionBus().send(errorReply);
    } else {
        // Send success reply manually
        QDBusMessage reply = message.createReply();
        QDBusConnection::sessionBus().send(reply);
    }
}
