/***************************************************************************
 * Copyright (C) 2025-2026 UnionTech Software Technology Co., Ltd.
 * Copyright (c) 2014-2015 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
 * Copyright (c) 2014 Martin Bříza <mbriza@redhat.com>
 * Copyright (c) 2013 Abdurrahman AVCI <abdurrahmanavci@gmail.com>
 *
 * 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 2 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, write to the
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 ***************************************************************************/

#include "Display.h"

#include "Auth.h"
#include "Configuration.h"
#include "DaemonApp.h"
#include "DdeSeatdControl.h"
#include "DisplayManager.h"
#include "Messages.h"
#include "SeatManager.h"
#include "SocketServer.h"
#include "SocketWriter.h"
#include "TreelandConnector.h"
#include "TreelandDisplayServer.h"
#include "XorgDisplayServer.h"

#include "config.h"
#include "Login1Manager.h"

#include <QDBusConnection>
#include <QDebug>
#include <QFile>
#include <QLocalSocket>
#include <QScopeGuard>

#include <pwd.h>
#include <qstringliteral.h>
#include <systemd/sd-login.h>
#include <sys/time.h>
#include <unistd.h>
#include <linux/vt.h>
#include <utility>

#define STRINGIFY(x) #x

namespace DDM {
    static bool isTtyInUse(const QString &desiredTty) {
        char **sessions = nullptr;
        auto guard = qScopeGuard([&sessions] {
            if (sessions) {
                for (char **s = sessions; s && *s; ++s)
                    free(*s);
                free(sessions);
            }
        });
        sd_get_sessions(&sessions);
        for (char **s = sessions; s && *s; ++s) {
            char *tty = nullptr;
            char *state = nullptr;
            auto guard2 = qScopeGuard([&tty, &state] {
                if (tty)
                    free(tty);
                if (state)
                    free(state);
            });
            if (sd_session_get_tty(*s, &tty) < 0 || sd_session_get_state(*s, &state) < 0)
                continue;
            if (desiredTty == tty && strcmp(state, "closing") != 0) {
                qDebug() << "tty" << desiredTty << "already in use by session" << *s;
                return true;
            }
        }
        return false;
    }

    static int fetchAvailableVt() {
        if (!isTtyInUse(QStringLiteral("tty" STRINGIFY(DDM_INITIAL_VT)))) {
            return DDM_INITIAL_VT;
        }
        const auto vt = daemonApp->seatdControl()->activeVt();
        if (vt > 0 && !isTtyInUse(QStringLiteral("tty%1").arg(vt))) {
            return vt;
        }
        return daemonApp->seatdControl()->findAvailableVt();
    }

    static bool isVtReservedByDdm(int vt) {
        for (Display *display : std::as_const(daemonApp->seatManager()->displays)) {
            if (display->terminalId == vt)
                return true;
            for (Auth *auth : std::as_const(display->auths)) {
                if (auth->tty == vt)
                    return true;
            }
        }
        return false;
    }

    static int fetchAvailableUserVt() {
        for (int vt = DDM_INITIAL_VT; vt <= MAX_NR_CONSOLES; ++vt) {
            if (isVtReservedByDdm(vt))
                continue;
            if (!isTtyInUse(QStringLiteral("tty%1").arg(vt)))
                return vt;
        }

        return daemonApp->seatdControl()->findAvailableVt();
    }

    Display::Display(SeatManager *parent, QString name)
        : QObject(parent)
        , name(name)
        , m_socketServer(new SocketServer(this)) {

        // Create display server
        terminalId = fetchAvailableVt();
        qDebug("Using VT %d", terminalId);
        m_treeland = new TreelandDisplayServer(m_socketServer, this);

        // Record current VT as ddm user session
        DaemonApp::instance()->displayManager()->AddSession(
            {},
            name,
            "dde",
            terminalId);

        // connect connected signal
        connect(m_socketServer, &SocketServer::connected, this, &Display::connected);

        // connect login signal
        connect(m_socketServer, &SocketServer::login, this, &Display::login);

        // connect logout signal
        connect(m_socketServer, &SocketServer::logout, this, &Display::logout);

        // connect lock signal
        connect(m_socketServer, &SocketServer::lock, this, &Display::lock);

        // connect unlock signal
        connect(m_socketServer, &SocketServer::unlock,this, &Display::unlock);

        // connect login result signals
        connect(this, &Display::loginFailed, m_socketServer, &SocketServer::loginFailed);
    }

    Display::~Display() {
        stop();
    }

    void Display::activateSession(const QString &user, int xdgSessionId) {
        qWarning() << "Display activateSession requested for user" << user
                   << "xdgSessionId" << xdgSessionId
                   << "display VT" << terminalId;
        if (xdgSessionId <= 0 && user != QStringLiteral("dde")) {
            qCritical() << "Invalid xdg session id" << xdgSessionId << "for user" << user;
            return;
        }

        m_treeland->activateUser(user, xdgSessionId);

        if (xdgSessionId > 0 && Logind::isAvailable()) {
            OrgFreedesktopLogin1ManagerInterface manager(Logind::serviceName(), Logind::managerPath(), QDBusConnection::systemBus());
            manager.ActivateSession(QString::number(xdgSessionId));
        }
    }

    bool Display::start() {
        if (m_started)
            return true;

        if (!daemonApp->seatdControl()->requestSwitchVt(terminalId)) {
            qCritical() << "Failed to switch to greeter VT" << terminalId;
            return false;
        }
        if (!m_treeland->start())
            return false;

        // start socket server
        m_socketServer->start("treeland");

        // Update dbus info
        DaemonApp::instance()->displayManager()->setAuthInfo(m_socketServer->socketAddress());

        // change the owner and group of the socket to avoid permission denied errors
        struct passwd *pw = getpwnam("dde");
        if (pw && chown(qPrintable(m_socketServer->socketAddress()), pw->pw_uid, pw->pw_gid) == -1)
            qWarning() << "Failed to change owner of the socket";

        // set flags
        m_started = true;

        return true;
    }

    void Display::stop() {
        // check flag
        if (!m_started)
            return;

        for (auto *auth : std::as_const(auths)) {
            disconnect(auth, &Auth::sessionFinished, nullptr, nullptr);
            delete auth;
        }
        auths.clear();

        // stop socket server
        m_socketServer->stop();

        if (m_x11Server)
            m_x11Server->stop();

        // stop display server
        m_treeland->stop();

        // reset flag
        m_started = false;

        // emit signal
        emit stopped();
    }

    void Display::connected(QLocalSocket *socket) {
        // send logged in users (for possible crash recovery)
        SocketWriter writer(socket);
        bool hasActiveUser = false;
        for (Auth *auth : std::as_const(auths)) {
            if (auth->sessionOpened) {
                hasActiveUser = true;
                writer << quint32(DaemonMessages::UserLoggedIn) << auth->user << auth->xdgSessionId;
            }
        }

        // Autologin is only tried on the first connection after boot.
        if (!m_connectedOnce) {
            m_connectedOnce = true;
            if (!hasActiveUser && !mainConfig.Autologin.User.get().isEmpty()) {
                if (startAutologin())
                    return;
            }
        }

        SocketWriter showGreeter(socket);
        showGreeter << quint32(DaemonMessages::ShowGreeter);
    }

    void Display::login(QLocalSocket *socket,
                        const QString &user, const QString &password,
                        const Session &session) {
        if (user == QLatin1String("dde")) {
            qWarning() << "Login attempt for user dde";
            emit loginFailed(socket, user);
            return;
        }

        qInfo() << "Start login for user" << user;

        // Get Auth object
        Auth *auth = nullptr;
        for (auto *item : std::as_const(auths)) {
            if (item->user == user) {
                auth = item;
                break;
            }
        }

        if (!auth)
            auth = new Auth(this, user);

        if (auth->authenticated) {
            qWarning() << "Existing authentication ongoing, aborting";
            return;
        }
        const bool insertedAuth = !auths.contains(auth);
        if (insertedAuth)
            auths << auth;

        // Run password check
        if (!auth->authenticate(password.toLocal8Bit())) {
            if (insertedAuth) {
                auths.removeAll(auth);
                delete auth;
            }
            Q_EMIT loginFailed(socket, user);
            return;
        }

        // save last user and last session
        DaemonApp::instance()->displayManager()->setLastActivatedUser(user);
        if (mainConfig.Users.RememberLastUser.get())
            stateConfig.Last.User.set(auth->user);
        else
            stateConfig.Last.User.setDefault();
        if (mainConfig.Users.RememberLastSession.get())
            stateConfig.Last.Session.set(session.fileName());
        else
            stateConfig.Last.Session.setDefault();
        stateConfig.save();

        if (!startUserSession(auth, session))
            Q_EMIT loginFailed(socket, user);
    }

    bool Display::startUserSession(Auth *auth, const Session &session) {
        // sanity check
        if (!session.isValid()) {
            qCritical() << "Invalid session" << session.fileName();
            auths.removeAll(auth);
            delete auth;
            return false;
        }
        if (session.xdgSessionType().isEmpty()) {
            qCritical() << "Failed to find XDG session type for session" << session.fileName();
            auths.removeAll(auth);
            delete auth;
            return false;
        }
        if (session.exec().isEmpty()) {
            qCritical() << "Failed to find command for session" << session.fileName();
            auths.removeAll(auth);
            delete auth;
            return false;
        }

        const QString sessionId = QStringLiteral("Session%1").arg(daemonApp->newSessionId());
        auth->sessionId = sessionId;

        // Special preparation for each display server type
        //
        // TODO: Let Treeland drop DRM master when inactive, so that X
        // server and other Wayland compositor can co-exist with
        // greeter (the Treeland)
        QByteArray cookie;
        if (session.isSingleMode()) {
            auth->type = Treeland;
            const int ownerPid = daemonApp->treelandConnector()->mainPid();
            auth->tty = daemonApp->seatdControl()->createGroupVt(ownerPid, auth->user, sessionId);
            if (auth->tty <= 0) {
                qCritical() << "Failed to allocate grouped VT for Treeland user session";
                auths.removeAll(auth);
                delete auth;
                return false;
            }
        } else if (session.xdgSessionType() == QLatin1String("x11")) {
            auth->type = X11;
            auth->tty = fetchAvailableUserVt();
        } else {
            auth->type = Wayland;
            auth->tty = fetchAvailableUserVt();
        }

        if (auth->tty <= 0) {
            qCritical() << "Failed to allocate VT for user session";
            auths.removeAll(auth);
            delete auth;
            return false;
        }

        qInfo() << "Authentication succeeded for user" << auth->user << ", opening session"
                << session.fileName() << ", command:" << session.exec() << ", VT:" << auth->tty;

        // Prepare session environment
        QProcessEnvironment env;
        env.insert(session.additionalEnv());

        env.insert(QStringLiteral("XDG_SESSION_PATH"), daemonApp->displayManager()->sessionPath(sessionId));

        env.insert(QStringLiteral("PATH"), mainConfig.Users.DefaultPath.get());
        env.insert(QStringLiteral("DESKTOP_SESSION"), session.desktopSession());
        if (!session.desktopNames().isEmpty())
            env.insert(QStringLiteral("XDG_CURRENT_DESKTOP"), session.desktopNames());
        env.insert(QStringLiteral("XDG_SESSION_CLASS"), QStringLiteral("user"));
        env.insert(QStringLiteral("XDG_SESSION_TYPE"), session.xdgSessionType());
        env.insert(QStringLiteral("XDG_VTNR"), QString::number(auth->tty));
        env.insert(QStringLiteral("XDG_SEAT"), name);
        env.insert(QStringLiteral("XDG_SEAT_PATH"), daemonApp->displayManager()->seatPath(name));
        env.insert(QStringLiteral("XDG_SESSION_DESKTOP"), session.desktopNames());

        if (session.isSingleMode()) {
            env.insert("DDE_CURRENT_COMPOSITOR", "TreeLand");
        } else if (session.xdgSessionType() == QLatin1String("x11")) {
            qInfo() << "Stopping Treeland";
            daemonApp->treelandConnector()->disconnect();
            m_treeland->stop();
            QThread::msleep(500); // give some time to treeland to stop properly

            // Start X server
            qInfo() << "Starting X11 display server";
            m_x11Server = new XorgDisplayServer(this);
            connect(m_x11Server, &XorgDisplayServer::stopped, this, &Display::stop);
            if (!m_x11Server->start(auth->tty)) {
                qCritical() << "Failed to start X11 display server";
                delete m_x11Server;
                m_x11Server = nullptr;
                auths.removeAll(auth);
                delete auth;
                return false;
            }
            m_x11Server->setupDisplay();
            auth->display = m_x11Server->display;
            env.insert(QStringLiteral("DISPLAY"), m_x11Server->display);
            cookie = m_x11Server->cookie();
        } else {
            qInfo() << "Stopping Treeland";
            daemonApp->treelandConnector()->disconnect();
            m_treeland->stop();
            QThread::msleep(500); // give some time to treeland to stop properly
        }

        // Open Logind session & Exec the desktop process
        int xdgSessionId = auth->openSession(session.exec(), env, cookie);

        if (xdgSessionId <= 0) {
            qCritical() << "Failed to open logind session for user" << auth->user;
            if (auth->type == Treeland)
                daemonApp->seatdControl()->destroyGroupVt(auth->tty);
            auths.removeAll(auth);
            delete auth;
            return false;
        }

        connect(auth, &Auth::sessionFinished, this, [this, auth]() {
            qWarning() << "Session for user" << auth->user << "finished";
            auths.removeAll(auth);
            daemonApp->displayManager()->RemoveSession(auth->sessionId);
            if (auth->type == Treeland)
                daemonApp->seatdControl()->destroyGroupVt(auth->tty);
            delete auth;
        });
        daemonApp->displayManager()->AddSession(sessionId, name, auth->user, auth->tty);
        daemonApp->displayManager()->setLastSession(sessionId);

        if (auth->type == Treeland)
            activateSession(auth->user, xdgSessionId);
        qInfo() << "Successfully logged in user" << auth->user;
        return true;
    }

    bool Display::startAutologin() {
        const QString user = mainConfig.Autologin.User.get();
        if (user.isEmpty())
            return false;

        if (user == QLatin1String("dde")) {
            qWarning() << "Autologin user must not be the greeter user";
            return false;
        }

        qInfo() << "Starting automatic login for user" << user;

        // Determine session: autologin-session, or last used session
        QString sessionName = mainConfig.Autologin.Session.get();
        if (sessionName.isEmpty())
            sessionName = stateConfig.Last.Session.get();
        if (sessionName.isEmpty()) {
            qWarning() << "No session configured for autologin, keeping greeter";
            return false;
        }

        Session session(Session::WaylandSession, sessionName);
        if (!session.isValid())
            session = Session(Session::X11Session, sessionName);
        if (!session.isValid()) {
            qCritical() << "Failed to load autologin session" << sessionName;
            return false;
        }

        // Create Auth with the dedicated autologin PAM service
        Auth *auth = new Auth(this, user);
        auth->pamService = QStringLiteral("ddm-autologin");
        auths << auth;

        if (!auth->authenticate(QByteArray())) {
            qWarning() << "Autologin authentication failed for user" << user;
            auths.removeAll(auth);
            delete auth;
            return false;
        }

        // save last user and last session
        DaemonApp::instance()->displayManager()->setLastActivatedUser(user);
        if (mainConfig.Users.RememberLastUser.get())
            stateConfig.Last.User.set(user);
        else
            stateConfig.Last.User.setDefault();
        if (mainConfig.Users.RememberLastSession.get())
            stateConfig.Last.Session.set(session.fileName());
        else
            stateConfig.Last.Session.setDefault();
        stateConfig.save();

        if (!startUserSession(auth, session)) {
            qWarning() << "Failed to start automatic login session for user" << user;
            return false;
        }

        return true;
    }

    void Display::logout([[maybe_unused]] QLocalSocket *socket, int id) {
        qDebug() << "Logout requested for session id" << id;
        // Do not kill the session leader process before
        // TerminateSession! Logind will only kill the session's
        // cgroup (session_stop_scope) when the session is not in
        // "stopping" state, killing the session leader beforehand
        // will put the session in "stopping" early.

        // https://github.com/systemd/systemd/blob/main/src/login/logind-session.c#L938
        OrgFreedesktopLogin1ManagerInterface manager(Logind::serviceName(),
                                                     Logind::managerPath(),
                                                     QDBusConnection::systemBus());
        manager.TerminateSession(QString::number(id));
    }

    void Display::lock([[maybe_unused]] QLocalSocket *socket, int id) {
        qDebug() << "Lock requested for session id" << id;

        OrgFreedesktopLogin1ManagerInterface manager(Logind::serviceName(),
                                                     Logind::managerPath(),
                                                     QDBusConnection::systemBus());
        manager.LockSession(QString::number(id));
    }

    void Display::unlock(QLocalSocket *socket, const QString &user, const QString &password) {
        if (user == QLatin1String("dde")) {
            emit loginFailed(socket, user);
            return;
        }

        qInfo() << "Start identify user" << user;

        // Only run password check
        //
        // No user process execution, so the auth can be thrown away
        // immediately after use
        Auth auth(this, user);
        if (!auth.authenticate(password.toLocal8Bit())) {
            Q_EMIT loginFailed(socket, user);
            return;
        }

        // Save last user
        DaemonApp::instance()->displayManager()->setLastActivatedUser(user);
        if (mainConfig.Users.RememberLastUser.get())
            stateConfig.Last.User.set(user);
        else
            stateConfig.Last.User.setDefault();
        stateConfig.save();

        // Find the auth that started the session, which contains full informations
        for (auto *auth : std::as_const(auths)) {
            if (auth->user == user && auth->xdgSessionId > 0) {
                OrgFreedesktopLogin1ManagerInterface manager(Logind::serviceName(),
                                                             Logind::managerPath(),
                                                             QDBusConnection::systemBus());
                manager.UnlockSession(QString::number(auth->xdgSessionId));
                if (auth->type == Treeland)
                    activateSession(user, auth->xdgSessionId);
                else if (!daemonApp->seatdControl()->requestSwitchVt(auth->tty))
                    qWarning() << "Failed to switch to session VT" << auth->tty << "for user" << user;
                qInfo() << "Successfully identified user" << user;
                return;
            }
        }
        qWarning() << "No active session found for user" << user;
        Q_EMIT loginFailed(socket, user);
    }
}
