#include "skillsmanager.h"

#include <DFileDialog>

#include <QDebug>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QLoggingCategory>
#include <QMimeDatabase>
#include <QMimeType>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcess>
#include <QRegularExpression>
#include <QSettings>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTextStream>
#include <QTimer>
#include <QUrl>

#ifdef signals
#    undef signals
#endif
#include <gio/gio.h>
#define signals Q_SIGNALS

#include <yaml-cpp/yaml.h>

// Qt5/6 compatibility for SkipEmptyParts
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
#    define SKIP_EMPTY_PARTS QString::SkipEmptyParts
#else
#    define SKIP_EMPTY_PARTS Qt::SkipEmptyParts
#endif

Q_LOGGING_CATEGORY(skillsManager, "uos.ai.skills")
DWIDGET_USE_NAMESPACE

namespace uos_ai {

namespace {
/* 技能状态配置文件路径 */
static const QString &skillsConfigPath()
{
    static const QString path = SkillsManager::skillsConfigFilePath();
    return path;
}

/* 文件 + 目录数量阈值，防止拷贝超大目录 */
static const int MAX_SKILL_ENTRIES = 200;

/* 文件夹总大小阈值：10MB */
static const qint64 MAX_SKILL_DIR_SIZE = 10 * 1024 * 1024;

/* URL 下载超时与重定向限制 */
static const int SKILL_DOWNLOAD_TIMEOUT_MS = 30000;
static const int MAX_SKILL_URL_REDIRECTS = 3;

/* 初始化默认技能路径配置 */
static void initializeDefaultPaths()
{
    QString configPath = skillsConfigPath();

    /* 如果配置文件已存在，无需初始化 */
    if (QFile::exists(configPath)) {
        return;
    }

    /* 确保配置目录存在 */
    QFileInfo fileInfo(configPath);
    QString configDir = fileInfo.path();
    QDir dir;
    if (!dir.exists(configDir)) {
        dir.mkpath(configDir);
    }

    /* 写入默认配置 */
    QSettings settings(configPath, QSettings::IniFormat);
    settings.beginGroup("paths");
    QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
    settings.setValue("path_claude", homePath + "/.claude/skills");
    settings.setValue("path_agents", homePath + "/.agents/skills");
    settings.endGroup();
    settings.sync();

    qCDebug(skillsManager) << "Initialized default skill paths configuration:" << configPath;
}

/* 默认预装技能路径（对所有用户有效） */
static const QString &defaultSkillsPath()
{
#ifdef UOS_AI_SKILLS_DEFAULT
    static const QString path = QStringLiteral(UOS_AI_SKILLS_DEFAULT);
#else
    /* 编译时未定义，使用默认路径 */
    static const QString path = QStringLiteral("/usr/lib/uos-ai-assistant/skills-default");
#endif
    return path;
}

/* 从配置文件读取自定义技能路径列表 */
static QStringList readCustomSkillPaths()
{
    initializeDefaultPaths();

    QSettings settings(skillsConfigPath(), QSettings::IniFormat);
    /* 读取 paths 节下的 path 键列表 */
    settings.beginGroup("paths");
    QStringList customPaths;
    for (const QString &key : settings.childKeys()) {
        if (key.startsWith("path")) {
            QString path = settings.value(key).toString();
            if (!path.isEmpty()) {
                customPaths.append(path);
            }
        }
    }
    settings.endGroup();
    return customPaths;
}

/**
 * @brief 两级排序：来源 + 技能名称
 * @return 排序后的技能列表
 *
 * 排序规则：
 * 1. 一级排序：builtin > uos-ai > 其他来源
 * 2. 二级排序：在同一来源内，按技能名称排序
 */
static QList<SkillInfo> sortSkills(const QList<SkillInfo> &skills)
{
    QList<SkillInfo> result = skills;

    std::sort(result.begin(), result.end(), [](const SkillInfo &a, const SkillInfo &b) {
        /* 一级排序：builtin > uos-ai > 其他来源 */
        auto getPriority = [](const QString &source) -> int {
            if (source == "builtin") return 0; /* 最高优先级：默认预装技能 */
            if (source == "uos-ai") return 1; /* 次高优先级：用户自定义技能 */
            return 2; /* 其他来源 */
        };

        int aPriority = getPriority(a.source);
        int bPriority = getPriority(b.source);

        if (aPriority != bPriority) {
            return aPriority < bPriority;
        }

        if (a.source != b.source) {
            /* 同优先级内，按来源名自然顺序 */
            return a.source < b.source;
        }

        /* 二级排序：同一来源内，按技能名称排序 */
        return a.name < b.name;
    });

    return result;
}

/**
 * @brief 检查路径是否为支持的压缩包格式
 */
static bool isArchiveFile(const QString &path)
{
    QMimeDatabase db;
    QMimeType mime = db.mimeTypeForFile(path, QMimeDatabase::MatchContent);
    return mime.inherits("application/zip") || mime.inherits("application/x-tar");
}

static bool validateSkillUrl(const QUrl &url, QString *errorMsg)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    if (!url.isValid() || url.host().isEmpty()) {
        setError(SkillsManager::tr("The skill URL is invalid."));
        return false;
    }

    const QString scheme = url.scheme().toLower();
    if (scheme != QLatin1String("http") && scheme != QLatin1String("https")) {
        setError(SkillsManager::tr("Only http and https skill URLs are supported."));
        return false;
    }

    if (!url.userInfo().isEmpty()) {
        setError(SkillsManager::tr("Skill URLs must not contain credentials."));
        return false;
    }

    return true;
}

static QString sanitizedSkillName(const QString &name)
{
    QString sanitized = name.trimmed();
    sanitized.replace(QRegularExpression(QStringLiteral("[^\\p{L}\\p{N}._-]+")), QStringLiteral("-"));
    sanitized = sanitized.trimmed();
    while (sanitized.startsWith(QLatin1Char('.')) || sanitized.startsWith(QLatin1Char('-')))
        sanitized.remove(0, 1);
    while (sanitized.endsWith(QLatin1Char('.')) || sanitized.endsWith(QLatin1Char('-')))
        sanitized.chop(1);
    return sanitized;
}

static QString parseSkillNameFromContent(const QString &content)
{
    if (!content.startsWith("---"))
        return QString();

    const int endPos = content.indexOf("---", 3);
    if (endPos <= 0)
        return QString();

    try {
        const YAML::Node node = YAML::Load(content.mid(3, endPos - 3).trimmed().toStdString());
        if (node["name"])
            return QString::fromStdString(node["name"].as<std::string>()).trimmed();
    } catch (const YAML::Exception &e) {
        qCWarning(skillsManager) << "YAML parse error:" << e.what();
    }
    return QString();
}

static bool hasUnsafeArchiveEntryName(const QString &entryName)
{
    QString portable = entryName.trimmed();
    portable.replace(QLatin1Char('\\'), QLatin1Char('/'));
    if (portable.isEmpty() || portable.startsWith(QLatin1Char('/')))
        return true;
    if (portable == QLatin1String("..") || portable.startsWith(QLatin1String("../"))
        || portable.contains(QLatin1String("/../")) || portable.endsWith(QLatin1String("/..")))
        return true;
    if (portable.contains(QRegularExpression(QStringLiteral("^[A-Za-z]:/"))))
        return true;

    const QString normalized = QDir::cleanPath(portable);
    return normalized.isEmpty() || normalized == QLatin1String(".") || normalized == QLatin1String("..");
}

static bool runArchiveListCommand(const QString &program, const QStringList &arguments, QString *output)
{
    QProcess process;
    process.start(program, arguments);
    if (!process.waitForFinished(30000)) {
        process.kill();
        return false;
    }
    if (process.exitCode() != 0)
        return false;
    if (output)
        *output = QString::fromUtf8(process.readAllStandardOutput());
    return true;
}

static bool validateZipEntries(const QString &archivePath, QString *errorMsg)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    QString namesOutput;
    if (!runArchiveListCommand(QStringLiteral("unzip"), QStringList { QStringLiteral("-Z1"), archivePath }, &namesOutput)) {
        setError(SkillsManager::tr("Failed to inspect the skill archive."));
        return false;
    }

    const QStringList entries = namesOutput.split(QLatin1Char('\n'), SKIP_EMPTY_PARTS);
    if (entries.isEmpty()) {
        setError(SkillsManager::tr("The skill archive is empty."));
        return false;
    }
    if (entries.size() > MAX_SKILL_ENTRIES) {
        setError(SkillsManager::tr("The skill contains too many files (maximum %1 allowed).").arg(MAX_SKILL_ENTRIES));
        return false;
    }
    for (const QString &entry : entries) {
        if (hasUnsafeArchiveEntryName(entry)) {
            setError(SkillsManager::tr("The skill archive contains an unsafe path."));
            return false;
        }
    }

    QString detailsOutput;
    if (!runArchiveListCommand(QStringLiteral("unzip"), QStringList { QStringLiteral("-Z"), QStringLiteral("-l"), archivePath }, &detailsOutput)) {
        setError(SkillsManager::tr("Failed to inspect the skill archive."));
        return false;
    }
    for (const QString &line : detailsOutput.split(QLatin1Char('\n'), SKIP_EMPTY_PARTS)) {
        const QString trimmed = line.trimmed();
        if (trimmed.isEmpty() || trimmed.startsWith(QLatin1String("Archive:"))
            || trimmed.startsWith(QLatin1String("Length")) || trimmed.startsWith(QLatin1String("----")))
            continue;
        const QChar typeChar = trimmed.at(0);
        if (typeChar == QLatin1Char('l') || typeChar == QLatin1Char('h') || typeChar == QLatin1Char('c')
            || typeChar == QLatin1Char('b') || typeChar == QLatin1Char('p') || typeChar == QLatin1Char('s')) {
            setError(SkillsManager::tr("The skill archive contains unsupported file types."));
            return false;
        }
    }
    return true;
}

static bool validateTarEntries(const QString &archivePath, QString *errorMsg)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    QString output;
    if (!runArchiveListCommand(QStringLiteral("tar"), QStringList { QStringLiteral("-tvf"), archivePath }, &output)) {
        setError(SkillsManager::tr("Failed to inspect the skill archive."));
        return false;
    }

    const QStringList lines = output.split(QLatin1Char('\n'), SKIP_EMPTY_PARTS);
    if (lines.isEmpty()) {
        setError(SkillsManager::tr("The skill archive is empty."));
        return false;
    }
    if (lines.size() > MAX_SKILL_ENTRIES) {
        setError(SkillsManager::tr("The skill contains too many files (maximum %1 allowed).").arg(MAX_SKILL_ENTRIES));
        return false;
    }

    for (const QString &line : lines) {
        const QString trimmed = line.trimmed();
        if (trimmed.isEmpty())
            continue;
        const QChar typeChar = trimmed.at(0);
        if (typeChar == QLatin1Char('l') || typeChar == QLatin1Char('h') || typeChar == QLatin1Char('c')
            || typeChar == QLatin1Char('b') || typeChar == QLatin1Char('p') || typeChar == QLatin1Char('s')) {
            setError(SkillsManager::tr("The skill archive contains unsupported file types."));
            return false;
        }

        const QStringList parts = trimmed.split(QRegularExpression(QStringLiteral("\\s+")), SKIP_EMPTY_PARTS);
        QString entryName = parts.size() > 5 ? parts.mid(5).join(QLatin1Char(' ')) : QString();
        const int linkMarker = entryName.indexOf(QStringLiteral(" -> "));
        if (linkMarker >= 0)
            entryName = entryName.left(linkMarker);
        if (hasUnsafeArchiveEntryName(entryName)) {
            setError(SkillsManager::tr("The skill archive contains an unsafe path."));
            return false;
        }
    }
    return true;
}

static bool validateArchiveEntries(const QString &archivePath, QString *errorMsg)
{
    QMimeDatabase db;
    QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchContent);
    if (mime.inherits("application/zip"))
        return validateZipEntries(archivePath, errorMsg);
    return validateTarEntries(archivePath, errorMsg);
}

static bool downloadUrlToFile(const QUrl &initialUrl, const QString &targetPath, QString *errorMsg)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    QUrl currentUrl = initialUrl;
    const bool originalHttps = currentUrl.scheme().toLower() == QLatin1String("https");
    for (int redirect = 0; redirect <= MAX_SKILL_URL_REDIRECTS; ++redirect) {
        if (!validateSkillUrl(currentUrl, errorMsg))
            return false;

        QFile targetFile(targetPath);
        if (!targetFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
            setError(SkillsManager::tr("Failed to create a temporary file for the skill download."));
            return false;
        }

        QNetworkAccessManager manager;
        QNetworkRequest request(currentUrl);
        request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
        request.setRawHeader("User-Agent", "uos-ai-skill-installer/1.0");

        QNetworkReply *reply = manager.get(request);
        QEventLoop loop;
        QTimer timer;
        timer.setSingleShot(true);
        qint64 totalBytes = 0;
        bool writeFailed = false;
        bool sizeExceeded = false;

        QObject::connect(reply, &QNetworkReply::readyRead, [&]() {
            while (!reply->atEnd()) {
                const QByteArray chunk = reply->read(64 * 1024);
                if (chunk.isEmpty())
                    break;
                totalBytes += chunk.size();
                if (totalBytes > MAX_SKILL_DIR_SIZE) {
                    sizeExceeded = true;
                    reply->abort();
                    loop.quit();
                    return;
                }
                if (targetFile.write(chunk) != chunk.size()) {
                    writeFailed = true;
                    reply->abort();
                    loop.quit();
                    return;
                }
            }
        });
        QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
        QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
        timer.start(SKILL_DOWNLOAD_TIMEOUT_MS);
        loop.exec();

        targetFile.close();
        if (!timer.isActive()) {
            reply->abort();
            reply->deleteLater();
            QFile::remove(targetPath);
            setError(SkillsManager::tr("The skill URL download timed out."));
            return false;
        }
        timer.stop();

        if (sizeExceeded) {
            reply->deleteLater();
            QFile::remove(targetPath);
            setError(SkillsManager::tr("The skill download exceeds the limit (maximum 10 MB allowed)."));
            return false;
        }
        if (writeFailed) {
            reply->deleteLater();
            QFile::remove(targetPath);
            setError(SkillsManager::tr("Failed to write the downloaded skill file."));
            return false;
        }

        const QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
        if (redirectTarget.isValid()) {
            QUrl redirectedUrl = currentUrl.resolved(redirectTarget.toUrl());
            reply->deleteLater();
            QFile::remove(targetPath);
            if (redirect >= MAX_SKILL_URL_REDIRECTS) {
                setError(SkillsManager::tr("The skill URL redirected too many times."));
                return false;
            }
            if (originalHttps && redirectedUrl.scheme().toLower() == QLatin1String("http")) {
                setError(SkillsManager::tr("The skill URL must not redirect from https to http."));
                return false;
            }
            currentUrl = redirectedUrl;
            continue;
        }

        const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        if (statusCode > 0 && (statusCode < 200 || statusCode >= 300)) {
            reply->deleteLater();
            QFile::remove(targetPath);
            setError(SkillsManager::tr("The skill URL returned HTTP status %1.").arg(statusCode));
            return false;
        }

        if (reply->error() != QNetworkReply::NoError) {
            const QString errorString = reply->errorString();
            reply->deleteLater();
            QFile::remove(targetPath);
            setError(SkillsManager::tr("Failed to download the skill URL: %1").arg(errorString));
            return false;
        }

        reply->deleteLater();
        if (totalBytes <= 0) {
            QFile::remove(targetPath);
            setError(SkillsManager::tr("The skill URL downloaded an empty file."));
            return false;
        }
        return true;
    }

    setError(SkillsManager::tr("The skill URL redirected too many times."));
    return false;
}

static bool prepareSkillMdDirectory(const QString &skillMdPath, const QString &targetRoot, QString *preparedPath, QString *errorMsg)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    QFile skillFile(skillMdPath);
    if (!skillFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        setError(SkillsManager::tr("Failed to read SKILL.md."));
        return false;
    }

    const QString content = QTextStream(&skillFile).readAll();
    skillFile.close();
    const QString skillName = sanitizedSkillName(parseSkillNameFromContent(content));
    if (skillName.isEmpty()) {
        setError(SkillsManager::tr("SKILL.md is missing the required 'name' field."));
        return false;
    }

    QDir rootDir(targetRoot);
    const QString skillDirPath = rootDir.filePath(skillName);
    if (!rootDir.mkpath(skillName)) {
        setError(SkillsManager::tr("Failed to prepare a temporary skill directory."));
        return false;
    }

    if (!QFile::copy(skillMdPath, QDir(skillDirPath).filePath(QStringLiteral("SKILL.md")))) {
        setError(SkillsManager::tr("Failed to prepare SKILL.md for installation."));
        return false;
    }

    if (preparedPath)
        *preparedPath = skillDirPath;
    return true;
}

static bool validateSkillTree(const QString &rootPath, QString *errorMsg)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    const QFileInfo rootInfo(rootPath);
    if (rootInfo.isSymLink() || !rootInfo.isDir()) {
        setError(SkillsManager::tr("The skill source contains unsupported file types."));
        return false;
    }

    const QString rootCanonical = rootInfo.canonicalFilePath();
    if (rootCanonical.isEmpty()) {
        setError(SkillsManager::tr("Failed to inspect the skill files."));
        return false;
    }

    QStringList dirsToCheck { rootPath };
    int entryCount = 0;
    qint64 totalSize = 0;
    while (!dirsToCheck.isEmpty()) {
        QDir currentDir(dirsToCheck.takeLast());
        const QFileInfoList entries = currentDir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden);
        for (const QFileInfo &entry : entries) {
            ++entryCount;
            if (entryCount > MAX_SKILL_ENTRIES) {
                setError(SkillsManager::tr("The skill contains too many files (maximum %1 allowed).").arg(MAX_SKILL_ENTRIES));
                return false;
            }

            if (hasUnsafeArchiveEntryName(QDir(rootPath).relativeFilePath(entry.absoluteFilePath()))) {
                setError(SkillsManager::tr("The skill archive contains an unsafe path."));
                return false;
            }

            if (entry.isSymLink() || (!entry.isFile() && !entry.isDir())) {
                setError(SkillsManager::tr("The skill archive contains unsupported file types."));
                return false;
            }

            const QString canonical = entry.canonicalFilePath();
            if (canonical.isEmpty() || !(canonical == rootCanonical || canonical.startsWith(rootCanonical + QLatin1Char('/')))) {
                setError(SkillsManager::tr("The skill archive contains files outside the extraction directory."));
                return false;
            }

            if (entry.isFile()) {
                totalSize += entry.size();
                if (totalSize > MAX_SKILL_DIR_SIZE) {
                    setError(SkillsManager::tr("The skill size exceeds the limit (maximum 10 MB allowed)."));
                    return false;
                }
            } else if (entry.isDir()) {
                dirsToCheck << entry.absoluteFilePath();
            }
        }
    }
    return true;
}

/**
 * @brief 调用系统命令解压压缩包到指定目录
 * @return true 解压成功，false 超时或命令失败
 */
static bool extractArchive(const QString &archivePath, const QString &targetDir)
{
    QMimeDatabase db;
    QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchContent);

    QProcess process;
    if (mime.inherits("application/zip")) {
        process.start("unzip", QStringList() << "-q" << archivePath << "-d" << targetDir);
    } else {
        process.start("tar", QStringList() << "-xf" << archivePath << "-C" << targetDir);
    }
    if (!process.waitForFinished(30000)) {
        qWarning() << "Archive extraction timed out:" << archivePath;
        return false;
    }
    if (process.exitCode() != 0) {
        qWarning() << "Archive extraction failed:" << process.readAllStandardError();
        return false;
    }
    return true;
}

/**
 * @brief 在解压根目录中定位技能目录
 *
 * 优先深度1：寻找含 SKILL.md 的子目录（zip -r skill.zip skill/ 形式）
 * 降级深度0：若 SKILL.md 直接在解压根目录（zip 打包内容而非目录）
 *
 * @return 技能目录的绝对路径，未找到则返回空字符串
 */
static QString findSkillDirInExtracted(const QString &extractedRoot)
{
    /* 深度1：查找含 SKILL.md 的子目录 */
    QDir rootDir(extractedRoot);
    QFileInfoList subdirs = rootDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
    for (const QFileInfo &subdirInfo : subdirs) {
        QString skillMdPath = subdirInfo.absoluteFilePath() + "/SKILL.md";
        if (QFile::exists(skillMdPath)) {
            return subdirInfo.absoluteFilePath();
        }
    }

    /* 深度0：SKILL.md 直接在解压根目录 */
    if (QFile::exists(extractedRoot + "/SKILL.md")) {
        return extractedRoot;
    }

    return QString();
}

/**
 * @brief 解析结果：持有技能源目录、最终技能名称以及（若有）临时解压目录
 *
 * isValid() 为 false 时表示解析失败。
 */
struct SkillSource
{
    QString actualPath; /* 技能目录的绝对路径 */
    QString skillName; /* 最终技能名称（目录名或压缩包名） */
    std::unique_ptr<QTemporaryDir> tempDirHandle; /* 临时解压目录生命周期管理（非压缩包场景为空） */

    SkillSource() = default;
    SkillSource(const SkillSource &) = delete;
    SkillSource &operator=(const SkillSource &) = delete;
    SkillSource(SkillSource &&) = default;
    SkillSource &operator=(SkillSource &&) = default;

    bool isValid() const { return !actualPath.isEmpty(); }
};

/**
 * @brief 从压缩包解析技能源
 *
 * 解压到临时目录，定位含 SKILL.md 的技能目录，推导技能名称。
 */
static SkillSource resolveFromArchive(const QString &path, QString *errorMsg)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    SkillSource src;

    QFileInfo archiveInfo(path);
    if (!archiveInfo.exists() || !archiveInfo.isFile()) {
        qCWarning(skillsManager) << "Archive file does not exist:" << path;
        setError(SkillsManager::tr("The archive file does not exist."));
        return src;
    }

    src.tempDirHandle = std::make_unique<QTemporaryDir>();
    if (!src.tempDirHandle->isValid()) {
        qCWarning(skillsManager) << "Failed to create temp dir:" << src.tempDirHandle->errorString();
        setError(SkillsManager::tr("Failed to create temporary directory for extraction."));
        return src;
    }

    const QString tempPath = src.tempDirHandle->path();

    if (!validateArchiveEntries(path, errorMsg))
        return src;

    if (!extractArchive(path, tempPath)) {
        qCWarning(skillsManager) << "Failed to extract archive:" << path;
        setError(SkillsManager::tr("Failed to extract the archive. The file may be corrupted or the format is not supported."));
        return src;
    }

    if (!validateSkillTree(tempPath, errorMsg))
        return src;

    QString foundDir = findSkillDirInExtracted(tempPath);
    if (foundDir.isEmpty()) {
        qCWarning(skillsManager) << "No SKILL.md found in archive:" << path;
        setError(SkillsManager::tr("No SKILL.md file found in the archive."));
        return src;
    }

    src.actualPath = foundDir;
    if (foundDir == tempPath) {
        /* SKILL.md 直接在解压根目录：用压缩包文件名作技能名 */
        src.skillName = archiveInfo.baseName();
        if (src.skillName.endsWith(".tar", Qt::CaseInsensitive))
            src.skillName.chop(4);
    } else {
        src.skillName = QDir(foundDir).dirName();
    }

    return src;
}

/**
 * @brief 从普通文件（SKILL.md）或目录解析技能源
 */
static SkillSource resolveFromPath(const QString &path, QString *errorMsg)
{
    SkillSource src;
    QFileInfo fi(path);

    if (fi.isFile()) {
        src.actualPath = fi.path();
    } else if (fi.isDir()) {
        src.actualPath = fi.filePath();
    } else {
        qCWarning(skillsManager) << "Invalid path type:" << path;
        if (errorMsg)
            *errorMsg = SkillsManager::tr("The specified path is invalid.");
        return src;
    }

    src.skillName = QDir(src.actualPath).dirName();
    return src;
}

/**
 * @brief 将用户提供的路径解析为 SkillSource
 */
static SkillSource resolveSkillPath(const QString &path, QString *errorMsg)
{
    if (isArchiveFile(path))
        return resolveFromArchive(path, errorMsg);
    return resolveFromPath(path, errorMsg);
}

}   // end anonymous namespace

/* ========== SkillInfo 实现 ========== */

void SkillInfo::loadDetails() const
{
    m_detailsLoaded = true;

    if (path.isEmpty()) {
        qCWarning(skillsManager) << "Cannot load details: path is empty";
        m_details = QString();
        return;
    }

    /* 查找技能文件 */
    QString skillFilePath = path + "/SKILL.md";
    QFile skillFile(skillFilePath);

    if (!skillFile.exists()) {
        /* 尝试查找其他 .md 文件 */
        QDir dir(path);
        QStringList mdFiles = dir.entryList(QStringList() << "*.md", QDir::Files);
        if (!mdFiles.isEmpty()) {
            skillFilePath = path + "/" + mdFiles.first();
            skillFile.setFileName(skillFilePath);
        } else {
            qCWarning(skillsManager) << "No skill file found in:" << path;
            m_details = QString();
            return;
        }
    }

    if (!skillFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qCWarning(skillsManager) << "Failed to open skill file:" << skillFilePath;
        m_details = QString();
        return;
    }

    QString content;
    QTextStream in(&skillFile);
    content = in.readAll();
    skillFile.close();

    /* 提取 details 部分（去除 front matter） */
    if (content.startsWith("---")) {
        int endPos = content.indexOf("---", 3);
        if (endPos > 0) {
            m_details = content.mid(endPos + 3).trimmed();
            return;
        }
    }

    /* 没有 front matter，返回全部内容 */
    m_details = content.trimmed();
}

void SkillInfo::loadFiles() const
{
    m_filesLoaded = true;

    if (path.isEmpty()) {
        qCWarning(skillsManager) << "Cannot load files: path is empty";
        m_files.clear();
        return;
    }

    QDir dir(path);
    if (!dir.exists()) {
        qCWarning(skillsManager) << "Skill directory does not exist:" << path;
        m_files.clear();
        return;
    }

    /* 递归遍历目录，收集文件列表 */
    m_files.clear();
    collectFiles(dir);
}

void SkillInfo::collectFiles(const QDir &dir) const
{
    /* 先处理文件 */
    QFileInfoList fileInfos = dir.entryInfoList(QDir::Files);
    for (const QFileInfo &fileInfo : fileInfos) {
        QString fileName = fileInfo.fileName();
        /* 排除 LICENSE 系列文件（不区分大小写）和 README 系列文件（不区分大小写） */
        if (fileName.startsWith("LICENSE", Qt::CaseInsensitive) || fileName.startsWith("README", Qt::CaseInsensitive) || fileName.compare("SKILL.md", Qt::CaseInsensitive) == 0) {
            continue;
        }
        /* 添加完整绝对路径 */
        m_files.append(fileInfo.absoluteFilePath());
    }

    /* 递归处理子目录 */
    QFileInfoList dirInfos = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
    for (const QFileInfo &dirInfo : dirInfos) {
        QDir subdir(dirInfo.absoluteFilePath());
        collectFiles(subdir);
    }
}

/* ========== SkillsManager 实现 ========== */

SkillsManager::SkillsManager(QObject *parent)
    : QObject(parent)
{
    reloadSkills();
    setupFileWatcher();
}

SkillsManager::~SkillsManager() {}

QString SkillsManager::prompt() const
{
    QString result;
    QTextStream stream(&result);

    QList<SkillInfo> enabled = enabledSkills();

    stream << "# AGENTS\n\n";
    stream << "<skills_system priority=\"1\">\n\n";
    stream << "## Available Skills\n\n";
    stream << "<!-- SKILLS_TABLE_START -->\n";
    stream << "<usage>\n";
    stream << "When users ask you to perform tasks, check if any of the available "
              "skills below can help complete the task more effectively. Skills "
              "provide specialized capabilities and domain knowledge.\n\n";
    stream << "How to use skills:\n";
    stream << "- Review the skill descriptions in <available_skills> below\n";
    stream << "- Check if a skill matches the user's request or task context\n";
    stream << "- Apply the skill's instructions and guidelines to complete the "
              "task\n";
    stream << "- Each skill contains detailed instructions on usage and best "
              "practices\n\n";
    stream << "Usage notes:\n";
    stream << "- Only use skills listed in <available_skills> below\n";
    stream << "- Skills are already loaded in your context, no invocation "
              "needed\n";
    stream << "- Apply skill knowledge directly when relevant to the task\n";
    stream << "</usage>\n\n";

    stream << "<available_skills>\n\n";

    if (enabled.isEmpty()) {
        stream << "<!-- No skills are currently enabled -->\n";
    } else {
        for (const auto &skill : enabled) {
            stream << "<skill>\n";
            stream << "<name>" << skill.name << "</name>\n";

            if (!skill.description.isEmpty()) {
                stream << "<description>" << skill.description << "</description>\n";
            }

            //   if (!skill.details.isEmpty()) {
            //     stream << "<details>\n";
            //     stream << skill.details << "\n";
            //     stream << "</details>\n";
            //   }

            stream << "</skill>\n\n";
        }
    }

    stream << "</available_skills>\n";
    stream << "<!-- SKILLS_TABLE_END -->\n\n";
    stream << "</skills_system>\n";

    return result;
}

SkillInfo SkillsManager::readSkill(const QString &skillName) const
{
    return m_skills.value(skillName, {});
}

QList<SkillInfo> SkillsManager::skills() const
{
    return sortSkills(m_skills.values());
}

QString SkillsManager::userSkillInstallPath()
{
    return QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QStringLiteral("/.uos-ai/skills"));
}

QString SkillsManager::skillsConfigFilePath()
{
    return QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/deepin/uos-ai-assistant/skills.conf"));
}

QStringList SkillsManager::allSkillSearchPaths()
{
    return skillPaths();
}

QString SkillsManager::builtinSkillPath()
{
    return QDir::cleanPath(defaultSkillsPath());
}

QJsonObject SkillsManager::skillPathsData() const
{
    QJsonObject result;
    result[QStringLiteral("install_path")] = userSkillInstallPath();
    result[QStringLiteral("config_path")] = skillsConfigFilePath();
    result[QStringLiteral("builtin_path")] = builtinSkillPath();

    QJsonArray searchPaths;
    const QStringList paths = allSkillSearchPaths();
    for (const QString &path : paths)
        searchPaths.append(path);
    result[QStringLiteral("search_paths")] = searchPaths;

    return result;
}

QJsonArray SkillsManager::skillsData() const
{
    QJsonArray result;
    const auto skills = sortSkills(m_skills.values());

    for (const auto &skill : skills) {
        QJsonObject obj;
        obj["name"] = skill.name;
        obj["description"] = skill.description;
        obj["path"] = skill.path;
        obj["source"] = skill.source;
        obj["enabled"] = skill.enabled;
        result.append(obj);
    }

    return result;
}

QList<SkillInfo> SkillsManager::enabledSkills() const
{
    QList<SkillInfo> result;
    for (const auto &skill : m_skills) {
        if (skill.enabled) {
            result.append(skill);
        }
    }
    return sortSkills(result);
}

QJsonArray SkillsManager::enabledSkillsData() const
{
    QJsonArray result;
    QList<SkillInfo> enabledSkillsList;

    for (const auto &skill : m_skills) {
        if (skill.enabled) {
            enabledSkillsList.append(skill);
        }
    }

    const auto sorted = sortSkills(enabledSkillsList);
    for (const auto &skill : sorted) {
        QJsonObject obj;
        obj["name"] = skill.name;
        obj["description"] = skill.description;
        obj["path"] = skill.path;
        obj["source"] = skill.source;
        obj["enabled"] = skill.enabled;
        result.append(obj);
    }

    return result;
}

bool SkillsManager::setSkillEnabled(const QString &skillName, bool enabled)
{
    if (!m_skills.contains(skillName)) {
        return false;
    }

    if (m_skills[skillName].enabled == enabled) {
        /* 状态未改变，无需保存 */
        return true;
    }

    m_skills[skillName].enabled = enabled;

    /* 保存到统一配置文件 */
    QSettings settings(skillsConfigPath(), QSettings::IniFormat);
    /* 键名格式: source/skillName/enabled */
    QString source = m_skills[skillName].source;
    settings.setValue(source + "/" + skillName + "/enabled", enabled);
    settings.sync(); /* 立即写入磁盘 */

    Q_EMIT skillsUpdated();
    return true;
}

// todo 异步加载
void SkillsManager::reloadSkills()
{
    m_skills.clear();

    QStringList paths = skillPaths();
    for (const QString &path : paths) {
        loadSkillsFromDirectory(path);
    }

    Q_EMIT skillsUpdated();
}

bool SkillsManager::hasSkill(const QString &skillName) const
{
    return m_skills.contains(skillName);
}

QStringList SkillsManager::skillPaths()
{
    QStringList paths;

    /* 去重：规范化后保序去重，确保 uos-ai 路径始终最先加载 */
    QStringList result;
    QSet<QString> seen;

    /* 优先级1：用户自定义路径（优先级最高，可覆盖其他来源） */
    QString cleanUserPath = userSkillInstallPath();
    if (!seen.contains(cleanUserPath)) {
        seen.insert(cleanUserPath);
        result.append(cleanUserPath);
    }

    /* 优先级2：配置文件的自定义路径 */
    QStringList customPaths = readCustomSkillPaths();
    for (const QString &path : customPaths) {
        QString clean = QDir::cleanPath(path);
        if (!seen.contains(clean)) {
            seen.insert(clean);
            result.append(clean);
        }
    }

    /* 优先级3：默认预装路径（优先级最低，被所有用户路径覆盖） */
    QString defaultPath = defaultSkillsPath();
    QString cleanDefaultPath = QDir::cleanPath(defaultPath);
    if (!seen.contains(cleanDefaultPath)) {
        seen.insert(cleanDefaultPath);
        result.append(cleanDefaultPath);
    }

    return result;
}

void SkillsManager::loadSkillsFromDirectory(const QString &directory)
{
    QDir baseDir(directory);
    if (!baseDir.exists()) {
        /* Only uos-ai skills directory should be auto-created */
        if (QDir::cleanPath(directory) == userSkillInstallPath()) {
            baseDir.mkpath(".");
            qCWarning(skillsManager) << "Skills directory does not exist, created:" << directory;
        } else {
            qCWarning(skillsManager) << "Skills directory does not exist:" << directory;
        }
        return;
    }

    /* 从目录路径中提取来源 (uos-ai, claude, builtin, etc.) */
    QString source = "unknown";

    /* 检查是否是默认预装路径，优先标识为 builtin */
    QString cleanDir = QDir::cleanPath(directory);
    if (cleanDir == QDir::cleanPath(defaultSkillsPath())) {
        source = "builtin";
    } else {
        /* 普通路径逻辑：从目录结构提取来源 */
        QDir pathDir(directory);
        QString dirName = pathDir.dirName();   // "skills"
        if (dirName == "skills") {
            pathDir.cdUp();   // 上一级目录
            QString parentName = pathDir.dirName();   // ".claude"、".uos-ai" 或自定义路径的父目录名
            if (parentName.startsWith(".")) {
                source = parentName.mid(1);   // 去掉前面的点号，如 "claude"、"uos-ai"
            } else {
                source = parentName; /* 直接使用父目录名 */
            }
        }

        /* 防撞：防止用户自定义路径被误识别为 builtin 或 uos-ai */
        QString uosAiSkillsPath = userSkillInstallPath();
        if ((source == "builtin" && cleanDir != QDir::cleanPath(defaultSkillsPath())) || (source == "uos-ai" && cleanDir != uosAiSkillsPath)) {
            /* 不是真正的系统路径，重命名为 xxx-custom */
            source = source + "-custom";
        }
    }

    /* 遍历技能目录下的子目录，每个子目录代表一个技能 */
    QFileInfoList subdirs =
            baseDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);

    for (const QFileInfo &subdirInfo : subdirs) {
        QString skillDir = subdirInfo.absoluteFilePath();
        QString skillName = subdirInfo.fileName();

        /* 查找子目录中的 SKILL.md 文件 */
        QFile skillFile(skillDir + "/SKILL.md");
        if (!skillFile.exists()) {
            /* 如果没有 SKILL.md，尝试查找任意 .md 文件 */
            QDir dir(skillDir);
            QStringList mdFiles = dir.entryList(QStringList() << "*.md", QDir::Files);
            if (!mdFiles.isEmpty()) {
                skillFile.setFileName(skillDir + "/" + mdFiles.first());
            } else {
                qCWarning(skillsManager) << "No skill file found in directory:" << skillDir;
                continue;
            }
        }

        if (!skillFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
            qCWarning(skillsManager) << "Failed to open skill file:" << skillFile.fileName();
            continue;
        }

        QString content;
        QTextStream in(&skillFile);
        content = in.readAll().trimmed();
        skillFile.close();

        /* 先解析 SKILL.md 获取真实的 skill name */
        SkillInfo skill;
        skill.path = skillDir; /* 技能所在目录 */
        skill.source = source; /* 从目录路径提取的来源 */

        parseSkillFrontMatter(content, skill);

        /* name 字段缺失，跳过非标准 skill */
        if (skill.name.isEmpty()) {
            qCWarning(skillsManager) << "Skill name not defined in front matter, skipping:" << skillDir;
            continue;
        }

        /* 如果已存在同名技能（来自优先级更高的路径），跳过 */
        if (m_skills.contains(skill.name)) {
            qCDebug(skillsManager) << "Skill already exists, skipping:" << skill.name
                                   << "from" << source;
            continue;
        }

        /* 从统一配置文件读取启用状态，默认启用 */
        QSettings settings(skillsConfigPath(), QSettings::IniFormat);
        skill.enabled = settings.value(source + "/" + skill.name + "/enabled", true).toBool();

        m_skills[skill.name] = skill;
    }
}

void SkillsManager::setupFileWatcher()
{
    QStringList paths = skillPaths();
    for (const QString &path : paths) {
        QDir dir(path);
        if (dir.exists()) {
            m_watcher.removePath(path);
            m_watcher.addPath(path);
        }
    }

    connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this,
            &SkillsManager::reloadSkills);
}

void SkillsManager::parseSkillFrontMatter(const QString &content,
                                          SkillInfo &skill)
{
    if (!content.startsWith("---")) {
        /* 没有 front matter，使用目录名作为 description */
        /* details 不再在此加载，改为按需读取 */
        skill.description = skill.name.isEmpty() ? "Unknown skill" : skill.name;
        return;
    }

    int endPos = content.indexOf("---", 3);
    if (endPos > 0) {
        /* 解析 YAML front matter */
        QString frontMatter = content.mid(3, endPos - 3).trimmed();
        /* details 不再在此加载到内存 */

        parseYamlFrontMatter(frontMatter, skill);
    } else {
        /* 只有开头 --- 没有结束，按无 front matter 处理 */
        skill.description = skill.name.isEmpty() ? "Unknown skill" : skill.name;
    }

    /* 如果 description 为空，使用 name */
    if (skill.description.isEmpty()) {
        skill.description = skill.name;
    }
}

void SkillsManager::parseYamlFrontMatter(const QString &frontMatter,
                                         SkillInfo &skill)
{
    try {
        YAML::Node node = YAML::Load(frontMatter.toStdString());

        /* 解析 name 字段 */
        if (node["name"]) {
            skill.name = QString::fromStdString(node["name"].as<std::string>());
        }

        /* 解析 description 字段 */
        if (node["description"]) {
            skill.description = QString::fromStdString(node["description"].as<std::string>());
        }
    } catch (const YAML::Exception &e) {
        qCWarning(skillsManager) << "YAML parse error:" << e.what();
    }
}

/**
 * @brief 检查路径是否在危险的黑名单目录中
 */
static bool isDangerousPath(const QString &path, const QString &homePath)
{
    QString normalizedPath = QDir::cleanPath(path);

    /* 根目录 */
    if (normalizedPath == "/" || normalizedPath.isEmpty()) {
        return true;
    }

    /* 系统关键目录 */
    const QStringList systemDirs = { "/etc", "/usr", "/var", "/tmp", "/sys", "/proc", "/dev", "/run", "/boot", "/home" };
    for (const QString &sysDir : systemDirs) {
        if (normalizedPath == sysDir) {
            return true;
        }
    }

    /* 用户主目录本身 */
    if (normalizedPath == homePath) {
        return true;
    }

    /* ~/.uos-ai 及其子目录（防止循环嵌套） */
    if (normalizedPath.startsWith(homePath + "/.uos-ai")) {
        return true;
    }

    return false;
}

/**
 * @brief 统计目录中的文件和子目录数量（限制递归深度）
 */
static int countDirectoryEntries(const QString &dirPath)
{
    QDir dir(dirPath);
    QFileInfoList entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries);

    int count = 0;
    for (const QFileInfo &entry : entries) {
        count++;
        if (entry.isDir()) {
            /* 递归统计子目录（限制递归深度） */
            count += countDirectoryEntries(entry.absoluteFilePath());
            /* 如果已经超过阈值，提前返回 */
            if (count > MAX_SKILL_ENTRIES) {
                return count;
            }
        }
    }
    return count;
}

/**
 * @brief 统计目录的总大小（迭代遍历所有子目录）
 * @return 目录总大小（字节）
 */
static qint64 calculateDirectorySize(const QString &dirPath)
{
    qint64 totalSize = 0;
    QStringList dirsToCheck;
    dirsToCheck << dirPath;

    while (!dirsToCheck.isEmpty()) {
        QString currentPath = dirsToCheck.takeLast();
        QDir currentDir(currentPath);
        QFileInfoList entries = currentDir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries);

        for (const QFileInfo &entry : entries) {
            if (entry.isFile()) {
                totalSize += entry.size();
            } else if (entry.isDir()) {
                /* 将子目录加入待遍历列表 */
                dirsToCheck << entry.absoluteFilePath();
            }
        }
    }
    return totalSize;
}

bool SkillsManager::validateSkillMd(const QString &actualPath, QString *errorMsg, QString *outName)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    QFile skillFile(actualPath + "/SKILL.md");
    if (!skillFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qCWarning(skillsManager) << "Failed to open SKILL.md for validation:" << actualPath;
        setError(tr("Failed to read SKILL.md."));
        return false;
    }
    QString content = QTextStream(&skillFile).readAll().trimmed();
    skillFile.close();

    SkillInfo tempSkill;
    parseSkillFrontMatter(content, tempSkill);
    if (tempSkill.name.isEmpty()) {
        qCWarning(skillsManager) << "SKILL.md is missing required 'name' field in:" << actualPath;
        setError(tr("SKILL.md is missing the required 'name' field."));
        return false;
    }

    if (outName)
        *outName = tempSkill.name;

    return true;
}

bool SkillsManager::addSkillFromUrl(const QString &url, QString *errorMsg, QString *outSkillName)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    const QUrl parsedUrl(url.trimmed());
    if (!validateSkillUrl(parsedUrl, errorMsg))
        return false;

    QTemporaryDir tempDir;
    if (!tempDir.isValid()) {
        setError(tr("Failed to create temporary directory for the skill download."));
        return false;
    }

    const QString urlFileName = QFileInfo(parsedUrl.path()).fileName();
    const QString downloadName = urlFileName.isEmpty() ? QStringLiteral("downloaded-skill") : urlFileName;
    const QString downloadedPath = QDir(tempDir.path()).filePath(downloadName);
    if (!downloadUrlToFile(parsedUrl, downloadedPath, errorMsg))
        return false;

    if (isArchiveFile(downloadedPath))
        return addSkill(downloadedPath, errorMsg, outSkillName);

    QFile skillFile(downloadedPath);
    if (skillFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        const QString prefix = QString::fromUtf8(skillFile.read(16));
        skillFile.close();
        if (prefix.trimmed().startsWith(QStringLiteral("---"))) {
            QString preparedPath;
            if (!prepareSkillMdDirectory(downloadedPath, tempDir.path(), &preparedPath, errorMsg))
                return false;
            return addSkill(preparedPath, errorMsg, outSkillName);
        }
    }

    setError(tr("The URL does not point to a supported SKILL.md file or skill archive."));
    return false;
}

bool SkillsManager::addSkillSource(const QString &source, QString *errorMsg, QString *outSkillName)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    const QString trimmedSource = source.trimmed();
    if (trimmedSource.isEmpty()) {
        setError(tr("Skill source cannot be empty."));
        return false;
    }

    const QUrl sourceUrl(trimmedSource);
    const QString scheme = sourceUrl.scheme().toLower();
    if (scheme == QLatin1String("http") || scheme == QLatin1String("https"))
        return addSkillFromUrl(trimmedSource, errorMsg, outSkillName);

    if (sourceUrl.isValid() && !scheme.isEmpty()) {
        if (scheme == QLatin1String("file"))
            setError(tr("Use a local file path instead of a file URL."));
        else
            setError(tr("Only http and https skill URLs are supported."));
        return false;
    }

    return addSkill(trimmedSource, errorMsg, outSkillName);
}

bool SkillsManager::addSkill(const QString &path, QString *errorMsg, QString *outSkillName)
{
    auto setError = [&](const QString &msg) {
        if (errorMsg)
            *errorMsg = msg;
    };

    /* 解析路径 → SkillSource */
    SkillSource src = resolveSkillPath(path, errorMsg);
    if (!src.isValid())
        return false;

    QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
    QDir sourceDir(src.actualPath);

    /* 验证技能目录存在 */
    if (!sourceDir.exists()) {
        qCWarning(skillsManager) << "Skill folder does not exist:" << src.actualPath;
        setError(tr("The skill directory does not exist."));
        return false;
    }

    /* 安全检查 1：路径黑名单检查 */
    if (isDangerousPath(QDir::cleanPath(src.actualPath), homePath)) {
        qCWarning(skillsManager) << "Refused to add skill from dangerous path:" << src.actualPath;
        setError(tr("The skill cannot be imported from this path."));
        return false;
    }

    /* 校验 SKILL.md 文件必须存在 */
    if (!sourceDir.exists("SKILL.md")) {
        qCWarning(skillsManager) << "SKILL.md file not found in:" << src.actualPath;
        setError(tr("No SKILL.md file found in the skill directory."));
        return false;
    }

    /* 校验 SKILL.md 内容与跨来源冲突 */
    if (!validateSkillMd(src.actualPath, errorMsg, outSkillName))
        return false;

    if (!validateSkillTree(src.actualPath, errorMsg))
        return false;

    /* 安全检查 2：目录内容预检（防止拷贝超大目录） */
    int entryCount = countDirectoryEntries(src.actualPath);
    if (entryCount > MAX_SKILL_ENTRIES) {
        qCWarning(skillsManager) << "Skill directory too large (" << entryCount << " entries), max allowed:" << MAX_SKILL_ENTRIES;
        setError(tr("The skill contains too many files (maximum %1 allowed).").arg(MAX_SKILL_ENTRIES));
        return false;
    }

    /* 安全检查 3：检查文件夹总大小（>=10MB） */
    qint64 dirSize = calculateDirectorySize(src.actualPath);
    if (dirSize > MAX_SKILL_DIR_SIZE) {
        qCWarning(skillsManager) << "Cannot add skill: directory size too large (" << dirSize / (1024.0 * 1024.0) << " MB), max allowed is 10 MB";
        setError(tr("The skill size exceeds the limit (maximum 10 MB allowed)."));
        return false;
    }

    if (src.skillName.isEmpty()) {
        qCWarning(skillsManager) << "Invalid skill folder name";
        setError(tr("The skill folder name is invalid."));
        return false;
    }

    /* 获取目标路径: ~/.uos-ai/skills/<skillName>/ */
    QString targetDirPath = QDir(userSkillInstallPath()).filePath(src.skillName);
    QDir targetDir(targetDirPath);

    /* 如果目标文件夹已存在，移动到回收站以便覆盖 */
    if (targetDir.exists()) {
        if (!moveToTrash(targetDirPath)) {
            qCWarning(skillsManager) << "Failed to move existing skill folder to trash:" << targetDirPath;
            setError(tr("Failed to overwrite the existing skill."));
            return false;
        }
        qCDebug(skillsManager) << "Moved existing skill folder to trash for overwrite:" << targetDirPath;
    }

    /* 创建目标父目录（确保 ~/.uos-ai/skills/ 存在） */
    QDir parentDir = targetDir;
    if (!parentDir.cdUp() || !parentDir.exists()) {
        if (!parentDir.mkpath(".")) {
            qCWarning(skillsManager) << "Failed to create skills directory:" << parentDir.path();
            setError(tr("Failed to create the skills directory."));
            return false;
        }
    }

    /* 拷贝整个文件夹 */
    if (!copyDirectory(sourceDir.path(), targetDirPath)) {
        qCWarning(skillsManager) << "Failed to copy skill directory from" << sourceDir.path() << "to" << targetDirPath;
        setError(tr("Failed to copy the skill files."));
        return false;
    }

    qCDebug(skillsManager) << "Skill added successfully:" << src.skillName;
    return true;
}

QJsonObject SkillsManager::addSkillFromUrlForWeb(const QString &url)
{
    QString errorMsg;
    QString skillName;
    const bool success = addSkillFromUrl(url, &errorMsg, &skillName);

    QJsonObject result;
    result["success"] = success;
    result["error"] = errorMsg;
    result["skillName"] = skillName;
    return result;
}

QJsonObject SkillsManager::addSkillForWeb()
{
    // 支持的文件类型：压缩包和 markdown 文件
    const QStringList suffixes = {
        "*.zip",
        "*.tar",
        "*.tar.gz",
        "*.tgz",
        "*.tar.bz2",
        "*.tbz2",
        "*.tar.xz",
        "*.txz",
        "SKILL.md"
    };

    DFileDialog dlg;
    dlg.setDirectory(QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first());
    dlg.setViewMode(DFileDialog::Detail);
    dlg.setFileMode(DFileDialog::ExistingFile);
    const QString filter = tr("Supported files") + QString(" (%1)").arg(suffixes.join(" "));
    dlg.setNameFilter(filter);
    dlg.selectNameFilter(filter);
    dlg.setObjectName("fileDialogAddSkill");

    if (dlg.exec() != DFileDialog::Accepted) {
        QJsonObject result;
        result["success"] = false;
        result["error"] = "";
        return result;
    }

    const QStringList selected = dlg.selectedFiles();
    if (selected.isEmpty()) {
        QJsonObject result;
        result["success"] = false;
        result["error"] = "";
        return result;
    }

    QString errorMsg;
    QString skillName;
    bool success = addSkill(selected.first(), &errorMsg, &skillName);

    QJsonObject result;
    result["success"] = success;
    result["error"] = errorMsg;
    result["skillName"] = skillName;
    return result;
}

bool SkillsManager::removeSkill(const QString &skillName)
{
    /* 检查技能是否存在于 m_skills 中 */
    if (!m_skills.contains(skillName)) {
        qCWarning(skillsManager) << "Skill not found:" << skillName;
        return false;
    }

    const SkillInfo &skill = m_skills[skillName];

    /* 仅允许删除来自 uos-ai 路径的技能 */
    QString uosAiSkillsPath = userSkillInstallPath();
    if (!skill.path.startsWith(uosAiSkillsPath)) {
        qCWarning(skillsManager) << "Cannot remove skill from non-uos-ai path:" << skill.path;
        return false;
    }

    /* 移动到回收站 */
    if (!moveToTrash(skill.path)) {
        qCWarning(skillsManager) << "Failed to move skill to trash:" << skill.path;
        return false;
    }

    qCDebug(skillsManager) << "Skill moved to trash:" << skillName;
    return true;
}

bool SkillsManager::copyDirectory(const QString &sourcePath, const QString &targetPath)
{
    QDir sourceDir(sourcePath);
    QDir targetDir;

    /* 创建目标目录 */
    if (!targetDir.mkpath(targetPath)) {
        return false;
    }

    /* 遍历源目录中的所有文件和子目录 */
    QFileInfoList entries = sourceDir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries);
    for (const QFileInfo &entry : entries) {
        QString srcPath = entry.absoluteFilePath();
        QString dstPath = targetPath + "/" + entry.fileName();

        if (entry.isSymLink() || (!entry.isFile() && !entry.isDir())) {
            return false;
        }

        if (entry.isDir()) {
            /* 递归拷贝子目录 */
            if (!copyDirectory(srcPath, dstPath)) {
                return false;
            }
        } else {
            /* 拷贝文件 */
            if (!QFile::copy(srcPath, dstPath)) {
                return false;
            }
        }
    }
    return true;
}

bool SkillsManager::moveToTrash(const QString &path)
{
    /* Qt 5.14+ 使用 Qt API */
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
    if (!QFile::moveToTrash(path)) {
        qCWarning(skillsManager) << "Failed to move to trash using Qt API:" << path;
        return false;
    }
    return true;
#else
    /* 使用 GIO API 移动到回收站 */
    GFile *file = g_file_new_for_path(path.toUtf8().constData());
    if (!file) {
        qCWarning(skillsManager) << "Failed to create GFile for:" << path;
        return false;
    }

    GError *error = nullptr;
    gboolean success = g_file_trash(file, nullptr, &error);
    g_object_unref(file);

    if (!success) {
        if (error) {
            qCWarning(skillsManager) << "Failed to move to trash using GIO:" << error->message;
            g_error_free(error);
        } else {
            qCWarning(skillsManager) << "Failed to move to trash using GIO:" << path;
        }
        return false;
    }

    return true;
#endif
}

}   // namespace uos_ai
