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

#include "modelscopeitem.h"

#include "localmodelserver.h"
#include "dbwrapper.h"
#include "externalllm/modelhubwrapper.h"
#include "downloader.h"
#include "serverwrapper.h"

#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QtConcurrent>
#include <QResizeEvent>
#include <QtConcurrent>
#include <QDateTime>

#include <DFontSizeManager>
#include <DDialog>

DWIDGET_USE_NAMESPACE
using namespace uos_ai;

ModelScopeItem::ModelScopeItem(const ItemInfo &info, DWidget *parent)
    : m_info(info)
    , DWidget(parent)
{
    initUI();
    initConnect();

    QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
    m_installPath = homePath + "/.local/share/deepin-modelhub/models";
}

void ModelScopeItem::initUI()
{
    m_pLabelTheme = new DLabel;
    DFontSizeManager::instance()->bind(m_pLabelTheme, DFontSizeManager::T6, QFont::Medium);
    m_pLabelSize = new DLabel;
    DFontSizeManager::instance()->bind(m_pLabelSize, DFontSizeManager::T8, QFont::Normal);
    m_pLabelSize->setVisible(false);

    m_pSpinner = new DSpinner;
    m_pSpinner->setFixedSize(QSize(18, 18));
    m_pSpinner->setVisible(false);
    m_pLabelSummary = new DLabel;
    DFontSizeManager::instance()->bind(m_pLabelSummary, DFontSizeManager::T8, QFont::Normal);
    m_pLabelSummary->setElideMode(Qt::ElideRight);

    QHBoxLayout *titleLayout = new QHBoxLayout;
    titleLayout->addWidget(m_pLabelTheme);
    titleLayout->addSpacing(5);
    titleLayout->addWidget(m_pLabelSize);
    titleLayout->addStretch();

    QHBoxLayout *summaryLayout = new QHBoxLayout;
    summaryLayout->addWidget(m_pSpinner);
    summaryLayout->addWidget(m_pLabelSummary);
    summaryLayout->addStretch();

    QVBoxLayout *textLayout = new QVBoxLayout;
    textLayout->setContentsMargins(0, 0, 0, 0);
    textLayout->setSpacing(2);
    textLayout->addLayout(titleLayout);
    textLayout->addLayout(summaryLayout);

    m_pBtnInstall = new DSuggestButton(tr("Install"));
    m_pBtnInstall->setFixedHeight(30);
    m_pBtnInstall->setMinimumWidth(70);

    m_pBtnUninstall = new DPushButton(tr("Uninstall"));
    m_pBtnUninstall->setFixedHeight(30);
    m_pBtnUninstall->setMinimumWidth(70);
    m_pBtnUninstall->hide();

    m_pBtnStop = new DPushButton(tr("Cancel"));
    m_pBtnStop->setFixedHeight(30);
    m_pBtnStop->setMinimumWidth(70);
    m_pBtnStop->hide();

    QHBoxLayout *mainLayout = new QHBoxLayout(this);
    mainLayout->setContentsMargins(10, 10, 10, 10);
    mainLayout->addLayout(textLayout);
    mainLayout->addStretch();
    mainLayout->addWidget(m_pBtnInstall, 0, Qt::AlignVCenter);
    mainLayout->addWidget(m_pBtnUninstall, 0, Qt::AlignVCenter);
    mainLayout->addWidget(m_pBtnStop, 0, Qt::AlignVCenter);

    setLayout(mainLayout);
}

void ModelScopeItem::initConnect()
{
    connect(m_pBtnInstall, &DSuggestButton::clicked, this, &ModelScopeItem::onInstall);
    connect(m_pBtnUninstall, &DSuggestButton::clicked, this, &ModelScopeItem::onUninstall);
    connect(m_pBtnStop, &DSuggestButton::clicked, this, &ModelScopeItem::stopDownload);
    connect(&LocalModelServer::getInstance(), &LocalModelServer::modelPluginsStatusChanged, this, &ModelScopeItem::checkInstallStatus);
}

void ModelScopeItem::setText(const QString &theme, const QString &summary, double sizeGb)
{
    m_pLabelTheme->setText(theme);
    m_pLabelSummary->setText(summary);
    m_pLabelSummary->setToolTip(summary);
    m_sizeGb = sizeGb;
    m_pLabelSize->setText(QString(tr("About %1GB")).arg(m_sizeGb));
}

void ModelScopeItem::onInstall()
{
    // 注意事项
    // DeepSeek本地模型对电脑配置有一定要求，低于推荐配置体验会有影响。
    // 暂不安装 确定安装
    // 查看推荐配置
    // 推荐配置
    // CPU：高性能多核CPU（如Intel i5/i7或AMD Ryzen 5/7系列），主频3.0 GHz以上
    // GPU：显存4GB以上
    // 内存：8GB及以上
    // 硬盘：固态硬盘（SSD）并预留5GB以上空间
    DDialog dlg(this);
    dlg.setIcon(QIcon(":/assets/images/warning.svg"));
    dlg.setMinimumWidth(380);
    dlg.setTitle(tr("Precautions"));
    dlg.setMessage(tr("The DeepSeek local model has certain requirements for computer configuration. If the configuration is lower than the recommended one, the experience will be affected."));

    DWidget *recommend = new DWidget(&dlg);
    QHBoxLayout *recommendLayout = new QHBoxLayout(recommend);
    DLabel *label = new DLabel(recommend);
    label->setText(tr("Recommended configuration"));
    QString tip(tr("<b>Recommended Configuration</b><br><b>CPU: </b>High-performance multi-core CPU (such as Intel i5/i7 or AMD Ryzen 5/7 series), with a clock speed of above 3.0 GHz.<br><b>GPU: </b>With a video memory of above 4GB.<br><b>Memory: </b>8GB and above.<br><b>Hard Drive: </b>Solid State Drive (SSD) with at least 5GB of free space reserved."));
    label->setToolTip(tip);
    DLabel *label2 = new DLabel(recommend);
    label2->setPixmap(QIcon::fromTheme("uos-ai-assistant_tips").pixmap(20, 20));
    label2->setToolTip(tip);
    recommendLayout->addWidget(label);
    recommendLayout->addWidget(label2);
    dlg.addContent(recommend, Qt::AlignCenter);

    dlg.addButton(tr("Install later"), false, DDialog::ButtonNormal);
    dlg.addButton(tr("Confirm installation"), true, DDialog::ButtonRecommend);

    if (DDialog::Accepted != dlg.exec()) {
        return;
    }

    m_startTimeMs = QDateTime::currentMSecsSinceEpoch();
    m_pBtnInstall->setText(tr("Installing"));
    m_pBtnInstall->setEnabled(false);
    m_pBtnInstall->setVisible(false);
    m_pLabelSize->setVisible(false);
    m_pBtnStop->show();

    QDir destinationDir(m_installPath + "/." + m_info.appName);
    destinationDir.removeRecursively();
    if (destinationDir.mkpath("gguf")) {
        qInfo() << "Directory created successfully:";
    } else {
        qWarning() << "Failed to create directory:" ;
        return;
    }

    destinationDir.cd("gguf");

    m_downloader.reset(new Downloader(destinationDir.absolutePath()));
    connect(m_downloader.data(), &Downloader::downloadFinished, this, &ModelScopeItem::onCheckFile);
    connect(m_downloader.data(), &Downloader::onDownloadProgress, this, &ModelScopeItem::onDownloadProgress);

    foreach (const QString &fileUrl, m_info.modelFileList) {
        QString url = m_info.baseUrl + "/" + m_info.appName + "/resolve/master/" + fileUrl;
        m_downloader->addDownloadTask(QUrl(url));
    }

    if (m_timer == nullptr) {
        m_timer = new QTimer(this);
        m_timer->setInterval(30000);
        connect(m_timer, &QTimer::timeout, this, [&] {
            this->stopDownload();
            this->onCheckFileFailed();
        });
    }
    m_timer->start();

    adjustSummaryLabelWidth();
}

void ModelScopeItem::onUninstall()
{
    DDialog dlg(this);
    dlg.setIcon(QIcon(":/assets/images/warning.svg"));
    dlg.setMaximumWidth(380);
    dlg.setTitle(tr("Are you sure you want to delete this model?"));
    dlg.addButton(tr("Cancel", "button"), false, DDialog::ButtonNormal);
    dlg.addButton(tr("Confirm", "button"), true, DDialog::ButtonRecommend);

    if (DDialog::Accepted == dlg.exec()) {
        QString modelName = m_installPath + "/" + m_info.modelName;
        QDir installFile(modelName);

        if (installFile.exists()) {
            if (installFile.removeRecursively())
                qInfo() << "Directory removed successfully.";
            else
                qWarning() << "Failed to remove directory.";
        }
        checkInstallStatus();
    }
}

void ModelScopeItem::onCheckFile()
{
    if (m_timer) {
        m_timer->stop();
    }
    m_pBtnInstall->setText(tr("Checking"));
    m_pBtnInstall->setVisible(true);
    m_pBtnStop->hide();
    m_pBtnInstall->adjustSize();
    m_pLabelSummary->setText(m_pLabelSummary->toolTip());
    m_pSpinner->stop();
    m_pSpinner->setVisible(false);
    adjustSummaryLabelWidth();

    QtConcurrent::run(&ModelScopeItem::runCheckFile, this, QString(m_installPath + "/." + m_info.appName), m_info.modelFileList);
}

void ModelScopeItem::onCheckFileFailed()
{
    checkInstallStatus();

    DDialog dlg(this);
    dlg.setIcon(QIcon(":/assets/images/warning.svg"));
    dlg.addButton(tr("Ok", "button"), true, DDialog::ButtonNormal);
    dlg.setTitle(tr("%0 download failed, please try again!").arg(m_pLabelTheme->text()));
    dlg.exec();

    QDir dir(m_installPath + "/." + m_info.appName);
    dir.removeRecursively();
}

void ModelScopeItem::checkInstallStatus()
{
    bool lastStatus = m_isInstall;
    m_isInstall = ModelhubWrapper::isModelInstalled(m_info.modelName);

    if (lastStatus != m_isInstall)
        ServerWrapper::instance()->updateLLMAccount();

    //判断模型是否在用户路径
    if (m_isInstall) {
        QString modelPath = m_installPath + "/" + m_info.modelName;
        QDir modelDir(modelPath);
        m_pBtnUninstall->setEnabled(modelDir.exists());
    }
    changeInstallStatus();
}

void ModelScopeItem::changeInstallStatus()
{
    if (m_isInstall) {
        m_pBtnInstall->hide();
        m_pBtnUninstall->show();
        m_pBtnStop->hide();
    } else {
        m_pBtnUninstall->hide();
        m_pBtnInstall->show();
        m_pBtnStop->hide();
        m_pBtnInstall->setText(tr("Install"));
        m_pBtnInstall->setEnabled(true);
        m_pBtnInstall->setVisible(true);
        m_pLabelSize->setVisible(true);
        if (ModelhubWrapper::isModelhubInstalled()) {
            m_pBtnInstall->setToolTip("");
        } else {
            m_pBtnInstall->setEnabled(false);
            m_pBtnInstall->setToolTip(tr("Please install the \"Embedding Plugins\" first before installing this model."));
        }
    }

    if (m_downloader && !m_downloader->isFinished()) {
        m_pBtnInstall->setEnabled(false);
        m_pBtnInstall->setVisible(false);
        m_pLabelSize->setVisible(false);
        m_pBtnStop->show();
    }

    adjustSummaryLabelWidth();
}

bool ModelScopeItem::getInstallStatus()
{
    return m_isInstall;
}

void ModelScopeItem::resizeEvent(QResizeEvent *event)
{
    DWidget::resizeEvent(event);
    adjustSummaryLabelWidth();
}

void ModelScopeItem::runCheckFile(ModelScopeItem *self, const QString &dirPath, const QStringList &fileList)
{
    QDir dir(dirPath);
    dir.cd("gguf");
    if (dir.exists()) {
        QFile shaFile(dir.filePath("sha256"));
        if (shaFile.open(QFile::ReadOnly)) {
            QJsonDocument doc = QJsonDocument::fromJson(shaFile.readAll());
            QVariantHash shaKV = doc.object().toVariantHash();
            int success = fileList.size() - 1;
            if (shaKV.size() == (fileList.size() - 1)) {
                for (QString item : fileList) {
                    if (item == "sha256")
                        continue;

                    QString itemTmp = item;
                    // bug-25/02/28 QString remove()方法在AMD和ARM行为不一致
                    bool ok = Downloader::checkSha256(dir.filePath(itemTmp.remove("modelhub/")), shaKV.value(item).toString());
                    if (!ok) {
                        qWarning() << item << "sha256 is not matched.";
                        break;
                    } else {
                        success--;
                    }
                }
                if (success == 0) {
                    QMetaObject::invokeMethod(self, "onDownloadFinished");
                    return;
                }
            } else {
                qWarning() << "sha256 file" << shaKV << "is not match file count" << fileList;
            }
        }
    }

    QMetaObject::invokeMethod(self, "onCheckFileFailed");
}

void ModelScopeItem::adjustSummaryLabelWidth()
{
    m_pBtnInstall->adjustSize();
    m_pBtnUninstall->adjustSize();

    int maxWidth = this->size().width() - 40;
    if (m_isInstall)
        maxWidth -= m_pBtnUninstall->width();
    else
        maxWidth -= m_pBtnInstall->width();

    if (m_pBtnStop->isVisible()) {
        maxWidth += m_pBtnInstall->width();
        maxWidth -= m_pBtnStop->width();
    }

    m_pLabelSummary->setMaximumWidth(maxWidth < 0 ? 0 : maxWidth);
}

void ModelScopeItem::stopDownload()
{
    if (m_timer) {
        m_timer->stop();
    }
    m_pLabelSummary->setText(m_pLabelSummary->toolTip());
    m_pSpinner->stop();
    m_pSpinner->setVisible(false);
    if (m_downloader && !m_downloader->isFinished() && !m_isInstall) {
        QString modelName = m_installPath + "/." + m_info.appName;

        QDir installFile(modelName);

        if (installFile.exists()) {
            if (installFile.removeRecursively())
                qDebug() << "Directory removed successfully.";
            else
                qWarning() << "Failed to remove directory.";
        }
        m_downloader->cancelDownloads();
        checkInstallStatus();
    }
}

void ModelScopeItem::onDownloadFinished()
{
    QString originalFolderPath = m_installPath + "/." + m_info.appName;
    QString targetFolderPath = m_installPath + "/" + m_info.modelName;

    QDir dir(originalFolderPath);
    if (dir.exists()) {
        if (dir.rename(originalFolderPath, targetFolderPath)) {
            qDebug() << "Folder renamed successfully.";
        } else {
            qWarning() << "Folder renaming failed.";
        }
    }
    checkInstallStatus();
}

void ModelScopeItem::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    if (m_downloader && !m_downloader->isFinished()) {
        if (bytesTotal == 0)
            return;

        //qDebug() << QString("onDownloadProgress(%1 / %2)").arg(bytesReceived).arg(bytesTotal);
        qint64 curTimeMs = QDateTime::currentMSecsSinceEpoch();
        if (curTimeMs - m_startTimeMs == 0) {
            return;
        }

        if (m_timer) {
            m_timer->start();
        }
        static qint64 lastTimeMs = QDateTime::currentMSecsSinceEpoch();
        double speed = static_cast<double>(bytesReceived) / (curTimeMs - m_startTimeMs);
        int remainTime = static_cast<int>((bytesTotal - bytesReceived) / speed / 1000);
        if (curTimeMs - lastTimeMs > 500) {
            lastTimeMs = curTimeMs;
            //qDebug() << QString("remainTime(%1 s)").arg(remainTime);
            int min    = remainTime / 60;
            int second = remainTime % 60;
            QString time;
            if (min == 0) {
                time = QString(tr("%1 seconds")).arg(second);
            } else {
                time = QString(tr("%1 minutes")).arg(min);
            }

            double mb = static_cast<double>(bytesReceived) / 1024 / 1024;
            // 已下载%1/%2，还剩%3分钟
            m_pLabelSummary->setText(QString(tr("%1MB/%2GB downloaded, %3 left.")).arg(QString::number(mb, 'f', 3)).arg(m_sizeGb).arg(time));
            adjustSummaryLabelWidth();
            if (!m_pSpinner->isVisible()) {
                m_pSpinner->setVisible(true);
                m_pSpinner->start();
            }
        }
    }
}

ModelScopeItem::ItemInfo ModelScopeItem::ItemInfo::deepseek_r1_1_5B()
{
    ItemInfo info;
    info.appName = "DeepSeek-R1-Distill-Qwen-1.5B-Q4_K_M";
    info.modelName = "DeepSeek-R1-1.5B";
    info.baseUrl = "https://www.modelscope.cn/models/uniontech-yourong";
    info.modelFileList  << "modelhub/config.json"
                        << "modelhub/LICENSE"
                        << "modelhub/template"
                        << "DeepSeek-R1-Distill-Qwen-1.5B-Q4_K_M.gguf"
                        << "sha256";
    return info;
}
