#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "gatewaychatclient.h"
#include <kysdk/diagnosetest/libkydatacollect.h>
#include "notificationmanager.h"
#include "plaintextsanitizer.h"
#include "screensharecapture.h"
#include "speechrecognitioncontroller.h"
#include "speechsynthesiscontroller.h"
#include "dbus/vadbusoperator.h"
#include "dbus/kadbusclient.h"

#include <QDebug>
#include <QDateTime>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDir>
#include <QEvent>
#include <QJsonDocument>
#include <QJsonObject>
#include <QCursor>
#include <QRegularExpression>
#include <QFile>
#include <QFileInfo>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QCoreApplication>
#include <QIcon>
#include <QScreen>
#include <QSettings>
#include <QShowEvent>
#include <QString>
#include <QStringList>
#include <QTextLayout>
#include <QTextOption>
#include <QTimer>
#include <QWindow>
#include <algorithm>

#ifdef Q_OS_LINUX
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#endif
#include <ukui-log4qt.h>
#include <ukuistylehelper/ukuistylehelper.h>

namespace {
const int kAsrRestartDelayMs = 2100;

void trackEvent(const char *eventCode, const char *pageName = "voiceChatPage")
{
    KTrackData *node = kdk_dia_data_init(KEVENTSOURCE_DESKTOP, KEVENT_CLICK);
    kdk_dia_upload_default(node, const_cast<char*>(eventCode), const_cast<char*>(pageName));
    kdk_dia_data_free(node);
}
const int kAsrErrorRestartDelayMs = 2100;
const int kAsrFinalSubmitDelayMs = 1500;
const int kCallbackRingTimeoutMs = 10 * 1000;
const int kCallbackRetryDelayMs = 60 * 1000;
const int kCallbackMaxAttempts = 3;
const int kMinStreamingReadChars = 18;
const int kMaxStreamingReadChars = 48;
const char *kFallbackSystemPrompt = "请仅使用纯文本回答，不要使用Markdown或HTML格式，不要使用代码块、列表标记、加粗或斜体。回答尽量简短。用户问题：";

struct InlineTextFit
{
    QString text;
    bool elided = false;
};

QString appendLineEllipsis(const QString &lineText, const QFontMetrics &metrics, int maxWidth)
{
    const QString trimmed = lineText.trimmed();
    const QString ellipsis = QStringLiteral("...");
    if (metrics.horizontalAdvance(ellipsis) > maxWidth) {
        return QString();
    }
    if (metrics.horizontalAdvance(trimmed + ellipsis) <= maxWidth) {
        return trimmed + ellipsis;
    }

    int left = 0;
    int right = trimmed.size();
    while (left < right) {
        const int mid = (left + right + 1) / 2;
        if (metrics.horizontalAdvance(trimmed.left(mid) + ellipsis) <= maxWidth) {
            left = mid;
        } else {
            right = mid - 1;
        }
    }
    return trimmed.left(left) + ellipsis;
}

InlineTextFit fitInlineText(const QString &text, const QFont &font, int maxWidth, int maxHeight)
{
    InlineTextFit result;
    if (text.isEmpty() || maxWidth <= 0 || maxHeight <= 0) {
        result.text = text;
        return result;
    }

    const QString displayText = text.simplified();
    if (displayText.isEmpty()) {
        result.text = displayText;
        return result;
    }

    const QFontMetrics metrics(font);
    const int maxLines = std::max(1, maxHeight / std::max(1, metrics.lineSpacing()));
    QStringList visibleLines;
    int usedLines = 0;

    QTextLayout layout(displayText, font);
    QTextOption option;
    option.setWrapMode(QTextOption::WrapAnywhere);
    layout.setTextOption(option);
    layout.beginLayout();
    while (true) {
        QTextLine line = layout.createLine();
        if (!line.isValid()) {
            break;
        }
        line.setLineWidth(maxWidth);

        const int lineEnd = line.textStart() + line.textLength();
        const bool hasMoreText = lineEnd < displayText.size();
        const QString lineText = displayText.mid(line.textStart(), line.textLength());
        if (usedLines >= maxLines - 1 && hasMoreText) {
            visibleLines << appendLineEllipsis(lineText, metrics, maxWidth);
            result.elided = true;
            layout.endLayout();
            result.text = visibleLines.join(QLatin1Char('\n'));
            return result;
        }

        visibleLines << lineText.trimmed();
        ++usedLines;
        if (usedLines >= maxLines) {
            break;
        }
    }
    layout.endLayout();

    result.text = visibleLines.join(QLatin1Char('\n'));
    return result;
}

QString voiceAssistantConfigPath()
{
    return QDir::homePath() + QStringLiteral("/.config/kylin-aiassistant/voice-assistant.ini");
}

QString voiceAssistantSystemConfigPath()
{
    return QStringLiteral("/etc/kylin-aiassistant/voice-assistant.ini");
}

QStringList voiceAssistantFallbackConfigPaths()
{
    QStringList paths;
    const QString appDir = QCoreApplication::applicationDirPath();
    if (!appDir.isEmpty()) {
        paths << QDir(appDir).filePath(QStringLiteral("data/voice-assistant.ini"));
        paths << QDir(appDir).filePath(QStringLiteral("../etc/kylin-aiassistant/voice-assistant.ini"));
    }
#ifdef VOICE_ASSISTANT_SOURCE_CONFIG_PATH
    paths << QStringLiteral(VOICE_ASSISTANT_SOURCE_CONFIG_PATH);
#endif
    paths.removeDuplicates();
    return paths;
}

QString loadSystemPrompt(QString *errorMessage)
{
    if (errorMessage) {
        errorMessage->clear();
    }

    const QString configPath = voiceAssistantConfigPath();
    QStringList candidatePaths = {
        configPath,
        voiceAssistantSystemConfigPath(),
    };
    candidatePaths << voiceAssistantFallbackConfigPaths();

    for (const QString &path : candidatePaths) {
        if (!QFile::exists(path)) {
            continue;
        }
        QSettings ini(path, QSettings::IniFormat);
        const QString prompt = ini.value(QStringLiteral("prompt/system")).toString();
        if (!prompt.trimmed().isEmpty()) {
            return prompt;
        }
    }
    qWarning() << "voice-assistant system prompt config missing or empty, using fallback prompt:" << candidatePaths;
    return QString::fromUtf8(kFallbackSystemPrompt);
}

bool isSentenceBoundary(QChar ch)
{
    return ch == QLatin1Char('.') || ch == QLatin1Char('!') || ch == QLatin1Char('?')
            || ch == QLatin1Char(';') || ch == QLatin1Char(':')
            || ch == QChar(0x3002) || ch == QChar(0xFF01) || ch == QChar(0xFF1F)
            || ch == QChar(0xFF1B) || ch == QChar(0xFF1A)
            || ch == QChar(0x3001) || ch == QChar(0xFF0C);
}

bool isParentAssistantRegistered()
{
    QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
    return interface && interface->isServiceRegistered(KADBusClient::serviceName());
}
} // namespace

MainWindow::MainWindow(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MainWindow),
    m_gatewayClient(new GatewayChatClient(this)),
    m_speechController(new SpeechRecognitionController(this)),
    m_synthesisController(new SpeechSynthesisController(this)),
    m_notificationManager(new NotificationManager(this)),
    m_callbackRingTimer(new QTimer(this)),
    m_callbackRetryTimer(new QTimer(this)),
    m_asrFinalSubmitTimer(new QTimer(this)),
    m_state(Idle),
    m_currentAiReplyText(QString()),
    m_hasActiveUserBubble(false),
    m_hasActiveAiBubble(false),
    m_showChatPage(false),
    m_resumeRecognitionAfterPlayback(false),
    m_screenShare(false)
{
    ui->setupUi(this);
    m_systemPrompt = loadSystemPrompt(&m_systemPromptError);

    m_callbackRingTimer->setSingleShot(true);
    m_callbackRetryTimer->setSingleShot(true);
    connect(m_callbackRingTimer, &QTimer::timeout, this, &MainWindow::handleCallbackRingTimeout);
    connect(m_callbackRetryTimer, &QTimer::timeout, this, &MainWindow::startCallbackAttempt);
    m_asrFinalSubmitTimer->setSingleShot(true);
    connect(m_asrFinalSubmitTimer, &QTimer::timeout, this, [this]() {
        submitPendingRecognitionText();
    });

    //this->setAttribute(Qt::WA_TranslucentBackground);
    this->setProperty("useSystemStyleBlur", false);
    this->setAutoFillBackground(true);
    QPalette windowPalette = palette();
    windowPalette.setColor(QPalette::Window, QColor(QStringLiteral("#FFFFFF")));
    setPalette(windowPalette);
    applyWindowStates();
    this->setProperty("ukui_surface_no_titlebar", true);
    // TODO(产品待确认): ukui_surface_skip_switcher / skip_taskbar 是否与多任务视图、任务栏展示策略冲突，需与产品设计对齐。
    // 参考 kylin-note Tray/MiniWindow：Wayland 下常用 ukui 属性；X11 侧可用 KWindowSystem::SkipTaskbar 等。
    this->setProperty("ukui_surface_skip_switcher", true);
    this->setProperty("ukui_surface_skip_taskbar", true);

#ifdef Q_OS_LINUX
    QTimer::singleShot(0, this, [this]() {
        skipTaskbarOnX11();
    });
#endif

    updateCloseButtonIcon(false);
    ui->closeButton->installEventFilter(this);

    {
        QIcon stopIcon;
        stopIcon.addFile(QStringLiteral(":/image/icon/stop.svg"));
        ui->interruptButton->setIcon(stopIcon);
        ui->interruptButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    }
    connect(ui->interruptButton, &QToolButton::clicked, this, [this]() {
        if (m_state != Reading) {
            return;
        }
        m_synthesisController->close();
        resumeRecognitionAfterPlayback();
        trackEvent("interrupt_voice_reading_click_event");
    });

    connect(m_speechController, &SpeechRecognitionController::recognitionStarted, this, [this]() {
        if (m_shuttingDown) {
            return;
        }

        qDebug() << "voice-assistant speech recognition started";
        restoreScreenShareAvailability();
        m_discardLateAsrResults = false;
        if (!m_muted && m_state != Listening) {
            setState(Listening);
        } else {
            updateVoiceStateUi();
        }
    });
    connect(m_speechController, &SpeechRecognitionController::partialText,
            this, &MainWindow::handlePartialText);
    connect(m_speechController, &SpeechRecognitionController::finalText,
            this, &MainWindow::handleFinalText);
    connect(m_speechController, &SpeechRecognitionController::startFailed,
            this, &MainWindow::handleRecognitionStartFailed);
    connect(m_speechController, &SpeechRecognitionController::errorOccurred,
            this, &MainWindow::handleRecognitionError);

    connect(m_gatewayClient, &GatewayChatClient::replyStarted,
            this, &MainWindow::beginAiReply);
    connect(m_gatewayClient, &GatewayChatClient::chunkReceived,
            this, &MainWindow::appendAiReply);
    connect(m_gatewayClient, &GatewayChatClient::replyFinished,
            this, &MainWindow::handleAiReplyFinished);
    connect(m_gatewayClient, &GatewayChatClient::errorOccurred,
            this, &MainWindow::handleChatError);

    connect(m_synthesisController, &SpeechSynthesisController::readingStarted,
            this, &MainWindow::pauseRecognitionForPlayback);
    connect(m_synthesisController, &SpeechSynthesisController::readingFinished, this, [this]() {
        if (m_shuttingDown) {
            return;
        }

        resumeRecognitionAfterPlayback();
    });
    connect(m_synthesisController, &SpeechSynthesisController::errorOccurred, this, [this](const QString &message) {
        if (m_shuttingDown) {
            return;
        }

        qWarning() << "voice-assistant speech synthesis error:" << message;
        if (shouldPersistErrorHistory(false)) {
            saveAssistantErrorMessageIfNeeded(message);
        }
        presentErrorToUser(message, false);
        m_currentAiReplyText = message.trimmed();
        m_currentAiReplySaved = true;
        resumeRecognitionAfterPlayback();
    });

    connect(ui->bottomWidget, &MiniPanel::subtitleToggled, this, [this](bool checked) {
        if (checked) {
            clearInlineError(true);
        }
        m_showChatPage = checked;
        updateMiddlePage();
        trackEvent(checked ? "enable_subtitle_click_event" : "disable_subtitle_click_event");
    });
    connect(ui->bottomWidget, &MiniPanel::screenShareToggled, this, [this](bool enabled) {
        setScreenShareEnabled(enabled);
        trackEvent(enabled ? "enable_screen_share_click_event" : "disable_screen_share_click_event");
    });
    connect(ui->bottomWidget, &MiniPanel::cutoffClicked, this, [this]() {
        if (m_shuttingDown) {
            return;
        }

        trackEvent("end_voice_chat_click_event");
        setScreenShareEnabled(false);
        if (m_callbackMode) {
            cancelCallbackFlow();
            hide();
            return;
        }
        if (m_asrListeningRoundActive) {
            const QString pendingText = pendingRecognitionTextWith(m_lastPartialText);
            if (!pendingText.trimmed().isEmpty()) {
                m_callbackPending = true;
                hide();
                if (!submitPendingRecognitionText(m_lastPartialText)) {
                    m_callbackPending = false;
                }
                return;
            }
        }
        if (m_state == Processing || m_state == Answering) {
            stopAudioForHide();
            m_callbackPending = true;
            hide();
        } else {
            stopAudioForHide();
            hide();
        }
    });
    connect(ui->bottomWidget, &MiniPanel::answerClicked, this, &MainWindow::onAnswerClicked);
    connect(ui->bottomWidget, &MiniPanel::restoreClicked, this, [this]() {
        setMiniMode(false);
        trackEvent("restore_from_mini_mode_click_event");
    });
    connect(ui->miniButton, &QToolButton::clicked, this, [this]() {
        setMiniMode(true);
        trackEvent("enter_mini_mode_click_event");
    });
    connect(ui->bottomWidget, &MiniPanel::muteToggled, this, [this](bool muted) {
        m_muted = muted;
        updateVoiceStateUi();
        trackEvent(muted ? "enable_mute_click_event" : "disable_mute_click_event");
        if (m_muted) {
            m_speechController->cancelPendingStart();
            const QString pendingText = pendingRecognitionTextWith(m_lastPartialText);
            if (m_asrListeningRoundActive && !pendingText.trimmed().isEmpty()) {
                submitPendingRecognitionText(m_lastPartialText);
                m_lastPartialText.clear();
            } else {
                cancelPendingFinalSubmit();
                if (m_speechController->isRunning()) {
                    m_speechController->stop();
                }
                if (m_state == Listening) {
                    setState(Idle);
                }
            }
        } else {
            if (m_state == Idle && !m_speechController->isRunning()) {
                startRecognition();
            } else if (m_state != Listening) {
                m_resumeRecognitionAfterPlayback = true;
            }
        }
    });
    m_screenShare = ui->bottomWidget->screenShareActive();

    connect(qApp, &QGuiApplication::screenAdded, this, [this](QScreen *) {
        if (isVisible()) {
            repositionNearScreenCorner();
        }
    });
    connect(qApp, &QGuiApplication::screenRemoved, this, [this](QScreen *) {
        if (isVisible()) {
            repositionNearScreenCorner();
        }
    });

    // 本地语音模型初始化可能阻塞十余秒，避免在构造阶段阻塞 UI 线程。
    scheduleStartRecognition(0);
}

MainWindow::~MainWindow()
{
    ui->closeButton->removeEventFilter(this);
    stopRecognition();
    delete ui;
}

void MainWindow::setState(VoiceAssistantState newState)
{
    if (m_shuttingDown && newState != Idle) {
        return;
    }

    if (m_state == newState) {
        updateVoiceStateUi();
        return;
    }
    KyInfo() << "voice-assistant state:" << m_state << "->" << newState;
    m_state = newState;
    updateVoiceStateUi();
}

void MainWindow::updateVoiceStateUi()
{
    if (m_shuttingDown) {
        return;
    }

    if (m_callbackMode) {
        return;
    }
    if (m_inlineErrorActive) {
        return;
    }

    const bool isReading = (m_state == Reading);

    // 用户正在说话：已收到 partial 或 final 后仍在延迟提交等待窗口内。
    // 两者都为 false 表示 ASR 已激活但用户尚未开口。
    const bool userSpeaking = m_asrListeningRoundActive || m_asrFinalSubmitTimer->isActive();

    ui->widget_voice_logo->setActive(!m_muted && m_state == Listening && userSpeaking);
    ui->widget_voice_logo->setDrawable(!isReading);
    ui->label_tip->setStyleSheet(QString());
    ui->label_tip->setWordWrap(false);
    ui->label_tip->setMinimumWidth(0);
    ui->label_tip->setMaximumWidth(QWIDGETSIZE_MAX);
    ui->label_tip->setMaximumHeight(QWIDGETSIZE_MAX);
    ui->label_tip->setToolTip(QString());

    if (isReading) {
        ui->label_tip->hide();
        ui->interruptButton->show();
        return;
    }

    ui->interruptButton->hide();

    if (m_state == Idle && m_muted) {
        ui->label_tip->setText(tr("Muted"));
        ui->label_tip->show();
    } else if (m_state == Idle) {
        ui->label_tip->setText(tr("Please start speaking"));
        ui->label_tip->show();
    } else if (m_state == Listening) {
        ui->label_tip->setText(userSpeaking ? tr("Listening") : tr("Please start speaking"));
        ui->label_tip->show();
    }
}

bool MainWindow::isRoundCompleted() const
{
    // 流式朗读期间状态可能已经进入 Reading，但网关仍在持续返回 chunk。
    // 只要 m_waitingGatewayReply 仍为 true，本轮网关回复就还未完成。
    return !m_waitingGatewayReply && m_state != Processing && m_state != Answering;
}

bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == ui->closeButton) {
        const QEvent::Type type = event->type();
        if (type == QEvent::Enter || type == QEvent::MouseButtonPress) {
            updateCloseButtonIcon(true);
        } else if (type == QEvent::Leave) {
            updateCloseButtonIcon(false);
        } else if (type == QEvent::MouseButtonRelease) {
            updateCloseButtonIcon(ui->closeButton->rect().contains(ui->closeButton->mapFromGlobal(QCursor::pos())));
        }
    }
    return QWidget::eventFilter(watched, event);
}

void MainWindow::showEvent(QShowEvent *event)
{
    QWidget::showEvent(event);
    if (!m_hookedWindowScreenChanged) {
        if (QWindow *wh = windowHandle()) {
            connect(wh, &QWindow::screenChanged, this, [this](QScreen *) {
                if (isVisible()) {
                    repositionNearScreenCorner();
                }
            });
            m_hookedWindowScreenChanged = true;
        }
    }
    if (!event->spontaneous()) {
        if (m_needsInitialPosition) {
            repositionNearScreenCorner();
            // Wayland 下首帧前 geometry 可能未稳定，再补一次避免落在屏外不可见。
            QTimer::singleShot(0, this, &MainWindow::repositionNearScreenCorner);
            m_needsInitialPosition = false;
        }
    }
}

void MainWindow::updateCloseButtonIcon(bool white)
{
    QIcon closeIcon;
    if (white) {
        closeIcon.addFile(QStringLiteral(":/image/icon/window-close-symbolic-white.svg"));
    } else {
        closeIcon = QIcon::fromTheme(QStringLiteral("window-close-symbolic"));
        if (closeIcon.isNull()) {
            closeIcon.addFile(QStringLiteral(":/image/icon/window-close-symbolic.svg"));
        }
    }
    ui->closeButton->setIcon(closeIcon);
}

void MainWindow::onActivationMessage(const QString &message)
{
    Q_UNUSED(message);
    qInfo() << "voice-assistant received activation message:" << message;
    if (m_shuttingDown) {
        qInfo() << "voice-assistant ignored activation message while shutting down";
        return;
    }

    if (isCallbackFlowActive()) {
        qInfo() << "voice-assistant cancelling callback flow for activation message";
        cancelCallbackFlow();
        // 回拨被外部中断（用户激活了语音助手），发送回拨失败通知
        sendCallbackFailureNotification();
    }
    // 隐藏后恢复：在原位置 show，不重新定位
    show();
    raise();
    activateWindow();
    startRecognitionForLauncherRequest();
}

void MainWindow::repositionNearScreenCorner()
{
    QScreen *screen = nullptr;
    if (const QWindow *wh = windowHandle()) {
        screen = wh->screen();
    }
    if (!screen) {
        screen = QGuiApplication::screenAt(QCursor::pos());
    }
    if (!screen) {
        screen = QGuiApplication::primaryScreen();
    }
    if (!screen) {
        return;
    }

    const QRect ag = screen->availableGeometry();
    QSize sz = frameGeometry().size();
    if (sz.width() <= 0 || sz.height() <= 0) {
        sz = size();
    }
    if (sz.width() <= 0 || sz.height() <= 0) {
        sz = minimumSize();
    }
    if (sz.width() <= 0 || sz.height() <= 0) {
        sz = QSize(288, 370);
    }

    const int margin = 8;
    int x = ag.right() - sz.width() - margin + 1;
    int y = ag.bottom() - sz.height() - margin + 1;

    const int minX = ag.left();
    const int minY = ag.top();
    const int maxX = std::max(minX, ag.right() - sz.width() + 1);
    const int maxY = std::max(minY, ag.bottom() - sz.height() + 1);
    x = std::min(std::max(x, minX), maxX);
    y = std::min(std::max(y, minY), maxY);

    move(x, y);
}

void MainWindow::startRecognition()
{
    if (m_shuttingDown) {
        return;
    }

    updateMiddlePage();

    if (m_speechController->isRunning()) {
        if (isVisible() && !m_muted && !m_callbackMode) {
            setState(Listening);
            return;
        }

        m_speechController->stop();
        if (m_state == Listening) {
            setState(Idle);
        }
        return;
    }

    if (!canStartRecognition()) {
        m_speechController->cancelPendingStart();
        if (m_state == Listening) {
            setState(Idle);
        }
        return;
    }

    if (m_muted) {
        m_speechController->cancelPendingStart();
        setState(Idle);
        return;
    }

    if (!m_speechController->start()) {
        if (m_speechController->isStopping()) {
            return;
        }
        m_showChatPage = false;
        ui->bottomWidget->setSubtitleChecked(false);
        updateMiddlePage();
        setState(Idle);
    } else {
        setState(Listening);
    }
}

void MainWindow::stopRecognition()
{
    cancelCallbackFlow();
    setScreenShareEnabled(false);
    cancelPendingFinalSubmit();
    invalidateScheduledRecognitionStarts();
    m_speechController->stop();
    m_hasActiveUserBubble = false;
    m_asrListeningRoundActive = false;
    m_discardLateAsrResults = false;
    m_resumeRecognitionAfterPlayback = false;
    m_waitingGatewayReply = false;
    m_streamReadBuffer.clear();
    m_streamReadingStarted = false;
    m_streamReadingFinished = false;
    m_showChatPage = false;
    m_muted = false;
    m_lastPartialText.clear();
    ui->bottomWidget->setSubtitleChecked(false);
    ui->bottomWidget->setMuteChecked(true);
    setState(Idle);
    updateMiddlePage();
}

void MainWindow::stopAudioForHide()
{
    setScreenShareEnabled(false);
    cancelPendingFinalSubmit();
    invalidateScheduledRecognitionStarts();
    m_speechController->cancelPendingStart();
    if (m_speechController->isRunning()) {
        m_speechController->stop();
    }
    if (m_synthesisController) {
        m_synthesisController->close();
    }

    m_asrListeningRoundActive = false;
    m_discardLateAsrResults = true;
    m_resumeRecognitionAfterPlayback = false;
    m_streamReadBuffer.clear();
    m_streamReadingStarted = false;
    m_streamReadingFinished = false;
    m_muted = false;
    m_lastPartialText.clear();
    ui->bottomWidget->setMuteChecked(true);

    if (m_state == Listening || m_state == Reading) {
        setState(Idle);
    } else {
        updateVoiceStateUi();
    }
}

void MainWindow::scheduleStartRecognition(int delayMs)
{
    const int generation = m_recognitionStartGeneration;
    QTimer::singleShot(delayMs, this, [this, generation]() {
        if (generation != m_recognitionStartGeneration) {
            return;
        }
        startRecognition();
    });
}

void MainWindow::invalidateScheduledRecognitionStarts()
{
    ++m_recognitionStartGeneration;
}

bool MainWindow::canStartRecognition() const
{
    return isVisible() && !m_muted && m_state == Idle && !m_callbackMode;
}

void MainWindow::maybeStartRecognitionForCurrentState()
{
    if (m_shuttingDown) {
        return;
    }

    if (canStartRecognition()) {
        scheduleStartRecognition(0);
    }
}

void MainWindow::startRecognitionForLauncherRequest()
{
    if (m_shuttingDown) {
        return;
    }

    if (isCallbackFlowActive()) {
        qInfo() << "voice-assistant ignored launcher recognition request while callback flow is active";
        return;
    }

    if (m_waitingGatewayReply || m_state == Processing || m_state == Answering || m_state == Reading) {
        updateMiddlePage();
        return;
    }

    if (m_speechController->isRunning()) {
        m_discardLateAsrResults = false;
        m_muted = false;
        ui->bottomWidget->setMuteChecked(true);
        setState(Listening);
        updateMiddlePage();
        return;
    }

    cancelPendingFinalSubmit();
    invalidateScheduledRecognitionStarts();
    m_speechController->cancelPendingStart();
    m_asrListeningRoundActive = false;
    m_discardLateAsrResults = false;
    m_resumeRecognitionAfterPlayback = false;
    m_waitingGatewayReply = false;
    m_hasActiveUserBubble = false;
    m_hasActiveAiBubble = false;
    m_streamReadBuffer.clear();
    m_streamReadingStarted = false;
    m_streamReadingFinished = false;
    m_muted = false;
    setScreenShareEnabled(false);
    ui->bottomWidget->setMuteChecked(true);

    if (!m_speechController->isRunning() && !m_speechController->isStopping()) {
        setState(Idle);
    }
    startRecognition();
}

void MainWindow::updateMiddlePage()
{
    ui->stackedWidget_middle->setCurrentWidget(m_showChatPage ? ui->page_chat : ui->page_listening);
}

void MainWindow::setScreenShareEnabled(bool enabled)
{
    if (enabled && !m_screenShareAvailable) {
        if (ui && ui->bottomWidget && ui->bottomWidget->screenShareActive()) {
            ui->bottomWidget->setScreenShareActive(false);
        }
        return;
    }

    m_screenShare = enabled;
    if (ui && ui->bottomWidget && ui->bottomWidget->screenShareActive() != enabled) {
        ui->bottomWidget->setScreenShareActive(enabled);
        m_screenShare = ui->bottomWidget->screenShareActive();
    }
    updateVoiceStateUi();
}

void MainWindow::markScreenShareUnavailable()
{
    m_screenShareAvailable = false;
    setScreenShareEnabled(false);
}

void MainWindow::restoreScreenShareAvailability()
{
    m_screenShareAvailable = true;
}

void MainWindow::ensureUserBubble()
{
    if (m_hasActiveUserBubble) {
        return;
    }

    ui->page_chat->addUserBubble();
    m_hasActiveUserBubble = true;
    clearActiveAiErrorBubble();
}

void MainWindow::handlePartialText(const QString &text)
{
    if (m_shuttingDown) {
        return;
    }

    if (m_discardLateAsrResults) {
        return;
    }

    if (!text.trimmed().isEmpty()) {
        const bool wasIdle = !m_asrListeningRoundActive;
        m_asrListeningRoundActive = true;
        m_lastPartialText = text;
        if (wasIdle) {
            updateVoiceStateUi();
        }
        // 已经有累计的 final 在等待 1.5s 静默窗口：新的 partial 说明用户仍在说话，
        // 重置 1.5s 静默计时器，等用户真正停下；否则保持原样（让真正的 final 来启动计时器），
        // 同时清掉任何残留的旧计时，避免下面的等待意外触发。
        if (!m_pendingFinalText.trimmed().isEmpty()) {
            m_asrFinalSubmitTimer->start(kAsrFinalSubmitDelayMs);
        } else if (m_asrFinalSubmitTimer->isActive()) {
            m_asrFinalSubmitTimer->stop();
        }
    }
    ensureUserBubble();
    ui->page_chat->setUserBubbleText(pendingRecognitionTextWith(text));
}

void MainWindow::handleFinalText(const QString &text)
{
    if (m_shuttingDown) {
        return;
    }

    if (m_discardLateAsrResults) {
        return;
    }

    if (!text.trimmed().isEmpty()) {
        if (m_pendingFinalText.trimmed().isEmpty()) {
            m_pendingFinalText = text;
        } else if (text == m_pendingFinalText || m_pendingFinalText.endsWith(text)) {
            // Some SDKs may repeat the latest final segment; keep one copy.
        } else if (text.startsWith(m_pendingFinalText)) {
            // Be tolerant of SDKs that return cumulative final text.
            m_pendingFinalText = text;
        } else {
            m_pendingFinalText += text;
        }
        m_lastPartialText.clear();
        m_asrListeningRoundActive = true;
        ensureUserBubble();
        ui->page_chat->setUserBubbleText(m_pendingFinalText);
        schedulePendingFinalSubmit();
    }
}

QString MainWindow::pendingRecognitionTextWith(const QString &tailText) const
{
    if (m_pendingFinalText.trimmed().isEmpty()) {
        return tailText;
    }

    if (tailText.trimmed().isEmpty()) {
        return m_pendingFinalText;
    }

    return m_pendingFinalText + tailText;
}

void MainWindow::cancelPendingFinalSubmit()
{
    if (m_asrFinalSubmitTimer) {
        m_asrFinalSubmitTimer->stop();
    }
    m_pendingFinalText.clear();
}

void MainWindow::schedulePendingFinalSubmit()
{
    if (!m_asrFinalSubmitTimer || m_pendingFinalText.trimmed().isEmpty()) {
        return;
    }
    m_asrFinalSubmitTimer->start(kAsrFinalSubmitDelayMs);
}

bool MainWindow::submitPendingRecognitionText(const QString &tailText)
{
    if (m_shuttingDown) {
        return false;
    }

    const QString text = pendingRecognitionTextWith(tailText);
    cancelPendingFinalSubmit();
    m_asrListeningRoundActive = false;
    m_lastPartialText.clear();

    if (m_discardLateAsrResults || text.trimmed().isEmpty()) {
        return false;
    }

    if (m_systemPrompt.trimmed().isEmpty()) {
        const QString message = m_systemPromptError.trimmed().isEmpty()
                ? tr("Voice assistant prompt config is missing or empty.")
                : m_systemPromptError;
        qWarning() << "voice-assistant refuses to send message without system prompt:" << message;
        savePendingUserMessageIfNeeded(text);
        if (shouldPersistErrorHistory(true)) {
            saveAssistantErrorMessageIfNeeded(message);
        }
        m_hasActiveUserBubble = false;
        m_discardLateAsrResults = false;
        setState(Idle);
        presentErrorToUser(message, true);
        m_currentAiReplyText = message.trimmed();
        m_currentAiReplySaved = true;
        if (canStartRecognition()) {
            scheduleStartRecognition(kAsrErrorRestartDelayMs);
        }
        return false;
    }

    m_speechController->stop();
    m_discardLateAsrResults = true;
    submitFinalText(text);
    return m_waitingGatewayReply;
}

void MainWindow::submitFinalText(const QString &text)
{
    if (m_shuttingDown) {
        return;
    }

    if (m_waitingGatewayReply) {
        return;
    }

    trackEvent("send_voice_message_event");
    ensureUserBubble();
    ui->page_chat->setUserBubbleText(text);
    m_hasActiveUserBubble = false;
    pauseRecognitionForPlayback();
    // 须在 sendMessage 之前置位：已连接时发送失败可能同步触发 errorOccurred，否则 inChatErrorContext 会误判为「非对话」。
    m_waitingGatewayReply = true;
    m_chatErrorSavedThisRound = false;
    setState(Processing);

    // 获取屏幕截图的 base64 编码（与原来 V11 完全一致的逻辑）
    QString imageBase64;
    bool hasScreenImage = false;
    if (m_screenShare) {
        const voiceassistant::VoiceScreenGrabResult grab = voiceassistant::capturePrimaryScreenPngBase64();
        if (grab.success && !grab.pngBase64.isEmpty()) {
            imageBase64 = grab.pngBase64;
            hasScreenImage = true;
        } else {
            KyInfo() << "屏幕共享：已开启但未取得有效截图，仅发送文本：" << grab.errorMessage;
        }
    }

    QString fullContent = m_systemPrompt + text;
    if (hasScreenImage) {
        fullContent += QStringLiteral(
                "\n\n【系统提示：本条消息附带屏幕截图，请直接根据截图简要的回答用户桌面正在操作的内容，切勿调用工具或读取文件。】");
    }
    const QString payload = GatewayChatClient::encodeContentWithScreenshotImage(fullContent,
                                                                                hasScreenImage ? imageBase64 : QString());

    // 只保存用户原始 ASR 结果（text），截图单独通过 imageBase64 传入，避免系统提示和 base64 污染 message 字段
    saveChatMessageToDatabase(text, true, QString(), hasScreenImage ? imageBase64 : QString());

    m_gatewayClient->sendMessage(payload);
}

void MainWindow::handleRecognitionError(const QString &message)
{
    if (m_shuttingDown) {
        return;
    }

    qWarning() << "voice-assistant speech recognition error:" << message;
    if (m_discardLateAsrResults) {
        return;
    }

    const bool useBubble = inChatErrorContext();
    const QString pendingUserText = pendingRecognitionTextWith(m_lastPartialText);
    cancelPendingFinalSubmit();
    m_asrListeningRoundActive = false;
    m_discardLateAsrResults = false;
    m_lastPartialText.clear();
    m_speechController->stop();
    m_hasActiveUserBubble = false;
    markScreenShareUnavailable();
    setState(Idle);
    savePendingUserMessageIfNeeded(pendingUserText);
    saveErrorMessageToHistory(message);
    presentErrorToUser(message, useBubble);
    m_currentAiReplyText = message.trimmed();
    m_currentAiReplySaved = true;
    updateMiddlePage();
    if (!m_resumeRecognitionAfterPlayback && canStartRecognition()) {
        scheduleStartRecognition(kAsrErrorRestartDelayMs);
    }
}

void MainWindow::handleRecognitionStartFailed(const QString &message)
{
    if (m_shuttingDown) {
        return;
    }

    qWarning() << "voice-assistant speech recognition start failed:" << message;

    cancelPendingFinalSubmit();
    invalidateScheduledRecognitionStarts();
    m_speechController->cancelPendingStart();
    m_asrListeningRoundActive = false;
    m_discardLateAsrResults = false;
    m_lastPartialText.clear();
    m_resumeRecognitionAfterPlayback = false;
    m_waitingGatewayReply = false;
    m_hasActiveUserBubble = false;
    m_hasActiveAiBubble = false;
    m_showChatPage = false;
    markScreenShareUnavailable();
    ui->bottomWidget->setSubtitleChecked(false);
    setState(Idle);
    saveErrorMessageToHistory(message);
    presentErrorToUser(message, false);
    m_currentAiReplyText = message.trimmed();
    m_currentAiReplySaved = true;
    updateMiddlePage();
}

void MainWindow::beginAiReply()
{
    if (m_shuttingDown) {
        return;
    }

    // 如果本轮对话已结束（例如已经报错），忽略迟到的 replyStarted 信号
    if (isRoundCompleted()) {
        qInfo() << "voice-assistant ignoring late replyStarted, round already completed";
        return;
    }

    if (m_hasActiveAiBubble) {
        return;
    }

    m_currentAiReplyText.clear();
    m_currentAiReplySaved = false;
    m_streamReadBuffer.clear();
    m_streamReadingStarted = false;
    m_streamReadingFinished = false;
    resetReplyThinkingFilter();
    ui->page_chat->addAIBubble();
    ui->page_chat->setLatestAIBubbleText(tr("Answering..."));
    m_hasActiveAiBubble = true;
    setState(Answering);
}

void MainWindow::appendAiReply(const QString &text)
{
    if (m_shuttingDown) {
        return;
    }

    // 如果本轮对话已结束，忽略迟到的 chunk
    if (isRoundCompleted()) {
        return;
    }

    if (text.isEmpty()) {
        return;
    }

    const QString visibleText = filterModelThinkingFromReplyChunk(text);
    const QString plainText = stripMarkdownForDisplay(visibleText);
    if (plainText.isEmpty()) {
        return;
    }

    beginAiReply();
    if (m_currentAiReplyText.isEmpty()) {
        ui->page_chat->setLatestAIBubbleText(plainText);
    } else {
        ui->page_chat->appendToAIBubble(plainText);
    }
    m_currentAiReplyText.append(plainText);
    m_currentAiReplySaved = false;
    enqueueStreamingReadText(plainText);
}

void MainWindow::handleAiReplyFinished(const QString &fullResponse)
{
    if (m_shuttingDown) {
        return;
    }

    // 如果本轮对话已结束（例如中间已经报错），忽略迟到的 replyFinished
    if (isRoundCompleted()) {
        qInfo() << "voice-assistant ignoring late replyFinished, round already completed";
        return;
    }

    m_waitingGatewayReply = false;

    const QString plainFullResponse = stripMarkdownForDisplay(stripModelThinkingForDisplay(fullResponse));

    // 朗读对齐用：在 m_currentAiReplyText 被权威全文覆盖前，先保存 chunk 累计文本。
    // 网关可能对 full_response 做规范化/补全，导致与 chunk 拼接不一致；
    // 流式朗读分支需要据此把"未入 TTS"的尾巴对齐到权威全文，避免少读。
    const QString chunkedReplyBeforeFinish = m_currentAiReplyText;

    if (!plainFullResponse.isEmpty()) {
        if (!m_hasActiveAiBubble) {
            beginAiReply();
        }
        clearActiveAiErrorBubble();
        ui->page_chat->setLatestAIBubbleText(plainFullResponse);
        m_currentAiReplyText = plainFullResponse;
    }

    m_hasActiveAiBubble = false;
    const QString textToRead = plainFullResponse.trimmed().isEmpty() ? m_currentAiReplyText : plainFullResponse;
    if (!textToRead.trimmed().isEmpty()) {
        ui->page_chat->showLatestAIBubbleActions();
    }

    if (m_kaDbusClient && !textToRead.trimmed().isEmpty()) {
        saveChatMessageToDatabase(textToRead, false, QString());
        m_currentAiReplySaved = true;
    } else if (textToRead.trimmed().isEmpty()) {
        m_currentAiReplySaved = true;
    }

    if (m_callbackPending && !isVisible()) {
        m_callbackPending = false;
        setState(Idle);
        notifyVoiceTaskCompleted(textToRead);

        QSettings callbackSettings(QDir::homePath() + "/.config/kylin-aiassistant/settings.ini",
                                   QSettings::IniFormat);
        bool callbackEnabled = callbackSettings.value("voice-callback-enabled", true).toBool();

        if (!callbackEnabled) {
            return;
        }

        beginCallbackAttempts();
        return;
    }

    if (!textToRead.trimmed().isEmpty()) {
        if (!isVisible()) {
            setState(Idle);
            m_resumeRecognitionAfterPlayback = false;
            return;
        }

        if (m_streamReadingStarted) {
            reconcileStreamReadBufferWithFullResponse(chunkedReplyBeforeFinish, textToRead);
            flushStreamingReadBuffer();
            m_synthesisController->finishStream();
        } else {
            setState(Reading);
            pauseRecognitionForPlayback();
            if (!m_synthesisController->speak(textToRead)) {
                resumeRecognitionAfterPlayback();
            }
        }
    } else {
        setState(Idle);
        resumeRecognitionAfterPlayback();
    }
}

QString MainWindow::filterModelThinkingFromReplyChunk(const QString &text)
{
    static const QString kOpenThinkTag = QStringLiteral("<think>");
    static const QString kCloseThinkTag = QStringLiteral("</think>");

    QString output;
    output.reserve(text.size());

    for (const QChar ch : text) {
        m_replyThinkTagBuffer.append(ch);
        const QString &targetTag = m_replyThinkBlockActive ? kCloseThinkTag : kOpenThinkTag;

        if (targetTag.startsWith(m_replyThinkTagBuffer, Qt::CaseInsensitive)) {
            if (m_replyThinkTagBuffer.compare(targetTag, Qt::CaseInsensitive) == 0) {
                m_replyThinkTagBuffer.clear();
                m_replyThinkBlockActive = !m_replyThinkBlockActive;
            }
            continue;
        }

        if (!m_replyThinkBlockActive) {
            output.append(m_replyThinkTagBuffer);
        }
        m_replyThinkTagBuffer.clear();
    }

    return output;
}

void MainWindow::resetReplyThinkingFilter()
{
    m_replyThinkBlockActive = false;
    m_replyThinkTagBuffer.clear();
}

void MainWindow::startStreamingReadIfNeeded()
{
    if (m_shuttingDown) {
        return;
    }

    if (m_streamReadingStarted || m_streamReadingFinished || !isVisible() || m_callbackPending) {
        return;
    }

    if (!m_synthesisController->beginStream()) {
        return;
    }

    m_streamReadingStarted = true;
    setState(Reading);
    pauseRecognitionForPlayback();
}

void MainWindow::enqueueStreamingReadText(const QString &text)
{
    if (m_shuttingDown) {
        return;
    }

    if (text.trimmed().isEmpty() || !isVisible() || m_callbackPending) {
        return;
    }

    startStreamingReadIfNeeded();
    if (!m_streamReadingStarted) {
        return;
    }

    m_streamReadBuffer.append(text);
    QString sentence;
    while (extractReadableSentence(&sentence)) {
        if (!m_synthesisController->enqueueText(sentence)) {
            m_streamReadingFinished = true;
            m_streamReadingStarted = false;
            m_streamReadBuffer.clear();
            resumeRecognitionAfterPlayback();
            return;
        }
    }
}

void MainWindow::flushStreamingReadBuffer()
{
    if (!m_streamReadingStarted) {
        return;
    }

    const QString tail = m_streamReadBuffer.trimmed();
    m_streamReadBuffer.clear();
    if (!tail.isEmpty()) {
        m_synthesisController->enqueueText(tail);
    }
    m_streamReadingFinished = true;
}

void MainWindow::reconcileStreamReadBufferWithFullResponse(const QString &chunkedReplyBeforeFinish,
                                                           const QString &authoritativeFullText)
{
    // 网关 done 携带的 full_response 可能与 chunk 拼接不一致（规范化/补全）。
    // 已入 TTS 的前缀 = chunk 累计 - 当前未派发的 m_streamReadBuffer。
    // 若权威全文以该前缀开头，则把"尚未入 TTS"的尾巴对齐为权威全文剩余部分，
    // 避免气泡显示完整而朗读少字。对不上则保持原 buffer 兜底。
    const int chunkedLen = chunkedReplyBeforeFinish.size();
    const int bufferLen = m_streamReadBuffer.size();
    if (chunkedLen < bufferLen) {
        return;
    }

    const QString enqueuedPrefix = chunkedReplyBeforeFinish.left(chunkedLen - bufferLen);
    if (!authoritativeFullText.startsWith(enqueuedPrefix)) {
        return;
    }

    const QString reconciledTail = authoritativeFullText.mid(enqueuedPrefix.size());
    if (reconciledTail == m_streamReadBuffer) {
        return;
    }

    qInfo() << "voice-assistant TTS stream buffer reconciled to full_response:"
            << "chunked tail=" << m_streamReadBuffer.size()
            << "authoritative tail=" << reconciledTail.size();
    m_streamReadBuffer = reconciledTail;
}

bool MainWindow::extractReadableSentence(QString *sentenceOut)
{
    if (!sentenceOut) {
        return false;
    }

    const QString trimmed = m_streamReadBuffer.trimmed();
    if (trimmed.isEmpty()) {
        m_streamReadBuffer.clear();
        return false;
    }

    int boundary = -1;
    for (int i = 0; i < m_streamReadBuffer.size(); ++i) {
        if (isSentenceBoundary(m_streamReadBuffer.at(i)) && i + 1 >= kMinStreamingReadChars) {
            boundary = i;
            break;
        }
    }

    if (boundary < 0 && trimmed.size() < kMaxStreamingReadChars) {
        return false;
    }

    if (boundary < 0) {
        boundary = std::min(kMaxStreamingReadChars - 1, m_streamReadBuffer.size() - 1);
    }

    *sentenceOut = m_streamReadBuffer.left(boundary + 1).trimmed();
    m_streamReadBuffer = m_streamReadBuffer.mid(boundary + 1);
    return !sentenceOut->isEmpty();
}

void MainWindow::handleChatError(const QString &message)
{
    if (m_shuttingDown) {
        return;
    }

    qWarning() << "voice-assistant gateway chat error:" << message;
    const bool useBubble = inChatErrorContext();
    const bool shouldResumeRecognition = m_resumeRecognitionAfterPlayback;
    m_waitingGatewayReply = false;
    m_streamReadBuffer.clear();
    m_streamReadingStarted = false;
    m_streamReadingFinished = false;
    markScreenShareUnavailable();
    if (m_synthesisController) {
        m_synthesisController->close();
    }
    setState(Idle);

    // 将错误消息保存到历史记录，恢复时显示为红色错误气泡（每轮仅落库一次）
    if (!m_chatErrorSavedThisRound) {
        saveErrorMessageToHistory(message);
        m_chatErrorSavedThisRound = true;
    }

    if (m_callbackPending && !isVisible()) {
        m_callbackPending = false;
        qInfo() << "voice-assistant callback skipped because hidden task failed";
        return;
    }

    presentErrorToUser(message, useBubble);
    m_currentAiReplyText = message.trimmed();
    m_currentAiReplySaved = true;
    if (shouldResumeRecognition) {
        resumeRecognitionAfterPlayback();
    }
}

void MainWindow::pauseRecognitionForPlayback()
{
    m_asrListeningRoundActive = false;
    m_resumeRecognitionAfterPlayback = true;
    if (m_speechController->isRunning()) {
        m_speechController->stop();
    }
}

void MainWindow::resumeRecognitionAfterPlayback()
{
    if (m_shuttingDown) {
        return;
    }

    if (!m_resumeRecognitionAfterPlayback) {
        return;
    }

    m_resumeRecognitionAfterPlayback = false;
    setState(Idle);

    if (m_muted) {
        return;
    }

    if (canStartRecognition()) {
        scheduleStartRecognition(kAsrRestartDelayMs);
    }
}

void MainWindow::on_closeButton_clicked()
{
    shutdownAndQuit();
}

void MainWindow::setDbusOperator(VADBusOperator *op)
{
    m_dbusOperator = op;
}

void MainWindow::setKaDbusClient(KADBusClient *client)
{
    m_kaDbusClient = client;
    if (!m_kaDbusClient) {
        return;
    }

    connect(m_kaDbusClient, &KADBusClient::voiceSessionIdAllocated, this, [this](const QString &sessionId) {
        m_voiceSessionAllocating = false;
        if (sessionId.trimmed().isEmpty()) {
            qWarning() << "MainWindow: failed to allocate voice session id asynchronously";
            if (m_voiceHistorySyncInProgress) {
                finishVoiceHistorySyncAndQuit();
            } else {
                m_pendingVoiceChatMessages.clear();
            }
            return;
        }
        m_voiceSessionId = sessionId;
        qDebug() << "[VOICE-HIST] voiceSessionIdAllocated:" << sessionId << "pendingCount=" << m_pendingVoiceChatMessages.size();
        if (m_voiceHistorySyncInProgress) {
            flushPendingVoiceChatMessagesForQuit();
        }
    }, Qt::UniqueConnection);
    connect(m_kaDbusClient, &KADBusClient::voiceChatMessageSaved, this, [this](int msgIndex, bool success) {
        if (!success) {
            qWarning() << "MainWindow: async save voice chat message failed, msgIndex=" << msgIndex;
        }

        if (!m_voiceHistorySyncInProgress) {
            return;
        }

        if (m_voiceHistoryPendingSaveCount > 0) {
            --m_voiceHistoryPendingSaveCount;
        }
        if (m_voiceHistoryPendingSaveCount <= 0) {
            finishVoiceHistorySyncAndQuit();
        }
    }, Qt::UniqueConnection);
}

void MainWindow::onShowWindow()
{
    qInfo() << "voice-assistant received ShowWindow D-Bus request";
    if (m_shuttingDown) {
        qInfo() << "voice-assistant ignored ShowWindow request while shutting down";
        return;
    }

    if (isCallbackFlowActive()) {
        qInfo() << "voice-assistant cancelling callback flow for ShowWindow request";
        cancelCallbackFlow();
        sendCallbackFailureNotification();
    }
    // 隐藏后恢复：在原位置 show，不重新定位
    show();
    raise();
    activateWindow();
    startRecognitionForLauncherRequest();
}

void MainWindow::shutdownAndQuit()
{
    if (m_shuttingDown) {
        return;
    }
    persistPendingConversationBeforeQuit();
    m_shuttingDown = true;
    hide();

    cancelCallbackFlow();
    cancelPendingFinalSubmit();
    invalidateScheduledRecognitionStarts();
    m_waitingGatewayReply = false;
    m_asrListeningRoundActive = false;
    m_discardLateAsrResults = true;
    m_resumeRecognitionAfterPlayback = false;
    m_streamReadBuffer.clear();
    m_streamReadingStarted = false;
    m_streamReadingFinished = false;
    if (m_speechController->isRunning()) {
        m_speechController->stop();
    }
    if (m_synthesisController) {
        m_synthesisController->close();
    }
    if (m_gatewayClient) {
        m_gatewayClient->shutdown();
    }
    stopRecognition();
    startVoiceHistorySyncBeforeQuit();
}

bool MainWindow::inChatErrorContext() const
{
    return m_showChatPage || m_hasActiveUserBubble || m_hasActiveAiBubble || m_waitingGatewayReply
            || m_asrListeningRoundActive;
}

void MainWindow::ensureChatVisibleForBubbleError()
{
    if (m_showChatPage) {
        return;
    }
    m_showChatPage = true;
    ui->bottomWidget->setSubtitleChecked(true);
    updateMiddlePage();
}

void MainWindow::appendStandaloneAiBubble(const QString &text)
{
    const QString t = text.trimmed();
    if (t.isEmpty()) {
        return;
    }
    ensureChatVisibleForBubbleError();

    if (!m_activeAiErrorBubbleText.isEmpty() && m_activeAiErrorBubbleText == t) {
        ui->page_chat->setLatestAIBubbleErrorText(t);
        m_currentAiReplyText.clear();
        m_hasActiveAiBubble = false;
        return;
    }

    // 如果已经有一个活跃的 AI 气泡（例如 replyStarted 触发的空白气泡），
    // 直接复用它来显示错误信息，避免出现多余的空泡泡。
    if (m_hasActiveAiBubble) {
        m_currentAiReplyText.clear();
        ui->page_chat->setLatestAIBubbleErrorText(t);
        m_hasActiveAiBubble = false;
        m_activeAiErrorBubbleText = t;
        return;
    }

    m_hasActiveAiBubble = false;
    ui->page_chat->addAIBubble();
    ui->page_chat->setLatestAIBubbleErrorText(t);
    m_hasActiveAiBubble = false;
    m_currentAiReplyText.clear();
    m_activeAiErrorBubbleText = t;
}

void MainWindow::clearActiveAiErrorBubble()
{
    m_activeAiErrorBubbleText.clear();
}

void MainWindow::ensureVoiceSessionIdAsync()
{
    if (!m_kaDbusClient || !m_voiceSessionId.isEmpty() || m_voiceSessionAllocating) {
        return;
    }

    m_voiceSessionAllocating = true;
    m_kaDbusClient->allocateVoiceSessionIdAsync();
}

void MainWindow::startVoiceHistorySyncBeforeQuit()
{
    qDebug() << "[VOICE-HIST] startVoiceHistorySyncBeforeQuit: m_kaDbusClient=" << (m_kaDbusClient != nullptr)
             << "pendingCount=" << m_pendingVoiceChatMessages.size()
             << "isParentRegistered=" << isParentAssistantRegistered()
             << "voiceSessionId=" << m_voiceSessionId;
    if (m_voiceHistorySyncInProgress) {
        return;
    }

    // 父进程已退出，跳过所有 D-Bus 历史同步，直接清理并退出，防止 D-Bus 调用阻塞
    if (!isParentAssistantRegistered()) {
        qWarning() << "[VOICE-HIST] startVoiceHistorySyncBeforeQuit: parent not registered, skipping sync and quitting";
        m_pendingVoiceChatMessages.clear();
        finishVoiceHistorySyncAndQuit();
        return;
    }

    m_voiceHistorySyncInProgress = true;

    if (!m_kaDbusClient || m_pendingVoiceChatMessages.isEmpty()) {
        qWarning() << "[VOICE-HIST] startVoiceHistorySyncBeforeQuit: early exit due to null client or empty pending";
        finishVoiceHistorySyncAndQuit();
        return;
    }

    QTimer::singleShot(10000, this, [this]() {
        if (!m_voiceHistorySyncInProgress) {
            return;
        }
        qWarning() << "MainWindow: voice history sync timed out before quit";
        finishVoiceHistorySyncAndQuit();
    });

    if (m_voiceSessionId.isEmpty()) {
        ensureVoiceSessionIdAsync();
        return;
    }

    flushPendingVoiceChatMessagesForQuit();
}

void MainWindow::flushPendingVoiceChatMessagesForQuit()
{
    qDebug() << "[VOICE-HIST] flushPendingVoiceChatMessagesForQuit: m_kaDbusClient=" << (m_kaDbusClient != nullptr)
             << "voiceSessionId=" << m_voiceSessionId
             << "isParentRegistered=" << isParentAssistantRegistered()
             << "pendingCount=" << m_pendingVoiceChatMessages.size();
    // 注意：即使父服务已注销（isParentAssistantRegistered 返回 false），只要 m_kaDbusClient 有效，仍尝试保存
    // 这是为了在主进程退出时也能持久化语音历史（包括屏幕截图）
    if (!m_kaDbusClient || m_voiceSessionId.isEmpty()) {
        qWarning() << "[VOICE-HIST] flushPendingVoiceChatMessagesForQuit: early exit, client or sessionId invalid";
        finishVoiceHistorySyncAndQuit();
        return;
    }

    if (m_pendingVoiceChatMessages.isEmpty()) {
        finishVoiceHistorySyncAndQuit();
        return;
    }

    const QList<PendingVoiceChatMessage> pending = m_pendingVoiceChatMessages;
    m_pendingVoiceChatMessages.clear();
    m_voiceHistoryPendingSaveCount = pending.size();
    qDebug() << "[VOICE-HIST] flushPendingVoiceChatMessagesForQuit: flushing" << pending.size() << "messages to DB";
    const QString gatewaySessionId = m_gatewayClient ? m_gatewayClient->sessionId() : QString();
    for (const PendingVoiceChatMessage &item : pending) {
        m_kaDbusClient->saveVoiceChatMessageAsync(m_voiceSessionId, item.msgIndex,
                                                  item.message, 0, item.operateTime, gatewaySessionId);
    }
}

void MainWindow::finishVoiceHistorySyncAndQuit()
{
    if (!m_voiceHistorySyncInProgress && !m_shuttingDown) {
        return;
    }
    // 防止 10s 超时与最后一条保存回调先后都跑到这里：ProcessExiting 与 quit() 只允许触发一次。
    static bool s_alreadyFinished = false;
    if (s_alreadyFinished) {
        return;
    }
    s_alreadyFinished = true;

    m_voiceHistorySyncInProgress = false;
    m_voiceHistoryPendingSaveCount = 0;
    m_pendingVoiceChatMessages.clear();

    if (!m_voiceSessionId.isEmpty() && m_kaDbusClient && isParentAssistantRegistered()) {
        m_kaDbusClient->notifyHistoryChangedAsync();
    }
    if (m_dbusOperator) {
        emit m_dbusOperator->ProcessExiting(VADBusOperator::CloseButton);
    }
    m_needsInitialPosition = true;
    qApp->quit();
}

void MainWindow::saveChatMessageToDatabase(const QString &message, bool sentByUser, const QString &modelMsg, const QString &imageBase64, const QString &colorMsg, int errorCode)
{
    qDebug() << "[VOICE-HIST] saveChatMessageToDatabase: m_kaDbusClient=" << (m_kaDbusClient != nullptr)
             << "sentByUser=" << sentByUser << "msgLen=" << message.length() << "pendingCount=" << m_pendingVoiceChatMessages.size()
             << "hasImage=" << !imageBase64.isEmpty();
    if (!m_kaDbusClient) {
        qWarning() << "[VOICE-HIST] saveChatMessageToDatabase: m_kaDbusClient is null, message discarded!";
        return;
    }

    // 只保存用户原始 ASR 结果（message），截图通过 imageBase64 参数单独传入
    // 这样历史记录中 message 字段永远是干净的用户输入，不会包含系统提示或 base64 垃圾
    QString cleanMessage = message;

    // 直接使用传入的 imageBase64 参数构造 imageSrc 字段（若为空则不保存图片）
    QString imageSrcField;
    if (!imageBase64.isEmpty()) {
        imageSrcField = imageBase64.startsWith("data:") ? imageBase64
                                                        : "data:image/png;base64," + imageBase64;
    }

    QJsonObject jsonObj;
    jsonObj[QStringLiteral("message")] = cleanMessage;
    jsonObj[QStringLiteral("imageSrc")] = imageSrcField;
    jsonObj[QStringLiteral("imagePath")] = QString();   // 语音截图没有本地文件路径
    jsonObj[QStringLiteral("sentByUser")] = sentByUser;
    jsonObj[QStringLiteral("reasonMessage")] = QString();
    jsonObj[QStringLiteral("modelMsg")] = sentByUser ? QString() : modelMsg;
    jsonObj[QStringLiteral("colorMsg")] = colorMsg.isEmpty() ? (sentByUser ? QStringLiteral("white") : QStringLiteral("black")) : colorMsg;
    jsonObj[QStringLiteral("errorCode")] = errorCode;
    jsonObj[QStringLiteral("isEnd")] = true;
    jsonObj[QStringLiteral("isReading")] = false;
    jsonObj[QStringLiteral("uniqueIndex")] = m_msgIndex;
    // 额外带上 showImageOptions 等字段，保持与主界面 item 结构一致
    jsonObj[QStringLiteral("showImageOptions")] = false;
    jsonObj[QStringLiteral("drawMsg")] = false;
    const QString jsonStr = QJsonDocument(jsonObj).toJson(QJsonDocument::Compact);

    const qlonglong operateTime = QDateTime::currentDateTime().toSecsSinceEpoch();
    const int msgIndex = m_msgIndex++;

    // 缓存到 pending 列表，仅在点击 closeButton 关闭时统一写入数据库
    qDebug() << "[VOICE-HIST] saveChatMessageToDatabase: caching to pending for batch save on quit";
    PendingVoiceChatMessage item;
    item.msgIndex = msgIndex;
    item.message = jsonStr;
    item.operateTime = operateTime;
    m_pendingVoiceChatMessages.append(item);
}

void MainWindow::savePendingUserMessageIfNeeded(const QString &message)
{
    const QString text = message.trimmed();
    if (text.isEmpty()) {
        return;
    }

    saveChatMessageToDatabase(text, true, QString());
}

void MainWindow::saveAssistantErrorMessageIfNeeded(const QString &message)
{
    const QString text = message.trimmed();
    if (text.isEmpty()) {
        return;
    }

    saveChatMessageToDatabase(text, false, QString(), QString(), QStringLiteral("red"), 1);
}

void MainWindow::saveErrorMessageToHistory(const QString &message)
{
    if (message.trimmed().isEmpty()) {
        return;
    }

    saveChatMessageToDatabase(message, false, QString(), QString(), QStringLiteral("red"), 1);
}
void MainWindow::persistPendingConversationBeforeQuit()
{
    const QString pendingUserText = pendingRecognitionTextWith(m_lastPartialText).trimmed();
    if (!pendingUserText.isEmpty()) {
        savePendingUserMessageIfNeeded(pendingUserText);
    }

    if (!m_currentAiReplySaved) {
        const QString aiText = m_currentAiReplyText.trimmed();
        if (!aiText.isEmpty()) {
            saveChatMessageToDatabase(aiText, false, QString());
        }
        m_currentAiReplySaved = true;
    }
}

bool MainWindow::shouldPersistErrorHistory(bool useChatBubble) const
{
    return useChatBubble || !m_pendingVoiceChatMessages.isEmpty() || !m_currentAiReplyText.trimmed().isEmpty();
}

void MainWindow::notifyVoiceTaskCompleted(const QString &message)
{
    const QString text = message.trimmed();
    if (text.isEmpty()) {
        return;
    }

    // 仅发送本地系统通知弹窗，不发送智能体通知
    if (m_notificationManager) {
        m_notificationManager->sendNotification(tr("Voice assistant"), text);
    }
}

void MainWindow::presentErrorToUser(const QString &message, bool useChatBubble)
{
    const QString trimmed = message.trimmed();
    const QString text = trimmed.isEmpty() ? tr("Unknown error.") : trimmed;

    if (useChatBubble && isVisible()) {
        appendStandaloneAiBubble(text);
        return;
    }

    if (isVisible() && !m_callbackMode) {
        showInlineError(text);
        return;
    }

    if (m_notificationManager) {
        m_notificationManager->sendNotification(tr("Voice assistant"), text);
    }
}

void MainWindow::showInlineError(const QString &text)
{
    ++m_inlineErrorGeneration;
    const int generation = m_inlineErrorGeneration;
    m_inlineErrorActive = true;
    const int availableWidth = std::max(width(), std::max(ui->stackedWidget_middle->width(), ui->page_listening->width()));
    const int maxTextWidth = std::max(120, availableWidth - 32);
    const int maxTextHeight = 42;
    const InlineTextFit fittedText = fitInlineText(text, ui->label_tip->font(), maxTextWidth, maxTextHeight);
    ui->interruptButton->hide();
    ui->label_tip->setText(fittedText.text);
    ui->label_tip->setToolTip(fittedText.elided ? text : QString());
    ui->label_tip->setStyleSheet(QStringLiteral("color: #D92D20;"));
    ui->label_tip->setAlignment(Qt::AlignCenter);
    ui->label_tip->setWordWrap(true);
    ui->label_tip->setMaximumWidth(maxTextWidth);
    ui->label_tip->setMinimumWidth(maxTextWidth);
    ui->label_tip->setMaximumHeight(maxTextHeight);
    ui->label_tip->show();

    QTimer::singleShot(6000, this, [this, generation]() {
        if (generation != m_inlineErrorGeneration) {
            return;
        }
        clearInlineError(true);
    });
}

void MainWindow::clearInlineError(bool refreshUi)
{
    if (!m_inlineErrorActive) {
        return;
    }

    ++m_inlineErrorGeneration;
    m_inlineErrorActive = false;
    ui->label_tip->setWordWrap(false);
    ui->label_tip->setMinimumWidth(0);
    ui->label_tip->setMaximumWidth(QWIDGETSIZE_MAX);
    ui->label_tip->setMaximumHeight(QWIDGETSIZE_MAX);
    ui->label_tip->setStyleSheet(QString());
    ui->label_tip->setToolTip(QString());

    if (refreshUi) {
        updateVoiceStateUi();
    }
}

void MainWindow::setCallbackMode(bool enabled)
{
    if (enabled) {
        if (m_miniMode) {
            setMiniMode(false);
        }
        clearInlineError(false);
        setScreenShareEnabled(false);
        m_callbackMode = true;
        ui->widget_voice_logo->hide();
        ui->label_callback_title->setText(tr("KylinBot"));
        ui->label_callback_title->show();
        ui->label_tip->setStyleSheet(QString());
        ui->label_tip->setText(tr("Inviting you to voice chat..."));
        ui->label_tip->show();
        ui->interruptButton->hide();
        ui->miniButton->hide();
        m_showChatPage = false;
        ui->bottomWidget->setSubtitleChecked(false);
        updateMiddlePage();
        ui->bottomWidget->setCallbackMode(true);
    } else {
        m_callbackMode = false;
        ui->label_callback_title->hide();
        ui->widget_voice_logo->show();
        ui->miniButton->show();
        ui->bottomWidget->setCallbackMode(false);
        updateVoiceStateUi();
    }
}

void MainWindow::beginCallbackAttempts()
{
    m_callbackAttemptCount = 0;
    startCallbackAttempt();
}

void MainWindow::startCallbackAttempt()
{
    if (m_callbackAttemptCount >= kCallbackMaxAttempts) {
        finishCallbackFailure();
        return;
    }

    if (m_callbackMode) {
        return;
    }

    if (isVisible()) {
        cancelCallbackFlow();
        return;
    }

    ++m_callbackAttemptCount;

    // 每次回拨尝试（包括重试）都发送系统通知弹窗，内容为语音聊天返回的文本
    if (m_notificationManager) {
        if (m_callbackAttemptCount > 1) {
            const QString replyText = m_currentAiReplyText.trimmed();
            m_notificationManager->sendNotification(tr("Voice assistant"),
                                                    replyText.isEmpty() ? tr("Inviting you to voice chat...") : replyText);
        }
    }

    setCallbackMode(true);
    // 隐藏后恢复：在原位置 show，不重新定位
    show();
    raise();
    activateWindow();
    m_callbackRingTimer->start(kCallbackRingTimeoutMs);
}

void MainWindow::handleCallbackRingTimeout()
{
    if (!m_callbackMode) {
        return;
    }

    setCallbackMode(false);
    hide();

    if (m_callbackAttemptCount >= kCallbackMaxAttempts) {
        finishCallbackFailure();
    } else {
        scheduleCallbackRetry();
    }
}

void MainWindow::scheduleCallbackRetry()
{
    if (m_callbackAttemptCount >= kCallbackMaxAttempts) {
        finishCallbackFailure();
        return;
    }

    m_callbackRetryTimer->start(kCallbackRetryDelayMs);
}

void MainWindow::sendCallbackFailureNotification()
{
    const QString failText = tr("The task is complete, but callback failed. Please enter the conversation to view details.");

    // 本地系统通知
    if (m_notificationManager) {
        m_notificationManager->sendNotification(tr("Voice assistant"), failText);
    }

    // 同时通知父进程（智能体），用于历史链接和未读标记
    if (m_kaDbusClient && !m_voiceSessionId.isEmpty() && isParentAssistantRegistered()) {
        m_kaDbusClient->notifyVoiceTaskCompletedAsync(m_voiceSessionId, failText);
    }
}

void MainWindow::finishCallbackFailure()
{
    cancelCallbackFlow();
    hide();
    sendCallbackFailureNotification();
}

void MainWindow::cancelCallbackFlow()
{
    m_callbackPending = false;
    m_callbackAttemptCount = 0;
    if (m_callbackRingTimer) {
        m_callbackRingTimer->stop();
    }
    if (m_callbackRetryTimer) {
        m_callbackRetryTimer->stop();
    }
    if (m_callbackMode) {
        setCallbackMode(false);
    }
}

bool MainWindow::isCallbackFlowActive() const
{
    return m_callbackPending
        || m_callbackMode
        || (m_callbackRingTimer && m_callbackRingTimer->isActive())
        || (m_callbackRetryTimer && m_callbackRetryTimer->isActive());
}

void MainWindow::onAnswerClicked()
{
    m_callbackPending = false;
    m_callbackAttemptCount = 0;
    m_callbackRingTimer->stop();
    m_callbackRetryTimer->stop();
    setCallbackMode(false);
    ui->bottomWidget->setSubtitleChecked(true);
    m_showChatPage = true;
    updateMiddlePage();

    const QString textToRead = m_currentAiReplyText.trimmed();
    if (!textToRead.isEmpty()) {
        setState(Reading);
        pauseRecognitionForPlayback();
        if (!m_synthesisController->speak(textToRead)) {
            resumeRecognitionAfterPlayback();
        }
    } else {
        maybeStartRecognitionForCurrentState();
    }
}

void MainWindow::setMiniMode(bool enabled)
{
    if (m_miniMode == enabled) {
        return;
    }
    m_miniMode = enabled;

    if (enabled) {
        ui->titleWidget->hide();
        ui->stackedWidget_middle->hide();
        ui->verticalLayout->setContentsMargins(0, 0, 0, 0);
        ui->verticalLayout->setSpacing(0);
        ui->bottomWidget->setMinimumHeight(40);
        ui->bottomWidget->setMaximumHeight(40);
        ui->bottomWidget->setMiniMode(true);
        resizeKeepingBottomRight(QSize(181, 40));
    } else {
        ui->bottomWidget->setMiniMode(false);
        ui->bottomWidget->setMinimumHeight(48);
        ui->bottomWidget->setMaximumHeight(48);
        ui->titleWidget->show();
        ui->stackedWidget_middle->show();
        ui->verticalLayout->setContentsMargins(0, 0, 0, 24);
        ui->verticalLayout->setSpacing(0);
        resizeKeepingBottomRight(QSize(288, 370));
    }
}

void MainWindow::resizeKeepingBottomRight(const QSize &newSize)
{
    const QPoint bottomRight = frameGeometry().bottomRight();

    setMinimumSize(newSize);
    setMaximumSize(newSize);
    resize(newSize);

    QSize frameSize = frameGeometry().size();
    if (frameSize.width() <= 0 || frameSize.height() <= 0) {
        frameSize = size();
    }
    move(bottomRight.x() - frameSize.width() + 1,
         bottomRight.y() - frameSize.height() + 1);
}

void MainWindow::applyWindowStates()
{
    setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);

    kdk::UkuiStyleHelper::self()->removeHeader(this);
}

void MainWindow::skipTaskbarOnX11()
{
#ifdef Q_OS_LINUX
    // DISPLAY may point to XWayland even though this is a native Wayland
    // window. Its WId is not an X11 Window and must never be passed to Xlib.
    if (QGuiApplication::platformName().compare(QStringLiteral("xcb"), Qt::CaseInsensitive) != 0) {
        return;
    }

    Display *display = XOpenDisplay(nullptr);
    if (!display) return;

    WId winId = this->winId();
    if (winId == 0) {
        XCloseDisplay(display);
        return;
    }

    Atom netWmState = XInternAtom(display, "_NET_WM_STATE", True);
    Atom netWmStateSkipTaskbar = XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", True);

    if (netWmState != None && netWmStateSkipTaskbar != None) {
        XEvent xev;
        memset(&xev, 0, sizeof(xev));
        xev.type = ClientMessage;
        xev.xclient.window = winId;
        xev.xclient.message_type = netWmState;
        xev.xclient.display = display;
        xev.xclient.format = 32;
        xev.xclient.data.l[0] = 1;  // _NET_WM_STATE_ADD
        xev.xclient.data.l[1] = netWmStateSkipTaskbar;
        xev.xclient.data.l[2] = 0;
        xev.xclient.data.l[3] = 1;  // source: application
        xev.xclient.data.l[4] = 0;

        XSendEvent(display, DefaultRootWindow(display), False,
                   SubstructureRedirectMask | SubstructureNotifyMask, &xev);
        XFlush(display);
        qDebug() << "MainWindow: skipTaskbar set via X11 native API, winId:" << winId;
    }

    XCloseDisplay(display);
#else
    Q_UNUSED(this);
#endif
}
