/*
 * Copyright (c) KylinSoft  Co., Ltd. 2024. All rights reserved.
 *
 * kaiming is licensed under the GPL v2.0+.
 * 
 * See the LICENSE file for more details.
 */

#include "KMHostAppDeploy.h"
#include "common/KMLogger.h"
#include "common/KMFileUtils.h"
#include "common/KMPackageManager.h"

#include <iostream>
#include <sstream>
#include <fstream>
#include <stdexcept>
#include <dirent.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>

class KMHostAppDeploy::Private
{
public:
    fs::path m_versionDataPath;
    std::string m_downloadRefId;
    fs::path m_infoPath;
    std::set<std::string> m_SynchronizedFiles;
    std::vector<std::string> m_vecService;
    std::vector<std::string> m_vecSysGroup;
    std::vector<std::string> m_vecDesktops;
    std::vector<std::string> m_vecAutostart;
    std::vector<std::string> m_vecChmods;
    std::vector<std::string> m_vecInstallkills;
    std::vector<std::string> m_vecUninstallkills;
    std::vector<std::string> m_vecStopservices;
    std::vector<std::string> m_vecRmdir;
    bool m_hostScript;
};

KMHostAppDeploy::KMHostAppDeploy(const std::string &infoPath)
    : d(std::make_unique<Private>())
{
    d->m_infoPath = infoPath;
}

KMHostAppDeploy::~KMHostAppDeploy()
{
}

bool KMHostAppDeploy::loadHostAppInfo()
{
    const fs::path infoFile = d->m_versionDataPath / "info.json";
    if (!fs::exists(infoFile))
    {
        kmlogger.debug("can't find app info file: %s", infoFile.string().c_str());
        return false;
    }

    KMInfoJson infoJson;
    infoJson.loadFile(infoFile);

    const auto &annotations = infoJson.getAnnotations();

    d->m_vecService = annotations.service;
    d->m_vecSysGroup = annotations.sysgroup;
    d->m_vecDesktops = annotations.desktops;
    d->m_vecAutostart = annotations.autostart;
    d->m_vecChmods = annotations.chmods;
    d->m_vecInstallkills = annotations.installkills;
    d->m_vecUninstallkills = annotations.uninstallkills;
    d->m_vecStopservices = annotations.stopservices;
    d->m_vecRmdir = annotations.rmdir;
    d->m_hostScript = annotations.hostscript;

    return annotations.hostapp;
}

void KMHostAppDeploy::setHostAppConfig(const fs::path &versionPath, const std::string &refID)
{
    d->m_versionDataPath = versionPath;
    d->m_downloadRefId = refID;
}

void KMHostAppDeploy::collectSynchronizedFiles()
{
    handleKareDriver();
    exportDesktopFile();

    fs::path m_extractDir = d->m_versionDataPath / "files";

    for (const auto &entry : fs::recursive_directory_iterator(m_extractDir))
    {
        std::string relativePath = entry.path().string().substr(m_extractDir.string().length());

        if (entry.path() == m_extractDir) {
            continue;
        }

        if (fs::is_regular_file(entry) || fs::is_directory(entry) || fs::is_symlink(entry))
        {
            if (fs::is_symlink(entry.path()) && fs::read_symlink(entry.path()) == entry.path())
            {
                continue;
            }

            d->m_SynchronizedFiles.insert(relativePath);
        }
    }
}

bool KMHostAppDeploy::checkFileConflicts()
{
    static const std::set<std::string> excludedDirs = {"/etc", "/var", "/usr/local/etc"};
    bool conflictFound = false;

    for (const auto &file : d->m_SynchronizedFiles)
    {
        if (shouldSkipFile(file, excludedDirs))
        {
            KMInfo("Skipping conflict check for: " + file);
            continue;
        }

        if (fs::exists(file))
        {
            KMError("File conflict already exists on system: " + file);
            conflictFound = true;
            //break;
        }
    }
    return conflictFound;
}

std::set<std::string> KMHostAppDeploy::getSynchronizedFiles()
{
    return d->m_SynchronizedFiles;
}

bool KMHostAppDeploy::shouldSkipFile(const std::string &file, const std::set<std::string> &excludedDirs)
{
    for (const auto &excludedDir : excludedDirs)
    {
        if (file.find(excludedDir) == 0)
        {
            return true;
        }
    }
    return false;
}

void KMHostAppDeploy::synchronizeFiles()
{
    fs::path m_sourcePath = d->m_versionDataPath / "files";
    std::set<std::string> excludeDirs = {"lib", "usr", "bin"};

    // 遍历源目录下的顶级目录
    for (const auto &entry : fs::directory_iterator(m_sourcePath))
    {
        if (!entry.is_directory())
        {
            continue;
        }

        std::string dirName = entry.path().filename().string();

        if (excludeDirs.find(dirName) != excludeDirs.end())
        {
            KMInfo("Skipping excluded directory: " + dirName);
            continue;
        }

        fs::path targetPath = fs::path("/") / dirName;
        std::ostringstream command;
        command << "rsync -av"
                << " " << entry.path() << "/ "
                << targetPath << "/";

        KMInfo("Syncing directory: " + entry.path().string());
        int result = system(command.str().c_str());
        if (result != 0)
        {
            KMError("Failed to sync directory: " + targetPath.string());
        }
    }

    handleInstallKills();
    handleAutoStart();
    handleDesktopFiles();
    handleChmods();

    writeSynchronizedFileList();
    fs::remove_all(m_sourcePath); // delete files

    handleSchemaCompilation();
    startServices();

    if (d->m_hostScript)
    {
        try
        {
            std::string basePath = "/opt/kaiming/exports/share/hostscript/" + d->m_downloadRefId + "/";
            executeScript(basePath + "postinst", "install");
        }
        catch (const std::exception &e)
        {
            kmlogger.error("Execute postinst script error: %s", e.what());
        }
    }
}

void KMHostAppDeploy::handleSchemaCompilation()
{
    fs::path schemaPath("/opt/system/resource/glib-2.0/schemas/");
    if (!fs::exists(schemaPath))
    {
        fs::create_directories(schemaPath);
    }

    std::ostringstream schemaCommand;
    schemaCommand << "glib-compile-schemas /opt/system/resource/glib-2.0/schemas/ >/dev/null 2>&1";
    system(schemaCommand.str().c_str());
}

void KMHostAppDeploy::handleAutoStart()
{
    fs::path autostartDir = "/etc/xdg/autostart";
    if (!fs::exists(autostartDir))
    {
        fs::create_directories(autostartDir);
    }

    for (const auto &autostartFile : d->m_vecAutostart)
    {
        if (autostartFile.empty())
        {
            kmlogger.debug("Empty autostart file path found, skipping...");
            continue;
        }

        fs::path sourceFile = autostartFile;
        fs::path targetFile = autostartDir / fs::path(autostartFile).filename();

        try
        {
            if (fs::exists(sourceFile))
            {
                fs::copy_file(sourceFile, targetFile, fs::copy_options::overwrite_existing);
                kmlogger.info("Copied autostart file: %s", autostartFile.c_str());
            }
            else
            {
                kmlogger.error("Autostart file not found: %s", sourceFile.string().c_str());
            }
        }
        catch (const std::exception &e)
        {
            kmlogger.error("Failed to copy autostart file: %s : %s", autostartFile.c_str(), e.what());
        }
    }
}

void KMHostAppDeploy::handleDesktopFiles()
{
    // 获取所有用户的桌面路径
    std::vector<fs::path> userDesktopDirs;
    DIR *dir = opendir("/home");
    if (dir)
    {
        struct dirent *entry;
        while ((entry = readdir(dir)) != nullptr)
        {
            if (entry->d_type == DT_DIR && strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0)
            {
                fs::path desktopPath = fs::path("/home") / entry->d_name / "桌面";
                if (fs::exists(desktopPath))
                {
                    userDesktopDirs.push_back(desktopPath);
                }
            }
        }
        closedir(dir);
    }

    for (const auto &desktopFile : d->m_vecDesktops)
    {
        if (desktopFile.empty())
        {
            kmlogger.debug("Empty desktop file path found, skipping...");
            continue;
        }

        fs::path sourceFile = desktopFile;
        fs::path fileName = fs::path(desktopFile).filename();

        // 拷贝到所有用户桌面
        for (const auto &userDesktop : userDesktopDirs)
        {
            fs::path userTargetFile = userDesktop / fileName;
            try
            {
                if (fs::exists(sourceFile))
                {
                    fs::copy_file(sourceFile, userTargetFile, fs::copy_options::overwrite_existing);
                    chmod(userTargetFile.c_str(), 0755);
                    d->m_SynchronizedFiles.insert(userTargetFile.string());
                    kmlogger.info("Copied desktop file to user desktop: %s", userTargetFile.string().c_str());
                }
            }
            catch (const std::exception &e)
            {
                kmlogger.error("Failed to copy desktop file to user desktop: %s : %s", userTargetFile.string().c_str(), e.what());
            }
        }
    }
}

void KMHostAppDeploy::handleChmods()
{
    for (const auto &chmodEntry : d->m_vecChmods)
    {
        if (chmodEntry.empty())
        {
            kmlogger.debug("Empty chmod entry found, skipping...");
            continue;
        }

        std::istringstream iss(chmodEntry);
        std::string modeStr, filePath;
        iss >> modeStr >> filePath;

        if (modeStr.empty() || filePath.empty())
        {
            kmlogger.error("Invalid chmod entry: %s", chmodEntry.c_str());
            continue;
        }

        bool isRecursive = false;
        size_t r_pos = modeStr.find("-R");

        if (r_pos != std::string::npos)
        {
            std::string suffix = modeStr.substr(r_pos);
            suffix.erase(std::remove_if(suffix.begin(), suffix.end(), ::isspace), suffix.end());
            if (suffix == "-R")
            {
                isRecursive = true;
                modeStr = modeStr.substr(0, r_pos);
            }
        }

        mode_t mode = static_cast<mode_t>(std::strtol(modeStr.c_str(), nullptr, 8));

        if (isRecursive)
        {
            chmodRecursive(filePath, mode);
            kmlogger.info("Recursively set permission %s for %s", modeStr.c_str(), filePath.c_str());
        }
        else
        {
            int ret = chmod(filePath.c_str(), mode);
            if (ret == 0)
            {
                struct stat pathStat;
                stat(filePath.c_str(), &pathStat);
                const char *type = S_ISDIR(pathStat.st_mode) ? "directory" : "file";
                kmlogger.info("Set permission %s for %s %s", modeStr.c_str(), type, filePath.c_str());
            }
            else
            {
                kmlogger.error("Failed to set permission %s for %s", modeStr.c_str(), filePath.c_str());
            }
        }
    }
}

void KMHostAppDeploy::chmodRecursive(const std::string& path, mode_t mode)
{
    struct stat statBuf;
    if (stat(path.c_str(), &statBuf) != 0) {
        return;
    }

    chmod(path.c_str(), mode);

    if (!S_ISDIR(statBuf.st_mode)) {
        return;
    }

    DIR* dir = opendir(path.c_str());
    if (!dir) {
        return;
    }

    struct dirent* entry;
    while ((entry = readdir(dir)) != nullptr) {
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        std::string fullPath = path + "/" + entry->d_name;
        chmodRecursive(fullPath, mode);
    }
    closedir(dir);
}
void KMHostAppDeploy::handleInstallKills()
{
    processKillList(d->m_vecInstallkills);
}

void KMHostAppDeploy::handleUninstallKills()
{
    processKillList(d->m_vecUninstallkills);
}

void KMHostAppDeploy::processKillList(const std::vector<std::string> &processes)
{
    for (const auto &procPath : processes)
    {
        if (procPath.empty())
        {
            kmlogger.debug("Empty process entry found, skipping...");
            continue;
        }

        std::string command = "pkill -9 -f '" + procPath + "'";
        int ret = system(command.c_str());
        if (ret == 0 || ret == 9)
        {
            kmlogger.info("Successfully killed process: %s", procPath.c_str());
        }
        else
        {
            kmlogger.error("Failed to kill process: %s", procPath.c_str());
        }
    }
}

void KMHostAppDeploy::handleStopServices()
{
    for (const auto &service : d->m_vecStopservices)
    {
        if (service.empty())
        {
            kmlogger.debug("Empty stopservice entry found, skipping...");
            continue;
        }

        std::ostringstream cmd;
        cmd << "systemctl stop " << service;
        int ret = system(cmd.str().c_str());
        if (ret == 0)
        {
            kmlogger.info("Stopped service: %s", service.c_str());
        }
        else
        {
            kmlogger.error("Failed to stop service: %s", service.c_str());
        }
    }
}

void KMHostAppDeploy::handleRmdir()
{
    static const std::vector<std::string> protectedPrefixes = {
        "/", "/home", "/usr", "/var", "/bin", "/lib", "/sbin", "/root", "/boot", "/dev", "/proc", "/sys", "/run"};

    // 检查是否为用户桌面目录
    auto isUserDesktop = [](const fs::path &dir) -> bool
    {
        std::string pathStr = dir.string();
        size_t pos = pathStr.find("/home/");
        if (pos == 0)
        {
            auto rest = pathStr.substr(6);
            auto slashPos = rest.find('/');
            if (slashPos != std::string::npos)
            {
                std::string subdir = rest.substr(slashPos + 1);
                if (subdir == "桌面" || subdir == "Desktop")
                {
                    return true;
                }
            }
        }
        return false;
    };

    for (const auto &dirPath : d->m_vecRmdir)
    {
        if (dirPath.empty())
        {
            kmlogger.debug("Empty rmdir entry found, skipping...");
            continue;
        }

        fs::path dirToRemove = dirPath;
        std::string dirStr = dirToRemove.string();

        // 保护系统目录和用户桌面
        bool isProtected = false;
        for (const auto &prefix : protectedPrefixes)
        {
            if (dirStr == prefix || (dirStr.size() > prefix.size() && dirStr.substr(0, prefix.size() + 1) == prefix + "/"))
            {
                isProtected = true;
                break;
            }
        }
        if (isProtected || isUserDesktop(dirToRemove))
        {
            kmlogger.error("Refused to remove protected or desktop directory: %s", dirStr.c_str());
            continue;
        }

        try
        {
            if (fs::exists(dirToRemove) && fs::is_directory(dirToRemove))
            {
                fs::remove_all(dirToRemove);
                kmlogger.info("Removed directory: %s", dirStr.c_str());
            }
            else
            {
                kmlogger.debug("Directory does not exist or is not a directory: %s", dirStr.c_str());
            }
        }
        catch (const std::exception &e)
        {
            kmlogger.error("Failed to remove directory: %s : %s", dirStr.c_str(), e.what());
        }
    }
}

void KMHostAppDeploy::startServices()
{
    for (const auto &sysgroup : d->m_vecSysGroup)
    {
        if (sysgroup.empty())
        {
            KMDebug("Empty system group found, skipping...");
            continue;
        }

        std::ostringstream sysGroupCommand;
        sysGroupCommand << "addgroup --system " << sysgroup;

        int result = system(sysGroupCommand.str().c_str());
        if (result != 0)
        {
            KMError("Failed to add system group: " + sysgroup + " (exit code: " + std::to_string(result) + ")");
        }
        else
        {
            KMInfo("Successfully add system group: " + sysgroup);
        }
    }

    for (const auto &service : d->m_vecService)
    {
        if (service.empty())
        {
            KMDebug("Empty service name found, skipping...");
            continue;
        }

        int reloadResult = system("systemctl daemon-reload");
        if (reloadResult != 0)
        {
            KMError("Failed to reload systemd daemon (exit code: " + std::to_string(reloadResult) + ")");
            continue;
        }

        std::ostringstream serviceCommand;
        serviceCommand << "systemctl enable --now " << service;

        int result = system(serviceCommand.str().c_str());
        if (result != 0)
        {
            KMError("Failed to enable and start service: " + service + " (exit code: " + std::to_string(result) + ")");
        }
        else
        {
            KMInfo("Successfully enabled and started service: " + service);
        }
    }
}

void KMHostAppDeploy::writeSynchronizedFileList()
{
    fs::path synchronizedPath = d->m_infoPath / (d->m_downloadRefId + ".hostlist");
    if (!fs::exists(d->m_infoPath))
    {
        if (!fs::create_directories(d->m_infoPath))
        {
            kmlogger.error("Failed to create directory: %s", d->m_infoPath.string().c_str());
            return;
        }
    }

    std::ofstream listFile(synchronizedPath, std::ios::out | std::ios::trunc);
    if (!listFile)
    {
        kmlogger.error("Failed to open file: %s", synchronizedPath.string().c_str());
        return;
    }

    for (const auto &file : d->m_SynchronizedFiles)
    {
        listFile << file << "\n";
    }
}

void KMHostAppDeploy::uninstall(TaskType type)
{
    if (d->m_hostScript)
    {
        try
        {
            std::string basePath = "/opt/kaiming/exports/share/hostscript/" + d->m_downloadRefId + "/";
            std::string action = (type == TaskType::TASK_TYPE_UPGRADE) ? "upgrade" : "uninstall";
            executeScript(basePath + "prerm", action);
        }
        catch (const std::exception &e)
        {
            kmlogger.error("Execute prerm script error: %s", e.what());
        }
    }

    fs::path synchronizedPath = d->m_infoPath / (d->m_downloadRefId + ".hostlist");
    // 读取已安装文件列表
    std::set<std::string> installedFiles = readSynchronizedFileList(synchronizedPath);

    if (installedFiles.empty())
    {
        KMInfo("No installed files found or file list is empty.");
        return;
    }

    static const std::set<std::string> excludedDirs = {"/usr/", "/var/", "/lib"};

    for (const auto &file : installedFiles)
    {
        fs::path filePath = file;
        std::string filePathStr = filePath.string();

        // 检查文件是否在排除目录中
        if (shouldSkipFile(filePathStr, excludedDirs))
        {
            KMInfo("Skipping protected directory file: " + filePathStr);
            continue;
        }

        try
        {
            if (fs::exists(filePath))
            {
                fs::remove(filePath);
                KMInfo("Deleted file: " + filePath.string());
            }
        }
        catch (const std::exception &e)
        {
            kmlogger.error("Failed to delete file: %s - %s", filePathStr.c_str(), e.what());
        }
    }

    removeEmptyDirs(installedFiles);
    handleUninstallKills();
    handleStopServices();
    handleRmdir();

    try
    {
        if (fs::exists(synchronizedPath))
        {
            fs::remove(synchronizedPath);
            KMInfo("Deleted synchronized file list: " + synchronizedPath.string());
        }
    }
    catch (const std::exception &e)
    {
        KMError("Failed to delete synchronized file list: " + std::string(e.what()));
    }
}

void KMHostAppDeploy::removeEmptyDirs(const std::set<std::string> &files)
{
    for (const auto &file : files)
    {
        fs::path dir = fs::path(file).parent_path();
        while (dir != "/" && fs::exists(dir) && fs::is_empty(dir))
        {
            fs::remove(dir);
            KMDebug("Removed empty dir: " + dir.string());
            dir = dir.parent_path();
        }
    }
}

std::set<std::string> KMHostAppDeploy::readSynchronizedFileList(const fs::path &listPath)
{
    std::set<std::string> fileSet;

    std::ifstream listFile(listPath);
    if (!listFile)
    {
        kmlogger.error("Failed to open synchronized file list: %s", listPath.string().c_str());
        return fileSet;
    }

    std::string line;
    while (std::getline(listFile, line))
    {
        fileSet.insert(line);
    }

    return fileSet;
}

void KMHostAppDeploy::handleKareDriver()
{
    fs::path sourceDir = d->m_versionDataPath / "files";
    if (fs::exists(sourceDir) && fs::is_directory(sourceDir))
    {
        for (const auto &entry : fs::directory_iterator(sourceDir))
        {
            if (entry.path().extension() == ".deb")
            {
                const fs::path targetDir = "/opt/kaiming/share/driver";
                fs::create_directories(targetDir);

                fs::path debFilePath = entry.path();
                fs::path targetFilePath = targetDir / debFilePath.filename();

                fs::copy(debFilePath, targetFilePath, fs::copy_options::overwrite_existing);
                d->m_SynchronizedFiles.insert(targetFilePath);
                kmlogger.info("Copy deb file: %s -> %s", debFilePath.string().c_str(), targetFilePath.string().c_str());

                std::string command = "kare -e v10sp1 -i " + targetFilePath.string();
                kmlogger.info("Executing kare: %s", command.c_str());

                int result = system(command.c_str());
                if (result != 0)
                {
                    kmlogger.error("Failed to execute kare command for file: %s", targetFilePath.string().c_str());
                }
                else
                {
                    kmlogger.info("Successfully installed deb package: %s", targetFilePath.string().c_str());
                }

                return;
            }
        }
    }
}

void KMHostAppDeploy::exportDesktopFile()
{
    fs::path sourceDir = d->m_versionDataPath / "files";
    const fs::path applicationsDir = sourceDir / "opt/system/resource/applications";
    const fs::path targetDir = KMFileUtils::isV11() ? "/opt/kaiming/share/applications/" : "/opt/kaiming/exports/share/applications/";

    if (!fs::exists(applicationsDir) || !fs::is_directory(applicationsDir))
    {
        return;
    }

    fs::create_directories(targetDir);

    for (const auto &entry : fs::directory_iterator(applicationsDir))
    {
        const auto &filePath = entry.path();

        if (!fs::is_regular_file(filePath) || filePath.extension() != ".desktop")
        {
            continue;
        }

        std::string content;
        std::ifstream inFile(filePath);
        if (!inFile)
            continue;

        content.assign(std::istreambuf_iterator<char>(inFile), std::istreambuf_iterator<char>());

        const size_t pos = content.find("[Desktop Entry]");
        if (pos != std::string::npos)
        {
            content.insert(pos + 15, "\nX-Kaiming=" + d->m_downloadRefId);
        }

        std::ofstream outFile(filePath, std::ios::trunc);
        if (!outFile)
            continue;

        outFile << content;
        outFile.close();

        const fs::path targetPath = targetDir / (d->m_downloadRefId + ".desktop");
        fs::rename(filePath, targetPath);
        d->m_SynchronizedFiles.insert(targetPath.string());

        KMPackageManager pkgManger;
        pkgManger.recordDesktopMd5(d->m_downloadRefId, targetPath.string(), d->m_SynchronizedFiles);
    }
}

void KMHostAppDeploy::executeScript(const std::string &scriptPath, const std::string &arg)
{
    struct stat fileStat;
    if (stat(scriptPath.c_str(), &fileStat) != 0)
    {
        throw std::runtime_error("Script not found: " + scriptPath);
    }

    if (chmod(scriptPath.c_str(), fileStat.st_mode | S_IXUSR) != 0)
    {
        throw std::runtime_error("Failed to make script executable: " + scriptPath);
    }

    pid_t pid = fork();
    if (pid < 0)
    {
        throw std::runtime_error("fork() failed");
    }

    if (pid == 0)
    {
        execl(scriptPath.c_str(), scriptPath.c_str(), arg.c_str(), (char *)nullptr);

        kmlogger.error("Failed to execute: %s - %s", scriptPath.c_str(), strerror(errno));
        exit(EXIT_FAILURE);
    }
    else
    {
        int status;
        if (waitpid(pid, &status, 0) == -1)
        {
            throw std::runtime_error("waitpid() failed for " + scriptPath);
        }

        if (WIFEXITED(status))
        {
            if (WEXITSTATUS(status) != 0)
            {
                throw std::runtime_error(scriptPath + " failed with exit code: " + std::to_string(WEXITSTATUS(status)));
            }
        }
        else
        {
            throw std::runtime_error(scriptPath + " terminated abnormally");
        }
    }
}
