/*
    SPDX-License-Identifier: GPL-2.0-or-later
    SPDX-FileCopyrightText: 2004 Jan Schaefer <j_schaef@informatik.uni-kl.de>
    SPDX-FileCopyrightText: 2011 Rodrigo Belem <rclbelem@gmail.com>
    SPDX-FileCopyrightText: 2015-2020 Harald Sitter <sitter@kde.org>
    SPDX-FileCopyrightText: 2019 Nate Graham <nate@kde.org>
    SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>
    SPDX-FileCopyrightText: 2026 Thomas Duckworth <tduck@filotimoproject.org>
*/

#include "sambausershareplugin.h"

#include <KLocalizedString>
#include <QClipboard>
#include <QCoroDBusPendingReply>
#include <QCoroSignal>
#include <QCoroTask>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDebug>
#include <QMainWindow>
#include <QMetaMethod>
#include <QNetworkInterface>
#include <QPushButton>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickItem>
#include <QQuickWidget>
#include <QStandardPaths>
#include <QTimer>
#include <QVBoxLayout>

#include <KIO/CommandLauncherJob>
#include <KLocalizedContext>
#include <KMessageBox>
#include <KOSRelease>
#include <KPluginFactory>
#include <KService>

#include "org.freedesktop.Avahi.Server.h"

#include "groupmanager.h"

#ifdef SAMBA_INSTALL
#include "sambainstaller.h"
#endif

#ifdef USE_SYSTEMD
#include "servicehelper.h"
#endif

K_PLUGIN_CLASS_WITH_JSON(SambaUserSharePlugin, "sambausershareplugin.json")

using namespace Qt::StringLiterals;

SambaUserSharePlugin::SambaUserSharePlugin(QObject *parent)
    : KPropertiesDialogPlugin(parent)
    , m_url(properties->item().mostLocalUrl().toLocalFile())
    , m_userManager(new UserManager(this))
{
    if (m_url.isEmpty()) {
        return;
    }

    const QFileInfo pathInfo(m_url);
    if (!pathInfo.permission(QFile::ReadUser | QFile::WriteUser)) {
        return;
    }

    /**
     * Set QtQuick rendering to software if parent is using RasterSurface.
     * This avoids random restarts with Dolphin for example.
     * BUG:494627
     */
    const auto parentDialog = qobject_cast<KPropertiesDialog *>(parent);
    if (parentDialog->nativeParentWidget()) {
        const auto nativeParentWidget = qobject_cast<QMainWindow *>(parentDialog->nativeParentWidget());
        if (nativeParentWidget && nativeParentWidget->windowHandle()) {
            if (nativeParentWidget->windowHandle()->surfaceType() == QSurface::RasterSurface) {
                QQuickWindow::setGraphicsApi(QSGRendererInterface::Software);
            }
        }
    }

    // TODO: this could be made to load delayed via invokemethod. we technically don't need to fully load
    //   the backing data in the ctor, only the qml view with busyindicator
    // TODO: relatedly if we make ShareContext and the Model more async vis a vis construction we can init them from
    //   QML and stop holding them as members in the plugin
    m_context = new ShareContext(properties->item().mostLocalUrl(), this);
    // FIXME maybe the manager ought to be owned by the model
    qmlRegisterAnonymousType<UserManager>("org.kde.filesharing.samba", 1);
    qmlRegisterAnonymousType<User>("org.kde.filesharing.samba", 1);
    m_model = new UserPermissionModel(m_context->m_shareData, m_userManager, this);
    qmlRegisterAnonymousType<PermissionsHelper>("org.kde.filesharing.samba", 1);
    m_permissionsHelper = new PermissionsHelper(m_context->m_shareData.path(), m_userManager, m_model);

#ifdef SAMBA_INSTALL
    qmlRegisterType<SambaInstaller>("org.kde.filesharing.samba", 1, 0, "Installer");
#endif
#ifdef USE_SYSTEMD
    qmlRegisterType<ServiceHelper>("org.kde.filesharing.samba", 1, 0, "ServiceHelper");
#endif
    qmlRegisterType<GroupManager>("org.kde.filesharing.samba", 1, 0, "GroupManager");
    // Need access to the column enum, so register this as uncreatable.
    qmlRegisterUncreatableType<UserPermissionModel>("org.kde.filesharing.samba",
                                                    1,
                                                    0,
                                                    "UserPermissionModel",
                                                    QStringLiteral("Access through sambaPlugin.userPermissionModel"));
    qmlRegisterAnonymousType<ShareContext>("org.kde.filesharing.samba", 1);
    qmlRegisterAnonymousType<SambaUserSharePlugin>("org.kde.filesharing.samba", 1);

    m_page.reset(new QWidget(parentDialog));
    m_page->setAttribute(Qt::WA_TranslucentBackground);

    auto widget = new QQuickWidget(m_page.get());
    // Set translation domain before setting the source so strings gets translated.
    auto i18nContext = new KLocalizedContext(widget->engine());
    i18nContext->setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN));
    widget->engine()->rootContext()->setContextObject(i18nContext);

    widget->setResizeMode(QQuickWidget::SizeRootObjectToView);
    widget->setFocusPolicy(Qt::StrongFocus);
    widget->setAttribute(Qt::WA_AlwaysStackOnTop, true);
    widget->quickWindow()->setColor(Qt::transparent);
    auto layout = new QVBoxLayout(m_page.get());
    layout->setContentsMargins({});
    layout->addWidget(widget);

    widget->rootContext()->setContextProperty(QStringLiteral("sambaPlugin"), this);

    const QUrl url(QStringLiteral("qrc:/org.kde.filesharing.samba/qml/main.qml"));
    widget->setSource(url);

    properties->setFileSharingPage(m_page.get());
    if (qEnvironmentVariableIsSet("TEST_FOCUS_SHARE")) {
        QTimer::singleShot(100, properties, &KPropertiesDialog::showFileSharingPage);
    }

    connect(this, &SambaUserSharePlugin::reinitialize, this, [this]() {
        initializeChain();
    });

    // Start the initialization chain asynchronously (once the event loop starts).
    // This eventually sets the plugin as ready.
    QMetaObject::invokeMethod(
        this,
        [this]() {
            initializeChain();
        },
        Qt::QueuedConnection);
}

QCoro::Task<void> SambaUserSharePlugin::initializeChain()
{
    if (isReady()) {
        co_return;
    }

    if (!isSambaInstalled()) {
#ifdef SAMBA_INSTALL
        Q_EMIT pagePushed(u"qrc:/org.kde.filesharing.samba/qml/InstallPage.qml"_s);
#else
        Q_EMIT pagePushed(u"qrc:/org.kde.filesharing.samba/qml/MissingSambaPage.qml"_s);
#endif
        co_return;
    }

#ifdef USE_SYSTEMD
    if (!m_serviceHelper) {
        m_serviceHelper = new ServiceHelper(this);
    }
    const bool serviceReady = co_await ServiceHelper::isServiceReady();

    if (!serviceReady) {
        Q_EMIT pagePushed(u"qrc:/org.kde.filesharing.samba/qml/ServicePage.qml"_s);
        co_return;
    }
#endif

    if (!m_groupManager) {
        m_groupManager = new GroupManager(this);
    }

    if (!m_groupManager->isReady()) {
        co_await qCoro(m_groupManager, &GroupManager::isReadyChanged);
    }

    if (!m_groupManager->errorExplanation().isEmpty()) {
        Q_EMIT pagePushed(u"qrc:/org.kde.filesharing.samba/qml/GroupPage.qml"_s);
        co_return;
    }

    m_userManager->load();
    co_await qCoro(m_userManager, &UserManager::loaded);

    auto user = m_userManager->currentUser();
    if (!user || user->name().isEmpty() || !user->inSamba()) {
        Q_EMIT pagePushed(u"qrc:/org.kde.filesharing.samba/qml/UserPage.qml"_s);
        co_return;
    }

    if (m_needsReboot) {
        Q_EMIT pagePushed(u"qrc:/org.kde.filesharing.samba/qml/RebootPage.qml"_s);
        co_return;
    }

    m_permissionsHelper->reload();

    co_await initAddressList();

    setReady(true);
}

QCoro::Task<void> SambaUserSharePlugin::initAddressList()
{
    QStringList addressList;

    const auto interfaces = QNetworkInterface::allInterfaces();
    for (const auto &interface : interfaces) {
        if (!(interface.flags() & QNetworkInterface::IsLoopBack)) {
            for (const auto &address : interface.addressEntries()) {
                if (address.ip().isPrivateUse()) {
                    addressList.append(address.ip().toString());
                }
            }
        }
    }

    // Get fully qualified domain name
    OrgFreedesktopAvahiServerInterface avahi(QStringLiteral("org.freedesktop.Avahi"), QStringLiteral("/"), QDBusConnection::systemBus());

    auto reply = co_await avahi.GetHostNameFqdn();

    if (reply.isValid()) {
        QString fqdn = reply.value();
        if (!fqdn.isEmpty()) {
            addressList.append(fqdn);
        }
    }

    m_addressList = addressList;
    Q_EMIT addressListChanged();
}

bool SambaUserSharePlugin::isSambaInstalled()
{
    if (QStandardPaths::findExecutable(u"smbd"_s).isEmpty()
        && QStandardPaths::findExecutable(u"smbd"_s, {u"/usr/sbin/"_s, u"/usr/local/sbin/"_s}).isEmpty()) {
        return false;
    }
    return true;
}

void SambaUserSharePlugin::showSambaStatus()
{
    auto job = new KIO::CommandLauncherJob(QStringLiteral("kinfocenter"), {QStringLiteral("kcm_samba")});
    job->setDesktopName(QStringLiteral("org.kde.kinfocenter"));
    job->start();
}

void SambaUserSharePlugin::copyAddressToClipboard(const QString &address)
{
    QGuiApplication::clipboard()->setText(address.trimmed());
}

void SambaUserSharePlugin::applyChanges()
{
    qDebug() << "!!! applying changes !!!" << m_context->enabled() << m_context->name() << m_context->guestEnabled() << m_model->getAcl()
             << m_context->m_shareData.path();
    if (!m_context->enabled()) {
        reportRemove(m_context->m_shareData.remove());
        return;
    }

    // TODO: should run this through reportAdd() as well, ACLs may be invalid and then we shouldn't try to save
    m_context->m_shareData.setAcl(m_model->getAcl());
    reportAdd(m_context->m_shareData.save());
}

QString SambaUserSharePlugin::bugReportUrl() const
{
    return KOSRelease().bugReportUrl();
}

static QString errorToString(KSambaShareData::UserShareError error)
{
    // KSambaShare is a right mess. Every function with an error returns the same enum but can only return a subset of
    // possible values. Even so, because it returns the enum we had best mapped all values to semi-suitable string
    // representations even when those are utter garbage when they require specific (e.g. an invalid ACL) that
    // we do not have here.
    switch (error) {
    case KSambaShareData::UserShareNameOk:
        Q_FALLTHROUGH();
    case KSambaShareData::UserSharePathOk:
        Q_FALLTHROUGH();
    case KSambaShareData::UserShareAclOk:
        Q_FALLTHROUGH();
    case KSambaShareData::UserShareCommentOk:
        Q_FALLTHROUGH();
    case KSambaShareData::UserShareGuestsOk:
        Q_FALLTHROUGH();
    case KSambaShareData::UserShareOk:
        // Technically anything but UserShareOk cannot happen, but best handle everything regardless.
        return QString();
    case KSambaShareData::UserShareExceedMaxShares:
        return i18nc("@info detailed error messsage", "You have exhausted the maximum amount of shared directories you may have active at the same time.");
    case KSambaShareData::UserShareNameInvalid:
        return i18nc("@info detailed error messsage", "The share name is invalid.");
    case KSambaShareData::UserShareNameInUse:
        return i18nc("@info detailed error messsage", "The share name is already in use for a different directory.");
    case KSambaShareData::UserSharePathInvalid:
        return i18nc("@info detailed error messsage", "The path is invalid.");
    case KSambaShareData::UserSharePathNotExists:
        return i18nc("@info detailed error messsage", "The path does not exist.");
    case KSambaShareData::UserSharePathNotDirectory:
        return i18nc("@info detailed error messsage", "The path is not a directory.");
    case KSambaShareData::UserSharePathNotAbsolute:
        return i18nc("@info detailed error messsage", "The path is relative.");
    case KSambaShareData::UserSharePathNotAllowed:
        return i18nc("@info detailed error messsage", "This path may not be shared.");
    case KSambaShareData::UserShareAclInvalid:
        return i18nc("@info detailed error messsage", "The access rule is invalid.");
    case KSambaShareData::UserShareAclUserNotValid:
        return i18nc("@info detailed error messsage", "An access rule's user is not valid.");
    case KSambaShareData::UserShareGuestsInvalid:
        return i18nc("@info detailed error messsage", "The 'Guest' access rule is invalid.");
    case KSambaShareData::UserShareGuestsNotAllowed:
        return i18nc("@info detailed error messsage", "Enabling guest access is not allowed.");
    case KSambaShareData::UserShareSystemError:
        return KSambaShare::instance()->lastSystemErrorString().simplified();
    }
    Q_UNREACHABLE();
    return QString();
}

void SambaUserSharePlugin::reportAdd(KSambaShareData::UserShareError error)
{
    if (error == KSambaShareData::UserShareOk) {
        return;
    }

    QString errorMessage = errorToString(error);
    if (error == KSambaShareData::UserShareSystemError) {
        // System errors are (untranslated) CLI output. Give them localized context.
        errorMessage = xi18nc("@info error in the underlying binaries. %1 is CLI output",
                              "<para>An error occurred while trying to share the directory."
                              " The share has not been created.</para>"
                              "<para>Samba internals report:</para><message>%1</message>",
                              errorMessage);
    }
    KMessageBox::error(qobject_cast<QWidget *>(parent()), errorMessage, i18nc("@info/title", "Failed to Create Network Share"));
}

void SambaUserSharePlugin::reportRemove(KSambaShareData::UserShareError error)
{
    if (error == KSambaShareData::UserShareOk) {
        return;
    }

    QString errorMessage = errorToString(error);
    if (error == KSambaShareData::UserShareSystemError) {
        // System errors are (untranslated) CLI output. Give them localized context.
        errorMessage = xi18nc("@info error in the underlying binaries. %1 is CLI output",
                              "<para>An error occurred while trying to un-share the directory."
                              " The share has not been removed.</para>"
                              "<para>Samba internals report:</para><message>%1</message>",
                              errorMessage);
    }
    KMessageBox::error(qobject_cast<QWidget *>(parent()), errorMessage, i18nc("@info/title", "Failed to Remove Network Share"));
}

bool SambaUserSharePlugin::isReady() const
{
    return m_ready;
}

void SambaUserSharePlugin::setReady(bool ready)
{
    m_ready = ready;
    Q_EMIT readyChanged();
}

bool SambaUserSharePlugin::needsReboot() const
{
    return m_needsReboot;
}

void SambaUserSharePlugin::setNeedsReboot(bool reboot)
{
    if (m_needsReboot == reboot) {
        return;
    }
    m_needsReboot = reboot;
    Q_EMIT needsRebootChanged();
}

void SambaUserSharePlugin::reboot()
{
    QDBusInterface kdeLogoutPrompt(QStringLiteral("org.kde.LogoutPrompt"), QStringLiteral("/LogoutPrompt"), QStringLiteral("org.kde.LogoutPrompt"));
    kdeLogoutPrompt.call(QStringLiteral("promptReboot"));
}

#include "sambausershareplugin.moc"
