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

#include "web_engine_handler.h"
#include "jscontent.h"
#include "metadataparser.h"
#include "vnoteitem.h"
#include "actionmanager.h"
#include "vtextspeechandtrmanager.h"
#include "voice_player_handler.h"
#include "voice_to_text_handler.h"
#include "voice_recoder_handler.h"
#include "setting.h"
#include "globaldef.h"
#include "common/utils.h"
#include "common/VNoteMainManager.h"

#include <QApplication>
#include <QCursor>
#include <QDBusInterface>
#include <QDBusPendingReply>
#include <QKeyEvent>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QCollator>
#include <QDir>
#include <QDropEvent>
#include <QFile>
#include <QFileInfo>
#include <QTimer>
#include <QCursor>
#include <QMimeData>
#include <QUuid>
#include <QUrl>
// 条件编译：QWebEngineContextMenuRequest 只在 Qt6 中存在
#ifndef USE_QT5
#include <QWebEngineContextMenuRequest>
#endif
#include <QFileDialog>
#include <QStandardPaths>
// 条件编译：根据 Qt 版本包含不同的音频设备头文件
#ifdef USE_QT5
#include <QAudioDeviceInfo>
#include <QAudio>
#else
#include <QMediaDevices>
#include <QAudioDevice>
#endif

#include <dguiapplicationhelper.h>
#include <DSysInfo>
#include <QDebug>

// 获取字号接口
const QString APPEARANCE_SERVICE_V23 = "org.deepin.dde.Appearance1";
const QString APPEARANCE_PATH_V23 = "/org/deepin/dde/Appearance1";
const QString APPEARANCE_INTERFACE_V23 = "org.deepin.dde.Appearance1";

const QString APPEARANCE_SERVICE_V20 = "com.deepin.daemon.Appearance";
const QString APPEARANCE_PATH_V20 = "/com/deepin/daemon/Appearance";
const QString APPEARANCE_INTERFACE_V20 = "com.deepin.daemon.Appearance";

inline bool isV20() { return Dtk::Core::DSysInfo::majorVersion() == "20"; }
const QString DEEPIN_DAEMON_APPEARANCE_SERVICE = isV20() ? APPEARANCE_SERVICE_V20 : APPEARANCE_SERVICE_V23;
const QString DEEPIN_DAEMON_APPEARANCE_PATH = isV20() ? APPEARANCE_PATH_V20 : APPEARANCE_PATH_V23;
const QString DEEPIN_DAEMON_APPEARANCE_INTERFACE = isV20() ? APPEARANCE_INTERFACE_V20 : APPEARANCE_INTERFACE_V23;


DGUI_USE_NAMESPACE

namespace {

QString normalizePicturePath(const QString &path)
{
    QString localPath = path;
    const QUrl url(path);
    if (url.isLocalFile()) {
        localPath = url.toLocalFile();
    }

    const QString normalized = QDir::cleanPath(QDir::fromNativeSeparators(localPath));
    QString result;
    const QDir appDataDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
    const QString appImageRoot = QDir::cleanPath(appDataDir.filePath(QStringLiteral("images")));
    const QString appImageRootWithSep = appImageRoot + QStringLiteral("/");

    if (normalized.startsWith(QStringLiteral("images/"))) {
        result = QDir::cleanPath(appDataDir.filePath(normalized));
    } else {
        if (normalized.startsWith(appImageRootWithSep)) {
            result = normalized;
        } else {
            const QString webImageRoot = QDir::fromNativeSeparators(QStringLiteral(WEB_PATH) + QStringLiteral("/images/"));
            if (normalized.startsWith(webImageRoot)) {
                result = QDir::cleanPath(QDir(appImageRoot).filePath(normalized.mid(webImageRoot.size())));
            }
        }
    }

    if (!result.startsWith(appImageRootWithSep)) {
        return QString();
    }
    return result;
}

}

/*!
 * \class WebEngineHandler
 * \brief 用于 WebEngineView 和 JsContent 交互处理，
 *  部分 C++ 代码与界面交互的功能移至此类处理。
 */

WebEngineHandler::WebEngineHandler(QObject *parent)
    : QObject{parent}
    , m_voicePlayerHandler(new VoicePlayerHandler(this))
    , m_voiceToTextHandler(new VoiceToTextHandler(this))
{
    qInfo() << "WebEngineHandler constructor called";
    initFontsInformation();
    connectWebContent();

    connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::themeTypeChanged, this, &WebEngineHandler::onThemeChanged);

    connect(ActionManager::instance(), &ActionManager::actionTriggered, this, &WebEngineHandler::onMenuClicked);

    connect(m_voiceToTextHandler, &VoiceToTextHandler::audioLengthLimit, this, [this]() {
        Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::AsrTimeLimit);
    });
    connect(m_voiceToTextHandler, &VoiceToTextHandler::noNetworkConnection, this, [this]() {
            Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::NoNetwork);
    });
    connect(m_voicePlayerHandler, &VoicePlayerHandler::playStatusChanged, this, [=](VoicePlayerHandler::PlayState state){
        if (state == VoicePlayerHandler::PlayState::End) {
            Q_EMIT playingVoice(false);
        } else {
            Q_EMIT playingVoice(true);
        }
    });
    
    // 监听录音状态变化，控制编辑器中语音播放按钮的启用/禁用
    connect(VoiceRecoderHandler::instance(), &VoiceRecoderHandler::recoderStateChange, this, 
            [](VoiceRecoderHandler::RecoderType type) {
        qInfo() << "WebEngineHandler: Recording state changed to:" << type;
        // 录音时禁用播放按钮，非录音时启用
        bool enablePlayButton = (type != VoiceRecoderHandler::Recording);
        qInfo() << "WebEngineHandler: Setting voice play button enable to:" << enablePlayButton;
        Q_EMIT JsContent::instance()->callJsSetVoicePlayBtnEnable(enablePlayButton);
    });
    
    qInfo() << "WebEngineHandler constructor finished";
}

QObject *WebEngineHandler::target() const
{
    // qInfo() << "Getting target web engine";
    return m_targetWebEngine;
}

void WebEngineHandler::setTarget(QObject *targetWebEngine)
{
    qInfo() << "Setting target web engine";
    if (targetWebEngine != m_targetWebEngine) {
        qInfo() << "targetWebEngine is not equal to m_targetWebEngine";
        if (m_targetWebEngine)
            m_targetWebEngine->removeEventFilter(this);
        m_targetWebEngine = targetWebEngine;
        if (m_targetWebEngine)
            m_targetWebEngine->installEventFilter(this);
        Q_EMIT targetChanged();
    }
    qInfo() << "Target web engine setting finished";
}

bool WebEngineHandler::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_targetWebEngine) {
        switch (event->type()) {
        case QEvent::DragEnter: {
            auto *dropEvent = static_cast<QDropEvent *>(event);
            const QMimeData *mimeData = dropEvent->mimeData();
            m_dragImageCheckUrls = mimeData && mimeData->hasUrls() ? mimeData->urls() : QList<QUrl>();
            m_dragCanInsertImages = !m_dragImageCheckUrls.isEmpty() && VNoteMainManager::instance()->canInsertImages(m_dragImageCheckUrls);
            if (!m_dragImageCheckUrls.isEmpty() && !m_dragCanInsertImages) {
                dropEvent->setDropAction(Qt::IgnoreAction);
                dropEvent->ignore();
                return true;
            }
            break;
        }
        case QEvent::DragMove: {
            auto *dropEvent = static_cast<QDropEvent *>(event);
            const QMimeData *mimeData = dropEvent->mimeData();
            if (mimeData && mimeData->hasUrls()) {
                const QList<QUrl> urls = mimeData->urls();
                if (urls != m_dragImageCheckUrls) {
                    m_dragImageCheckUrls = urls;
                    m_dragCanInsertImages = VNoteMainManager::instance()->canInsertImages(m_dragImageCheckUrls);
                }
                if (!m_dragCanInsertImages) {
                    dropEvent->setDropAction(Qt::IgnoreAction);
                    dropEvent->ignore();
                    return true;
                }
            }
            break;
        }
        case QEvent::Drop: {
            auto *dropEvent = static_cast<QDropEvent *>(event);
            const QMimeData *mimeData = dropEvent->mimeData();
            const bool canInsertImages = mimeData && mimeData->hasUrls() && VNoteMainManager::instance()->canInsertImages(mimeData->urls());
            m_dragImageCheckUrls.clear();
            m_dragCanInsertImages = false;
            if (mimeData && mimeData->hasUrls() && !canInsertImages) {
                dropEvent->setDropAction(Qt::IgnoreAction);
                dropEvent->ignore();
                return true;
            }
            break;
        }
        case QEvent::DragLeave:
            m_dragImageCheckUrls.clear();
            m_dragCanInsertImages = false;
            break;
        default:
            break;
        }
    }

    return QObject::eventFilter(watched, event);
}

/**
   @return 通过 WebEngineView (qml) 调用 js 函数 \a function ，
    通过事件循环等待 onCallJsResult() 接收返回值。
 */
QVariant WebEngineHandler::callJsSynchronous(const QString &function)
{
    qInfo() << "Calling JS function synchronously:" << function;
    if (m_callJsLoop.isRunning()) {
        qCritical() << "reentrant call js function!";
        return {};
    }

    m_callJsResult.clear();
    if (target()) {
        Q_EMIT requesetCallJsSynchronous(function);
        m_callJsLoop.exec();
    }

    qInfo() << "JS function call finished";
    return m_callJsResult;
}

/**
   @brief 等待 callJsSynchronous() js 函数调用完成
 */
void WebEngineHandler::onCallJsResult(const QVariant &result)
{
    qInfo() << "JS call result received";
    m_callJsResult = result;
    m_callJsLoop.exit();
    qInfo() << "JS call result handling finished";
}

/**
 * @brief 初始化字体列表信息
 */
void WebEngineHandler::initFontsInformation()
{
    qDebug() << "Initializing fonts information";
    // 初始化字体服务Dus接口
    m_appearanceDBusInterface = new QDBusInterface(DEEPIN_DAEMON_APPEARANCE_SERVICE,
                                                   DEEPIN_DAEMON_APPEARANCE_PATH,
                                                   DEEPIN_DAEMON_APPEARANCE_INTERFACE,
                                                   QDBusConnection::sessionBus());
    if (m_appearanceDBusInterface->isValid()) {
        qInfo() << "Font service initialized successfully. Service:" << DEEPIN_DAEMON_APPEARANCE_SERVICE 
                << "Path:" << DEEPIN_DAEMON_APPEARANCE_PATH 
                << "Interface:" << DEEPIN_DAEMON_APPEARANCE_INTERFACE;
        
        QString defaultfont = m_appearanceDBusInterface->property("StandardFont").value<QString>();
        // 获取字体列表
        QDBusPendingReply<QString> font = m_appearanceDBusInterface->call("List", "standardfont");

        QJsonArray array = QJsonDocument::fromJson(font.value().toLocal8Bit().data()).array();
        QStringList list;
        for (int i = 0; i != array.size(); i++) {
            list << array.at(i).toString();
        }
        QList<QVariant> arg;
        arg << "standardfont" << list;
        // 获取带翻译的字体列表
        QDBusPendingReply<QString> font1 = m_appearanceDBusInterface->callWithArgumentList(QDBus::AutoDetect, "Show", arg);

        QJsonArray arrayValue = QJsonDocument::fromJson(font1.value().toLocal8Bit().data()).array();

        // 列表格式转换
        for (int i = 0; i != arrayValue.size(); i++) {
            QJsonObject object = arrayValue.at(i).toObject();
            object.insert("type", QJsonValue("standardfont"));
            m_fontList << object["Name"].toString();
            if (defaultfont == object["Id"].toString()) {
                // 根据id 获取带翻译的默认字体
                m_defaultFont = object["Name"].toString();
            }
        }

        qInfo() << "Default font with translation:" << m_defaultFont;
        // sort for display name
        std::sort(m_fontList.begin(), m_fontList.end(), [=](const QString &obj1, const QString &obj2) {
            QCollator qc;
            return qc.compare(obj1, obj2) < 0;
        });
        qDebug() << "Font list sorted, total fonts:" << m_fontList.size();
    } else {
        qWarning() << "Font service initialization failed. Service:" << DEEPIN_DAEMON_APPEARANCE_SERVICE << "not found";
    }
    qInfo() << "Fonts information initialization finished";
}

/*!
 * \brief 连接 web 内容处理器
 */
void WebEngineHandler::connectWebContent()
{
    qInfo() << "Connecting web content";
    connect(JsContent::instance(), &JsContent::getfontinfo, this, [this]() {
        Q_EMIT JsContent::instance()->callJsSetFontList(m_fontList, m_defaultFont);
    });

    connect(JsContent::instance(), &JsContent::loadFinsh, this, [this]() {
        Q_EMIT loadRichText();
        onThemeChanged();
    });

    connect(JsContent::instance(), &JsContent::popupMenu, this, &WebEngineHandler::onSaveMenuParam);
    connect(JsContent::instance(), &JsContent::textPaste, this, &WebEngineHandler::onPaste);
    connect(JsContent::instance(), &JsContent::viewPictrue, this, &WebEngineHandler::viewPicture);
    connect(JsContent::instance(), &JsContent::saveAudio, this, &WebEngineHandler::saveAudio);
    connect(JsContent::instance(), &JsContent::createNote, this, &WebEngineHandler::createNote);
    qInfo() << "Web content connection finished";
}

/*!
 * \brief 处理右键菜单请求 \a request , 完成后抛出 requestShowMenu() 信号
 */
#ifdef USE_QT5
void WebEngineHandler::onContextMenuRequested(QObject *request)
{
    qInfo() << "Context menu requested";
    // 从 QObject* 中安全地获取属性
    const QPoint pos = request->property("position").toPoint();

    switch (menuType) {
    case VoiceMenu: {
        processVoiceMenuRequest(request);
        break;
    }
    case PictureMenu: {
        // 图片菜单的逻辑很简单，直接弹出
        Q_EMIT requestShowMenu(menuType, pos);
        break;
    }
    case TxtMenu: {
        // 文本菜单的状态已由QML处理，这里调用C++是为了处理未来可能的特殊逻辑
        processTextMenuRequest(request);
        break;
    }
    default:
        break;
    }
    qInfo() << "Context menu request handling finished";
}
#else
void WebEngineHandler::onContextMenuRequested(QWebEngineContextMenuRequest *request)
{
    qInfo() << "Context menu requested";
    switch (menuType) {
    case VoiceMenu: {
        processVoiceMenuRequest(request);
    } break;
    case PictureMenu: {
        Q_EMIT requestShowMenu(menuType, request->position());
    } break;
    case TxtMenu: {
        processTextMenuRequest(request);
    } break;
    default:
        break;
    }
    qInfo() << "Context menu request handling finished";
}
#endif

/**
   @brief 插入音频文件， \a voicePath 为音频文件路径， \a voiceSize 为音频的长度，单位 ms
 */

void WebEngineHandler::onInsertVoiceItem(const QString &voicePath, quint64 voiceSize)
{
    qDebug() << "Inserting voice item, path:" << voicePath << "size:" << voiceSize << "ms";

    VNVoiceBlock data;
    data.ptrVoice->voiceSize = voiceSize;
    // 存储相对路径：AppData/voicenote/ -> voicenote/...
    data.ptrVoice->voicePath = Utils::makeVoiceRelative(voicePath);
    data.ptrVoice->createTime = QDateTime::currentDateTime();
    data.ptrVoice->voiceTitle = data.ptrVoice->createTime.toString("yyyyMMdd hh.mm.ss");
    // 为新录音生成唯一标识（UUID）
    data.ptrVoice->voiceId = QUuid::createUuid().toString(QUuid::WithoutBraces);

    MetaDataParser parse;
    QVariant value;
    parse.makeMetaData(&data, value);

    // 关闭应用时，需要同步插入语音并进行后台更新
    if (OpsStateInterface::instance()->isAppQuit()) {
        qDebug() << "App is quitting, performing synchronous voice insertion";
        callJsSynchronous(QString("insertVoiceItem('%1')").arg(value.toString()));
        return;
    }
    emit JsContent::instance()->callJsInsertVoice(value.toString());
    qInfo() << "Voice item insertion finished";
}

/**
 * @brief 系统主题发生改变处理
 */
void WebEngineHandler::onThemeChanged()
{
    qDebug() << "Theme changed, updating UI";
    DGuiApplicationHelper *dAppHelper = DGuiApplicationHelper::instance();
    DPalette dp = dAppHelper->applicationPalette();
    // 获取系统高亮色
    QString activeHightColor = dp.color(DPalette::Active, DPalette::Highlight).name();
    QString disableHightColor = dp.color(DPalette::Disabled, DPalette::Highlight).name();
    // 获取系统主题类型
    DGuiApplicationHelper::ColorType theme = dAppHelper->themeType();

    QString backgroundColor = DGuiApplicationHelper::LightType == theme ? "#FBFCFD" : "#090A17";
    // 现在背景色主要由 qml 组件和 web 前端 css 共同实现，但在 sw 下保留兼容设置
    emit JsContent::instance()->callJsSetTheme(theme, activeHightColor, disableHightColor, backgroundColor);
    qInfo() << "Theme change handling finished";
}

/**
 * @brief 接收 web 弹出菜单类型 \a menuType 及数据 \a json
 */
void WebEngineHandler::onSaveMenuParam(int type, const QVariant &json)
{
    qInfo() << "Saving menu parameter, type:" << type;
    menuType = static_cast<Menu>(type);
    menuJson = json;
    qInfo() << "Menu parameter saving finished";
}

/**
   @brief 右键菜单点击时触发，根据不同动作类型 \a kind 执行对应操作
 */
void WebEngineHandler::onMenuClicked(ActionManager::ActionKind kind)
{
    qInfo() << "Menu clicked, kind:" << kind;
    switch (kind) {
        case ActionManager::VoiceAsSave:
            // 另存语音
            if (!saveMP3()) {
                qWarning() << "Failed to save MP3";
                Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::SaveFailed);
            }
            break;
        case ActionManager::VoiceToText:
            m_voiceToTextHandler->setAudioToText(m_voiceBlock);
            break;
        case ActionManager::VoiceDelete:
        case ActionManager::PictureDelete:
        case ActionManager::TxtDelete:
            // 调用 js 删除选中文本
            Q_EMIT JsContent::instance()->callJsDeleteSelection();
            break;
        case ActionManager::VoiceSelectAll:
        case ActionManager::PictureSelectAll:
        case ActionManager::TxtSelectAll:
            // 调用 js 实现全选，支持 translateText 区域
            Q_EMIT JsContent::instance()->callJsSelectAll();
            break;
        case ActionManager::VoiceCopy:
        case ActionManager::PictureCopy:
        case ActionManager::TxtCopy:
            // 直接调用web端的复制事件
#ifdef USE_QT5
            Q_EMIT triggerWebAction((int)QWebEnginePage::Copy);
#else
            Q_EMIT triggerWebAction(QWebEnginePage::Copy);
#endif
            break;
        case ActionManager::VoiceCut:
        case ActionManager::PictureCut:
        case ActionManager::TxtCut:
            // 直接调用web端的剪切事件
#ifdef USE_QT5
            Q_EMIT triggerWebAction((int)QWebEnginePage::Cut);
#else
            Q_EMIT triggerWebAction(QWebEnginePage::Cut);
#endif
            break;
        case ActionManager::VoicePaste:
        case ActionManager::PicturePaste:
        case ActionManager::TxtPaste:
            // 粘贴事件，从剪贴板获取数据
            onPaste(isVoicePaste());
            break;
        case ActionManager::PictureView: {
            // 查看图片
            const QString normalizedPath = normalizePicturePath(menuJson.toString());
            Q_EMIT viewPicture(normalizedPath);
            break;
        }
        case ActionManager::PictureSaveAs:
            // 另存图片
            savePictureAs();
            break;
        case ActionManager::TxtSpeech: {
#ifdef USE_QT5
            // Qt5 中使用 QAudioDeviceInfo 检查音频输出设备
            QList<QAudioDeviceInfo> outputDevices = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput);
            if (outputDevices.isEmpty()) {
                qWarning() << "No audio output devices available";
                QString errString = VTextSpeechAndTrManager::instance()->errorString(VTextSpeechAndTrManager::NoOutputDevice);
                if (!errString.isEmpty()) {
                    Q_EMIT popupToast(errString, VTextSpeechAndTrManager::NoOutputDevice);
                }
                break;
            }
#else
            QList<QAudioDevice> outputDevices = QMediaDevices::audioOutputs();
            if (outputDevices.isEmpty()) {
                qWarning() << "No audio output devices available";
                QString errString = VTextSpeechAndTrManager::instance()->errorString(VTextSpeechAndTrManager::NoOutputDevice);
                if (!errString.isEmpty()) {
                    Q_EMIT popupToast(errString, VTextSpeechAndTrManager::NoOutputDevice);
                }
                break;
            }
#endif
            auto status = VTextSpeechAndTrManager::instance()->onTextToSpeech();
            if (VTextSpeechAndTrManager::Success != status) {
                qWarning() << "Text to speech failed with status:" << status;
                QString errString = VTextSpeechAndTrManager::instance()->errorString(status);
                if (!errString.isEmpty()) {
                    Q_EMIT popupToast(errString, status);
                }
            }
            break;
        }
        case ActionManager::TxtStopreading: {
            auto status = VTextSpeechAndTrManager::instance()->onStopTextToSpeech();
            if (VTextSpeechAndTrManager::Success != status) {
                QString errString = VTextSpeechAndTrManager::instance()->errorString(status);
                if (!errString.isEmpty()) {
                    Q_EMIT popupToast(errString, status);
                }
            }
            break;
        }
        case ActionManager::TxtDictation: {
            auto status = VTextSpeechAndTrManager::instance()->onSpeechToText();
            if (VTextSpeechAndTrManager::Success != status) {
                QString errString = VTextSpeechAndTrManager::instance()->errorString(status);
                if (!errString.isEmpty()) {
                    Q_EMIT popupToast(errString, status);
                }
            }
            break;
        }
        // case ActionManager::TxtTranslate:
        //     VTextSpeechAndTrManager::onTextTranslate();
        //     break;
        default:
            break;
    }
    qInfo() << "Menu click handling finished";
}

/**
   @brief 拷贝剪贴内容，\a isVoice 标识音频数据，仅在语音记事本内使用，因此使用js前端的拷贝
    处理，其他类型(图片/文本)可通过剪贴版导入。
 */
void WebEngineHandler::onPaste(bool isVoice)
{
    qDebug() << "Paste operation requested, isVoice:" << isVoice;
    if (isVoice) {
        qInfo() << "Paste operation requested, isVoice is true";
#ifdef USE_QT5
        Q_EMIT triggerWebAction((int)QWebEnginePage::Paste);
#else
        Q_EMIT triggerWebAction(QWebEnginePage::Paste);
#endif
        return;
    }

    // 获取剪贴板信息
    QClipboard *clipboard = QApplication::clipboard();
    const QMimeData *mimeData = clipboard->mimeData();
    
    // 存在文件url
    if (mimeData->hasUrls()) {
        qInfo() << "mimeData has urls";
        QStringList paths;
        for (auto url : mimeData->urls()) {
            paths.push_back(url.path());
        }
        qDebug() << "Pasting URLs:" << paths;
        JsContent::instance()->insertImages(paths);
    } else if (mimeData->hasImage()) {
        qDebug() << "Pasting image from clipboard";
        JsContent::instance()->insertImages(qvariant_cast<QImage>(mimeData->imageData()));
    } else {
        // 无图片文件，直接调用web端的粘贴事件
        qDebug() << "Pasting text content";
#ifdef USE_QT5
        Q_EMIT triggerWebAction((int)QWebEnginePage::Paste);
#else
        Q_EMIT triggerWebAction(QWebEnginePage::Paste);
#endif
    }
    qInfo() << "Paste operation finished";
}

/*!
 * \brief 处理语音菜单请求 \a request
 */
#ifdef USE_QT5
void WebEngineHandler::processVoiceMenuRequest(QObject *request)
{
    qInfo() << "Processing voice menu request";
    m_voiceBlock.reset(new VNVoiceBlock);
    MetaDataParser dataParser;
    // 解析json数据
    if (!dataParser.parse(menuJson, m_voiceBlock.get())) {
        return;
    }

    // 展开相对路径为绝对路径，避免误判
    if (m_voiceBlock && !m_voiceBlock->voicePath.isEmpty()) {
        m_voiceBlock->voicePath = Utils::makeVoiceAbsolute(m_voiceBlock->voicePath);
    }

    // 语音文件不存在使用弹出提示
    if (!QFile::exists(m_voiceBlock->voicePath)) {
        // 异步操作，防止阻塞前端事件
        QTimer::singleShot(0, this, [this] {
            Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::VoicePathNoAvail);
            // 调用 js 删除删除语音文本
            Q_EMIT JsContent::instance()->callJsDeleteSelection();
        });
        return;
    }

    // 如果当前有语音处于转换状态就将语音转文字选项置灰 (此逻辑已移至QML onAboutToShow，此处为兜底)
    ActionManager::instance()->enableAction(ActionManager::VoiceToText, !OpsStateInterface::instance()->isVoice2Text());
    // 请求界面弹出右键菜单
    const QPoint pos = request->property("position").toPoint();
    Q_EMIT requestShowMenu(VoiceMenu, pos);
    qInfo() << "Voice menu request processing finished";
}
#else
void WebEngineHandler::processVoiceMenuRequest(QWebEngineContextMenuRequest *request)
{
    qInfo() << "Processing voice menu request";
    m_voiceBlock.reset(new VNVoiceBlock);
    MetaDataParser dataParser;
    // 解析json数据
    if (!dataParser.parse(menuJson, m_voiceBlock.get())) {
        qInfo() << "Failed to parse menu json";
        return;
    }

    // 展开相对路径为绝对路径，避免误判
    if (m_voiceBlock && !m_voiceBlock->voicePath.isEmpty()) {
        m_voiceBlock->voicePath = Utils::makeVoiceAbsolute(m_voiceBlock->voicePath);
    }

    // 语音文件不存在使用弹出提示
    if (!QFile::exists(m_voiceBlock->voicePath)) {
        qInfo() << "Voice file does not exist";
        // 异步操作，防止阻塞前端事件
        QTimer::singleShot(0, this, [this] {
            Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::VoicePathNoAvail);
            // 调用 js 删除删除语音文本
            Q_EMIT JsContent::instance()->callJsDeleteSelection();
        });
        return;
    }

    // 如果当前有语音处于转换状态就将语音转文字选项置灰
    ActionManager::instance()->enableAction(ActionManager::VoiceToText, !OpsStateInterface::instance()->isVoice2Text());
    // 请求界面弹出右键菜单
    Q_EMIT requestShowMenu(VoiceMenu, request->position());
    qInfo() << "Voice menu request processing finished";
}
#endif

/*!
 * \brief 处理文本菜单请求 \a request
 */
#ifdef USE_QT5
void WebEngineHandler::processTextMenuRequest(QObject *request)
{
    qInfo() << "Processing text menu request";
    // 此处的逻辑是"兜底"，主要逻辑已由QML实现，以确保在所有情况下状态正确
    const int intFlags = request->property("editFlags").toInt();
    auto flags = static_cast<QWebEngineContextMenuData::EditFlags>(intFlags);

    ActionManager::instance()->resetCtxMenu(ActionManager::MenuType::TxtCtxMenu, false); // 重置菜单选项

    ActionManager::instance()->visibleAction(ActionManager::TxtStopreading, false);

    // 设置普通菜单项状态
    if (flags.testFlag(QWebEngineContextMenuData::CanSelectAll)) {
        qInfo() << "flags has CanSelectAll";
        ActionManager::instance()->enableAction(ActionManager::TxtSelectAll, true);
    }
    if (flags.testFlag(QWebEngineContextMenuData::CanCopy)) {
        qInfo() << "flags has CanCopy";
        ActionManager::instance()->enableAction(ActionManager::TxtCopy, true);
        ActionManager::instance()->enableAction(ActionManager::TxtSpeech, true);
    }
    if (flags.testFlag(QWebEngineContextMenuData::CanCut)) {
        qInfo() << "flags has CanCut";
        ActionManager::instance()->enableAction(ActionManager::TxtCut, true);
    }
    if (flags.testFlag(QWebEngineContextMenuData::CanDelete)) {
        qInfo() << "flags has CanDelete";
        ActionManager::instance()->enableAction(ActionManager::TxtDelete, true);
    }
    if (flags.testFlag(QWebEngineContextMenuData::CanPaste)) {
        qInfo() << "flags has CanPaste";
        ActionManager::instance()->enableAction(ActionManager::TxtPaste, true);
        ActionManager::instance()->enableAction(ActionManager::TxtDictation, true);
    }

    const QPoint pos = request->property("position").toPoint();
    Q_EMIT requestShowMenu(TxtMenu, pos);
    qInfo() << "Text menu request processing finished";
}
#else
void WebEngineHandler::processTextMenuRequest(QWebEngineContextMenuRequest *request)
{
    qInfo() << "Processing text menu request";
    ActionManager::instance()->resetCtxMenu(ActionManager::MenuType::TxtCtxMenu, false); // 重置菜单选项

    // TASK-37707: Disable now
    ActionManager::instance()->visibleAction(ActionManager::TxtStopreading, false);

    auto flags = request->editFlags();

    // 设置普通菜单项状态
    // 可全选
    if (flags.testFlag(QWebEngineContextMenuRequest::CanSelectAll)) {
        qInfo() << "flags has CanSelectAll";
        ActionManager::instance()->enableAction(ActionManager::TxtSelectAll, true);
    }
    // 可复制
    if (flags.testFlag(QWebEngineContextMenuRequest::CanCopy)) {
        qInfo() << "flags has CanCopy";
        ActionManager::instance()->enableAction(ActionManager::TxtCopy, true);
        ActionManager::instance()->enableAction(ActionManager::TxtSpeech, true);
    }
    // 可剪切
    if (flags.testFlag(QWebEngineContextMenuRequest::CanCut)) {
        qInfo() << "flags has CanCut";
        ActionManager::instance()->enableAction(ActionManager::TxtCut, true);
    }
    // 可删除
    if (flags.testFlag(QWebEngineContextMenuRequest::CanDelete)) {
        qInfo() << "flags has CanDelete";
        ActionManager::instance()->enableAction(ActionManager::TxtDelete, true);
    }
    // 可粘贴
    if (flags.testFlag(QWebEngineContextMenuRequest::CanPaste)) {
        qInfo() << "flags has CanPaste";
        ActionManager::instance()->enableAction(ActionManager::TxtPaste, true);
        ActionManager::instance()->enableAction(ActionManager::TxtDictation, true);
    }

    // 请求界面弹出右键菜单
    Q_EMIT requestShowMenu(TxtMenu, request->position());
    qInfo() << "Text menu request processing finished";
}
#endif

/*!
 * \brief 处理图片菜单请求
 */
#ifdef USE_QT5
// Qt5 下图片菜单直接由 onContextMenuRequested 发出信号，无需单独的 process 函数
#else
void WebEngineHandler::processPictureMenuRequest(QWebEngineContextMenuRequest *request)
{
    qInfo() << "Processing picture menu request";
    ActionManager::instance()->resetCtxMenu(ActionManager::MenuType::PictureCtxMenu, true);

    auto flags = request->editFlags();

    // 设置普通菜单项状态
    // 可全选
    if (flags.testFlag(QWebEngineContextMenuRequest::CanSelectAll)) {
        qInfo() << "flags has CanSelectAll";
        ActionManager::instance()->enableAction(ActionManager::PictureSelectAll, true);
    }
    // 可复制
    if (flags.testFlag(QWebEngineContextMenuRequest::CanCopy)) {
        qInfo() << "flags has CanCopy";
        ActionManager::instance()->enableAction(ActionManager::PictureCopy, true);
    }
    // 可剪切
    if (flags.testFlag(QWebEngineContextMenuRequest::CanCut)) {
        qInfo() << "flags has CanCut";
        ActionManager::instance()->enableAction(ActionManager::PictureCut, true);
    }
    // 可删除
    if (flags.testFlag(QWebEngineContextMenuRequest::CanDelete)) {
        qInfo() << "flags has CanDelete";
        ActionManager::instance()->enableAction(ActionManager::PictureDelete, true);
    }
    // 可粘贴
    if (flags.testFlag(QWebEngineContextMenuRequest::CanPaste)) {
        qInfo() << "flags has CanPaste";
        ActionManager::instance()->enableAction(ActionManager::PicturePaste, true);
    }

    Q_EMIT requestShowMenu(PictureMenu, request->position());
    qInfo() << "Picture menu request processing finished";
}
#endif

/**
   @return 返回当前拷贝数据是否包含录音
 */
bool WebEngineHandler::isVoicePaste()
{
    qInfo() << "Checking if voice paste";
    // 调用web前端接口
    return callJsSynchronous("returnCopyFlag()").toBool();
}

bool WebEngineHandler::saveMP3()
{
    qInfo() << "Saving MP3";
    if (!m_voiceBlock) {
        qWarning() << "No voice block available for saving";
        return false;
    }
    
    QString savedPath = setting::instance()->getOption(VNOTE_EXPORT_TEXT_PATH_KEY).toString();
    QString defaultDir;
    if (!savedPath.isEmpty()) {
        qInfo() << "savedPath is not empty";
        defaultDir = savedPath;
    } else {
        qInfo() << "savedPath is empty";
        defaultDir = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
    }
    
    QString defaultName = defaultDir + "/" + m_voiceBlock.get()->voiceTitle + ".mp3";
    qDebug() << "Saving MP3 with default name:" << defaultName;
    QString fileName = QFileDialog::getSaveFileName(0, tr("save as MP3"), defaultName, "*.mp3");
    if (!fileName.isEmpty()) {
        qInfo() << "fileName is not empty";
        QFileInfo fileInfo(fileName);
        QDir dir = fileInfo.absoluteDir();
        if (!dir.exists()) {
            qWarning() << "Target directory does not exist:" << dir.path();
            return false;
        }
        
        setting::instance()->setOption(VNOTE_EXPORT_TEXT_PATH_KEY, dir.path());
        qDebug() << "Saved unified export path to settings:" << dir.path();
        
        // 如果目标文件已存在，先删除它（因为 QFile::copy 不会覆盖已存在的文件）
        // 用户已经在 getSaveFileName 对话框中确认覆盖
        if (QFile::exists(fileName)) {
            qDebug() << "Target file exists, removing it before copy:" << fileName;
            if (!QFile::remove(fileName)) {
                qWarning() << "Failed to remove existing file:" << fileName;
                return false;
            }
        }
        
        QFile tmpFile(m_voiceBlock.get()->voicePath);
        bool copyResult = tmpFile.copy(fileName);
        if (!copyResult) {
            qWarning() << "Failed to copy voice file from:" << m_voiceBlock.get()->voicePath << "to:" << fileName;
        }
        return copyResult;
    }
    qInfo() << "MP3 saving finished, result is true";
    return true;
}

void WebEngineHandler::savePictureAs()
{
    qInfo() << "Saving picture as";
    // web端图片src为相对/web路径，需先解析为磁盘真实路径，否则复制失败
    QString originalPath = normalizePicturePath(menuJson.toString());
    if (originalPath.isEmpty()) {
        // 路径无法解析，提示保存失败，避免执行注定失败的复制
        qWarning() << "Failed to normalize picture path:" << menuJson.toString();
        Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::SaveFailed);
        return;
    }
    saveAsFile(originalPath, QStandardPaths::writableLocation(QStandardPaths::PicturesLocation), "image");
    qInfo() << "Picture saving finished";
}

QString WebEngineHandler::saveAsFile(const QString &originalPath, QString dirPath, const QString &defalutName)
{
    // 存储文件夹默认为桌面
    qDebug() << "Saving file, original path:" << originalPath << "default name:" << defalutName;
    if (dirPath.isEmpty()) {
        qInfo() << "dirPath is empty";
        dirPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
    }

    QFileInfo fileInfo(originalPath);
    QString filter = "*." + fileInfo.suffix();
    QString baseName = defalutName.isEmpty() ? fileInfo.baseName() : defalutName;
    QString dir = QString("%1/%2").arg(dirPath).arg(baseName + "." + fileInfo.suffix());
    // 获取需要保存的文件位置，默认路径为用户图片文件夹，默认文件名为原文件名
    QString newPath = QFileDialog::getSaveFileName(0, "", dir, filter);
    if (newPath.isEmpty()) {
        qDebug() << "Save operation cancelled by user";
        return "";
    }
    // 添加文件后缀
    if (!newPath.endsWith(fileInfo.suffix())) {
        qInfo() << "newPath does not end with fileInfo.suffix";
        newPath += ("." + fileInfo.suffix());
    }

    QFileInfo info(newPath);
    if (!QFileInfo(info.dir().path()).isWritable()) {
        qWarning() << "No write permission for directory:" << info.dir().path();
        emit requestMessageDialog(VNoteMessageDialogHandler::NoPermission);
        return "";
    }
    if (info.exists()) {
        qInfo() << "File already exists";
        // 文件已存在，删除原文件
        if (!info.isWritable()) {
            qWarning() << "No write permission for file:" << newPath;
            emit requestMessageDialog(VNoteMessageDialogHandler::NoPermission);
            return "";
        }
        QFile::remove(newPath);
    }

    // 复制文件
    if (!QFile::copy(originalPath, newPath)) {
        qCritical() << "File copy failed from:" << originalPath << "to:" << newPath;
        emit requestMessageDialog(VNoteMessageDialogHandler::SaveFailed);
        return "";
    }
    qInfo() << "File saved successfully to:" << newPath;
    return newPath;
}
