/***************************************************************************
 * SPDX-FileCopyrightText: 2026 S. MANKOWSKI stephane@mankowski.fr
 * SPDX-License-Identifier: GPL-3.0-or-later
 ***************************************************************************/
/** @file
 * A dock widget for the AI assistant plugin.
 */
#include "skgassistantplugindockwidget.h"

#include <qeventloop.h>
#include <qfile.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qjsonparseerror.h>
#include <qnetworkreply.h>
#include <qnetworkrequest.h>
#include <qregularexpression.h>
#include <qshortcut.h>
#include <qstringlist.h>
#include <qtextcursor.h>
#include <qurlquery.h>

#include "skgassistant_settings.h"
#include "skgmainpanel.h"
#include "skgpropertyobject.h"
#include "skgservices.h"
#include "skgshow.h"
#include "skgtraces.h"
#include "skgtransactionmng.h"

#define PREFIX_AIASSISTANT_SESSION QStringLiteral("SKG_AI_SESSION_")

SKGAssistantPluginDockWidget::SKGAssistantPluginDockWidget(QWidget *iParent, SKGDocument *iDocument)
    : SKGWidget(iParent, iDocument)
{
    SKGTRACEINFUNC(10)
    if (iDocument == nullptr) {
        return;
    }

    ui.setupUi(this);

    ui.kSaveCurrentAiSession->setIcon(SKGServices::fromTheme(QStringLiteral("document-save")));
    ui.kDeleteAiSession->setIcon(SKGServices::fromTheme(QStringLiteral("edit-delete")));
    ui.kAskButton->setIcon(SKGServices::fromTheme(QStringLiteral("media-playback-start")));
    ui.kClearButton->setIcon(SKGServices::fromTheme(QStringLiteral("edit-clear")));
    ui.kHelpButton->setIcon(SKGServices::fromTheme(QStringLiteral("help-about")));
    ui.kSettingsButton->setIcon(SKGServices::fromTheme(QStringLiteral("settings-configure")));

    ui.kDisplayReasoning->addGroupedItem(QStringLiteral("0"),
                                         i18nc("Option to display or not the reasoning of the assistant", "No reasoning"),
                                         QString(),
                                         QString(),
                                         QStringLiteral("display_reasoning"));
    ui.kDisplayReasoning->addGroupedItem(QStringLiteral("1"), i18nc("Option to display or not the reasoning of the assistant", "Reasoning tag")), QString(),
        QString(), QStringLiteral("display_reasoning");
    ui.kDisplayReasoning->addGroupedItem(QStringLiteral("2"), i18nc("Option to display or not the reasoning of the assistant", "Current reasoning")), QString(),
        QString(), QStringLiteral("display_reasoning");
    ui.kDisplayReasoning->addGroupedItem(QStringLiteral("3"),
                                         i18nc("Option to display or not the reasoning of the assistant", "All reasoning"),
                                         QString(),
                                         QString(),
                                         QStringLiteral("display_reasoning"));
    ui.kDisplayReasoning->addGroupedItem(QStringLiteral("4"),
                                         i18nc("Option to display or not the reasoning of the assistant", "All reasoning and tools answers")),
        QString(), QString(), QStringLiteral("display_reasoning");
    ui.kDisplayReasoning->setState(QStringLiteral("1"));

    // Add help text to the help area
    QList<QString> helpTexts = {i18nc("Example of a IA question", "How much do I have in my current accounts?"),
                                i18nc("Example of a IA question", "What is the total amount of my expenses for this month?"),
                                i18nc("Example of a IA question", "Can you provide a summary of my recent transactions?"),
                                i18nc("Example of a IA question", "What are the top 5 categories where I spend the most money?"),
                                i18nc("Example of a IA question", "Can you suggest ways to reduce my monthly expenses?")};
    for (const QString &helpText : helpTexts) {
        auto label = new QLabel("<a href=\"" + helpText + "\">" + helpText + "</a>");
        label->setWordWrap(true);
        connect(label, &QLabel::linkActivated, this, [this](const QString &link) {
            ui.kUserPrompt->setPlainText(link);
        });
        ui.kHelpArea_2->addWidget(label);
    }

    ui.kHelpArea->hide();

    connect(ui.kAskButton, &QPushButton::clicked, this, &SKGAssistantPluginDockWidget::onAsk);
    connect(ui.kUserPrompt, &QPlainTextEdit::textChanged, this, &SKGAssistantPluginDockWidget::onUserPromptChanged);
    connect(ui.kClearButton, &QPushButton::clicked, this, &SKGAssistantPluginDockWidget::onClear);
    connect(ui.kDisplayReasoning, &SKGShow::stateChanged, this, &SKGAssistantPluginDockWidget::refreshConversation);
    connect(ui.kSaveCurrentAiSession, &QPushButton::clicked, this, &SKGAssistantPluginDockWidget::onSaveCurrentAiSession);
    connect(ui.kDeleteAiSession, &QPushButton::clicked, this, &SKGAssistantPluginDockWidget::onDeleteAiSession);
    connect(ui.kHelpButton, &QPushButton::clicked, this, [this]() {
        ui.kHelpArea->setVisible(!ui.kHelpArea->isVisible());
    });
    connect(ui.kAiSessions, &QComboBox::currentIndexChanged, this, &SKGAssistantPluginDockWidget::onAiSessionChanged);
    connect(ui.kSettingsButton, &QPushButton::clicked, this, [this]() {
        auto *mainPanel = SKGMainPanel::getMainPanel();
        if (mainPanel != nullptr) {
            mainPanel->optionsPreferences(QStringLiteral("Skrooge AI assistant plugin"));
        }
    });

    auto askShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Return), ui.kUserPrompt);
    connect(askShortcut, &QShortcut::activated, this, &SKGAssistantPluginDockWidget::onAsk);

    connect(iDocument, &SKGDocument::modified, this, &SKGAssistantPluginDockWidget::slotRefreshSessionsList);

    refreshSessionsList(false);
}

SKGAssistantPluginDockWidget::~SKGAssistantPluginDockWidget()
{
    if (m_pendingReply != nullptr) {
        m_pendingReply->abort();
    }
}

void SKGAssistantPluginDockWidget::onUserPromptChanged()
{
    ui.kAskButton->setEnabled(!ui.kUserPrompt->toPlainText().trimmed().isEmpty() && m_pendingReply == nullptr);
}

QUrl SKGAssistantPluginDockWidget::getCompletionEndpoint() const
{
    QString urlText = skgassistant_settings::serverUrl().trimmed();
    if (urlText.isEmpty()) {
        return QUrl();
    }
    if (!urlText.contains(QStringLiteral("://"))) {
        urlText = QStringLiteral("http://") + urlText;
    }

    QUrl endpoint(urlText);
    if (!endpoint.isValid()) {
        return endpoint;
    }

    QString path = endpoint.path();
    if (path.isEmpty() || path == QStringLiteral("/")) {
        path = QStringLiteral("/v1/chat/completions");
    } else if (!path.endsWith(QStringLiteral("/v1/chat/completions"))) {
        if (path.endsWith(u'/')) {
            path.chop(1);
        }
        if (!path.endsWith(QStringLiteral("/v1"))) {
            path += QStringLiteral("/v1");
        }
        path += QStringLiteral("/chat/completions");
    }
    endpoint.setPath(path);

    return endpoint;
}

void SKGAssistantPluginDockWidget::setBusy(bool iBusy)
{
    ui.kAskButton->setIcon(SKGServices::fromTheme(iBusy ? QStringLiteral("media-playback-stop") : QStringLiteral("media-playback-start")));
    ui.kClearButton->setEnabled(!iBusy);
    ui.kUserPrompt->setEnabled(!iBusy);
    ui.kAiSessionMngt->setEnabled(!iBusy);
}

QString SKGAssistantPluginDockWidget::getSystemPrompt()
{
    // Read skrooge_system_prompt.md file content
    QString systemPromptPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/skrooge_system_prompt.md"));
    QFile file(systemPromptPath);
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream stream(&file);

        auto systemPrompt = stream.readAll();
        auto userFamilySituation = skgassistant_settings::familySituation().trimmed();
        if (!userFamilySituation.isEmpty()) {
            systemPrompt += "\n\n##  User family situation\n\n" + userFamilySituation;
        }
        return systemPrompt;
    }
    return QString();
}

QString SKGAssistantPluginDockWidget::markdownToHtml(const QString &iContent)
{
    QTextDocument doc;
    doc.setMarkdown(iContent);
    return doc.toHtml();
}

void SKGAssistantPluginDockWidget::onClear()
{
    SKGTRACEINFUNC(10)
    ui.kUserPrompt->clear();
    ui.kAnswer->clear();
    m_messages = QJsonArray();
    ui.kAiSessions->setCurrentIndex(0);
}

void SKGAssistantPluginDockWidget::onAsk()
{
    SKGTRACEINFUNC(10)
    ui.kHelpArea->setVisible(false);
    if (m_pendingReply != nullptr) {
        // Stop the pending request if there is one
        m_pendingReply->abort();
        m_pendingReply = nullptr;
    } else {
        // Start a new request
        const QString question = ui.kUserPrompt->toPlainText().trimmed();
        if (question.isEmpty()) {
            return;
        }

        if (m_messages.isEmpty()) {
            QJsonObject msg;
            msg.insert(QStringLiteral("role"), QStringLiteral("system"));
            msg.insert(QStringLiteral("content"), getSystemPrompt());
            m_messages.push_back(msg);
        }

        // Add user question to the conversation history
        QJsonObject msg;
        msg.insert(QStringLiteral("role"), QStringLiteral("user"));
        msg.insert(QStringLiteral("content"), question);
        m_messages.push_back(msg);
        m_toolCallDepth = 0;

        SKGAssistantPluginDockWidget::refreshConversation();
        sendCompletionRequest();
    }
}

void SKGAssistantPluginDockWidget::sendCompletionRequest()
{
    SKGTRACEINFUNC(10)
    if (m_pendingReply != nullptr) {
        return;
    }

    const QUrl endpoint = getCompletionEndpoint();
    if (!endpoint.isValid()) {
        auto *mainPanel = SKGMainPanel::getMainPanel();
        if (mainPanel != nullptr) {
            mainPanel->displayMessage(skgassistant_settings::serverUrl().trimmed().isEmpty()
                                          ? i18nc("Error message", "Server URL is empty. Please configure it in the settings.")
                                          : i18nc("Error message", "Invalid server URL: %1", endpoint.toString()),
                                      SKGDocument::Error);
        }
        return;
    }

    QJsonObject payload;
    payload.insert(QStringLiteral("messages"), getRequestMessages(m_messages));
    payload.insert(QStringLiteral("temperature"), 1.0);
    payload.insert(QStringLiteral("stream"), false);
    payload.insert(QStringLiteral("tools"), getTools());

    QNetworkRequest request(endpoint);
    request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));

    QString token = skgassistant_settings::apiToken().trimmed();
    if (!token.isEmpty()) {
        request.setRawHeader("Authorization", "Bearer " + token.toUtf8());
    }

    auto payloadString = QJsonDocument(payload).toJson(QJsonDocument::Compact);
    m_pendingReply = m_networkManager.post(request, payloadString);
    connect(m_pendingReply, &QNetworkReply::finished, this, &SKGAssistantPluginDockWidget::onReplyFinished);

    setBusy(true);
}

QJsonArray SKGAssistantPluginDockWidget::getTools()
{
    QJsonArray tools;
    // Add the SQLite SELECT tool
    {
        QJsonObject tool;
        tool.insert(QStringLiteral("type"), QStringLiteral("function"));
        QJsonObject function;
        function.insert(QStringLiteral("name"), QStringLiteral("execute_sqlite_select"));
        function.insert(QStringLiteral("description"),
                        i18nc("Tool description", "Execute a SQLite SELECT query on the current Skrooge database. Only SELECT statements are allowed."));
        QJsonObject parameters;
        parameters.insert(QStringLiteral("type"), QStringLiteral("object"));
        QJsonObject properties;
        QJsonObject sqlProperty;
        sqlProperty.insert(QStringLiteral("type"), QStringLiteral("string"));
        sqlProperty.insert(QStringLiteral("description"), i18nc("Tool parameter description", "The SQL query to execute. Only SELECT statements are allowed."));
        properties.insert(QStringLiteral("sql"), sqlProperty);
        parameters.insert(QStringLiteral("properties"), properties);
        QJsonArray required;
        required.push_back(QStringLiteral("sql"));
        parameters.insert(QStringLiteral("required"), required);
        function.insert(QStringLiteral("parameters"), parameters);
        tool.insert(QStringLiteral("function"), function);
        tools << tool;
    }

    // Add the web search tool if enabled in settings
    if (skgassistant_settings::enableWebSearch()) {
        QJsonObject tool;
        tool.insert(QStringLiteral("type"), QStringLiteral("function"));
        QJsonObject function;
        function.insert(QStringLiteral("name"), QStringLiteral("web_search"));
        function.insert(QStringLiteral("description"), i18nc("Tool description", "Search the internet using DuckDuckGo to find relevant information."));
        QJsonObject parameters;
        parameters.insert(QStringLiteral("type"), QStringLiteral("object"));
        QJsonObject properties;
        QJsonObject queryProperty;
        queryProperty.insert(QStringLiteral("type"), QStringLiteral("string"));
        queryProperty.insert(QStringLiteral("description"), i18nc("Tool parameter description", "The search query to look up on the internet."));
        properties.insert(QStringLiteral("query"), queryProperty);
        QJsonObject maxResultsProperty;
        maxResultsProperty.insert(QStringLiteral("type"), QStringLiteral("integer"));
        maxResultsProperty.insert(QStringLiteral("description"), i18nc("Tool parameter description", "Maximum number of results to return (1-10)."));
        properties.insert(QStringLiteral("max_results"), maxResultsProperty);
        parameters.insert(QStringLiteral("properties"), properties);
        QJsonArray required;
        required.push_back(QStringLiteral("query"));
        parameters.insert(QStringLiteral("required"), required);
        function.insert(QStringLiteral("parameters"), parameters);
        tool.insert(QStringLiteral("function"), function);
        tools << tool;
    }
    return tools;
}

QJsonArray SKGAssistantPluginDockWidget::getRequestMessages(const QJsonArray &iMessages)
{
    QJsonArray requestMessages;
    for (const auto &messageValue : iMessages) {
        if (!messageValue.isObject()) {
            continue;
        }
        QJsonObject message = messageValue.toObject();
        message.remove(QStringLiteral("reasoning_content"));
        requestMessages.push_back(message);
    }
    return requestMessages;
}

QString SKGAssistantPluginDockWidget::contentToText(const QJsonValue &iContent)
{
    if (iContent.isString()) {
        return iContent.toString();
    }

    if (iContent.isArray()) {
        QStringList parts;
        const auto values = iContent.toArray();
        int nb = values.count();
        parts.reserve(nb);
        for (int i = 0; i < nb; ++i) {
            QString part = contentToText(values.at(i));
            if (!part.isEmpty()) {
                parts.push_back(part);
            }
        }
        return parts.join('\n');
    }

    if (iContent.isObject()) {
        QJsonObject obj = iContent.toObject();
        if (obj.contains(QStringLiteral("text"))) {
            return obj.value(QStringLiteral("text")).toString();
        }
        if (obj.contains(QStringLiteral("content"))) {
            return contentToText(obj.value(QStringLiteral("content")));
        }
        if (obj.contains(QStringLiteral("value"))) {
            return contentToText(obj.value(QStringLiteral("value")));
        }
    }
    return QString();
}

QString SKGAssistantPluginDockWidget::extractReasoningFromContent(QString &ioContent)
{
    QStringList reasonings;

    static const QRegularExpression completeThinkTag(QStringLiteral("<think\\b[^>]*>(.*?)</think>"),
                                                     QRegularExpression::DotMatchesEverythingOption | QRegularExpression::CaseInsensitiveOption);
    auto matchIterator = completeThinkTag.globalMatch(ioContent);
    while (matchIterator.hasNext()) {
        const QRegularExpressionMatch match = matchIterator.next();
        const QString reasoning = match.captured(1).trimmed();
        if (!reasoning.isEmpty()) {
            reasonings.push_back(reasoning);
        }
    }
    ioContent.remove(completeThinkTag);

    static const QRegularExpression openThinkTag(QStringLiteral("<think\\b[^>]*>(.*)$"),
                                                 QRegularExpression::DotMatchesEverythingOption | QRegularExpression::CaseInsensitiveOption);
    const QRegularExpressionMatch openMatch = openThinkTag.match(ioContent);
    if (openMatch.hasMatch()) {
        const QString reasoning = openMatch.captured(1).trimmed();
        if (!reasoning.isEmpty()) {
            reasonings.push_back(reasoning);
        }
        ioContent.remove(openMatch.capturedStart(0), ioContent.length() - openMatch.capturedStart(0));
    }

    ioContent = ioContent.trimmed();
    return reasonings.join(QStringLiteral("\n\n")).trimmed();
}

QString SKGAssistantPluginDockWidget::reasoningContentToText(const QJsonObject &iMessage, const QJsonObject &iChoice)
{
    QString reasoning = contentToText(iMessage.value(QStringLiteral("reasoning_content"))).trimmed();
    if (reasoning.isEmpty()) {
        reasoning = contentToText(iChoice.value(QStringLiteral("reasoning_content"))).trimmed();
    }
    if (reasoning.isEmpty()) {
        reasoning = contentToText(iChoice.value(QStringLiteral("reasoning"))).trimmed();
    }
    if (reasoning.isEmpty()) {
        const QJsonObject delta = iChoice.value(QStringLiteral("delta")).toObject();
        reasoning = contentToText(delta.value(QStringLiteral("reasoning_content"))).trimmed();
    }
    if (reasoning.isEmpty()) {
        QString content = contentToText(iMessage.value(QStringLiteral("content")));
        reasoning = extractReasoningFromContent(content);
    }
    if (reasoning.isEmpty()) {
        QString content = contentToText(iChoice.value(QStringLiteral("text")));
        reasoning = extractReasoningFromContent(content);
    }
    if (reasoning.isEmpty()) {
        QString content = contentToText(iChoice.value(QStringLiteral("delta")));
        reasoning = extractReasoningFromContent(content);
    }
    return reasoning;
}

QString SKGAssistantPluginDockWidget::sqlResultToJson(const SKGStringListList &iResult)
{
    QJsonObject output;
    if (iResult.isEmpty()) {
        output.insert(QStringLiteral("columns"), QJsonArray());
        output.insert(QStringLiteral("rows"), QJsonArray());
        output.insert(QStringLiteral("row_count"), 0);
        return QString::fromUtf8(QJsonDocument(output).toJson(QJsonDocument::Compact));
    }

    QJsonArray columns;
    const QStringList columnNames = iResult.at(0);
    for (const auto &columnName : columnNames) {
        columns.push_back(columnName);
    }
    output.insert(QStringLiteral("columns"), columns);

    QJsonArray rows;
    for (int rowIndex = 1; rowIndex < iResult.count(); ++rowIndex) {
        const QStringList values = iResult.at(rowIndex);
        QJsonObject row;
        for (int columnIndex = 0; columnIndex < columnNames.count(); ++columnIndex) {
            row.insert(columnNames.at(columnIndex), values.value(columnIndex));
        }
        rows.push_back(row);
    }
    output.insert(QStringLiteral("rows"), rows);
    output.insert(QStringLiteral("row_count"), rows.count());
    return QString::fromUtf8(QJsonDocument(output).toJson(QJsonDocument::Compact));
}

QString SKGAssistantPluginDockWidget::executeSqliteSelect(const QString &iSql) const
{
    QString sql = iSql.trimmed();
    while (sql.endsWith(u';')) {
        sql.chop(1);
        sql = sql.trimmed();
    }

    static QRegularExpression selectOnly(QStringLiteral("^\\s*SELECT\\b"), QRegularExpression::CaseInsensitiveOption);
    if (!selectOnly.match(sql).hasMatch() || sql.contains(u';')) {
        QJsonObject error;
        error.insert(QStringLiteral("error"), i18nc("Error message", "Only a single SQLite SELECT statement is allowed."));
        return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
    }

    SKGStringListList result;
    auto *mainPanel = SKGMainPanel::getMainPanel();
    auto *document = mainPanel != nullptr ? mainPanel->getDocument() : nullptr;
    if (document == nullptr) {
        QJsonObject error;
        error.insert(QStringLiteral("error"), i18nc("Error message", "No document is opened."));
        return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
    }

    auto err = document->executeSelectSqliteOrder(sql, result);
    if (err.isFailed()) {
        QJsonObject error;
        error.insert(QStringLiteral("error"), err.getFullMessage());
        return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
    }

    return sqlResultToJson(result);
}

QString SKGAssistantPluginDockWidget::executeWebSearch(const QString &iQuery, int iMaxResults) const
{
    if (iQuery.trimmed().isEmpty()) {
        QJsonObject error;
        error.insert(QStringLiteral("error"), i18nc("Error message", "Search query cannot be empty."));
        return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
    }

    QUrl searchUrl(QStringLiteral("https://api.duckduckgo.com/"));
    QUrlQuery query;
    query.addQueryItem(QStringLiteral("q"), iQuery);
    query.addQueryItem(QStringLiteral("format"), QStringLiteral("json"));
    query.addQueryItem(QStringLiteral("no_html"), QStringLiteral("1"));
    searchUrl.setQuery(query);

    QNetworkRequest request(searchUrl);
    request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("Skrooge-AI-Assistant"));

    QEventLoop eventLoop;
    QPointer<QNetworkReply> reply = const_cast<QNetworkAccessManager *>(&m_networkManager)->get(request);
    connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);

    eventLoop.exec();

    if (reply == nullptr || reply->error() != QNetworkReply::NoError) {
        QJsonObject error;
        error.insert(QStringLiteral("error"),
                     i18nc("Error message", "Web search failed: %1", reply != nullptr ? reply->errorString() : QStringLiteral("Unknown error")));
        if (reply != nullptr) {
            reply->deleteLater();
        }
        return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
    }

    QByteArray responseData = reply->readAll();
    reply->deleteLater();

    QJsonParseError parseError{};
    QJsonDocument responseDoc = QJsonDocument::fromJson(responseData, &parseError);
    if (parseError.error != QJsonParseError::NoError || !responseDoc.isObject()) {
        QJsonObject error;
        error.insert(QStringLiteral("error"), i18nc("Error message", "Failed to parse search results."));
        return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
    }

    QJsonObject responseObj = responseDoc.object();
    QJsonArray results;

    // Extract abstract result if available
    QString abstractText = responseObj.value(QStringLiteral("AbstractText")).toString().trimmed();
    QString abstractSource = responseObj.value(QStringLiteral("AbstractSource")).toString().trimmed();
    QString abstractUrl = responseObj.value(QStringLiteral("AbstractURL")).toString().trimmed();

    if (!abstractText.isEmpty()) {
        QJsonObject result;
        result.insert(QStringLiteral("title"), abstractSource.isEmpty() ? QStringLiteral("Summary") : abstractSource);
        result.insert(QStringLiteral("snippet"), abstractText);
        if (!abstractUrl.isEmpty()) {
            result.insert(QStringLiteral("url"), abstractUrl);
        }
        results.push_back(result);
    }

    // Extract related searches as additional results
    QJsonArray relatedSearches = responseObj.value(QStringLiteral("RelatedTopics")).toArray();
    int resultCount = results.count();
    for (const QJsonValue &topicValue : relatedSearches) {
        if (resultCount >= iMaxResults) {
            break;
        }

        if (topicValue.isObject()) {
            QJsonObject topic = topicValue.toObject();
            QString firstUrl = topic.value(QStringLiteral("FirstURL")).toString().trimmed();
            QString text = topic.value(QStringLiteral("Text")).toString().trimmed();

            if (!text.isEmpty()) {
                QJsonObject result;
                result.insert(QStringLiteral("title"), topic.value(QStringLiteral("Name")).toString());
                result.insert(QStringLiteral("snippet"), text);
                if (!firstUrl.isEmpty()) {
                    result.insert(QStringLiteral("url"), firstUrl);
                }
                results.push_back(result);
                ++resultCount;
            }
        }
    }

    if (results.isEmpty()) {
        QJsonObject output;
        output.insert(QStringLiteral("results"), QJsonArray());
        output.insert(QStringLiteral("result_count"), 0);
        output.insert(QStringLiteral("query"), iQuery);
        return QString::fromUtf8(QJsonDocument(output).toJson(QJsonDocument::Compact));
    }

    QJsonObject output;
    output.insert(QStringLiteral("results"), results);
    output.insert(QStringLiteral("result_count"), results.count());
    output.insert(QStringLiteral("query"), iQuery);
    return QString::fromUtf8(QJsonDocument(output).toJson(QJsonDocument::Compact));
}

QString SKGAssistantPluginDockWidget::executeToolCall(const QJsonObject &iToolCall) const
{
    const QJsonObject function = iToolCall.value(QStringLiteral("function")).toObject();
    const QString name = function.value(QStringLiteral("name")).toString();
    const QString arguments = function.value(QStringLiteral("arguments")).toString();

    QJsonParseError argumentsParseError{};
    QJsonDocument argumentsDoc = QJsonDocument::fromJson(arguments.toUtf8(), &argumentsParseError);
    QJsonObject argumentsObj;
    if (!arguments.isEmpty()) {
        if (argumentsParseError.error != QJsonParseError::NoError || !argumentsDoc.isObject()) {
            QJsonObject error;
            error.insert(QStringLiteral("error"), i18nc("Error message", "Invalid tool call arguments."));
            return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
        }
        argumentsObj = argumentsDoc.object();
    }

    if (name == QStringLiteral("execute_sqlite_select")) {
        return executeSqliteSelect(argumentsObj.value(QStringLiteral("sql")).toString());
    }

    if (name == QStringLiteral("web_search")) {
        int maxResults = argumentsObj.value(QStringLiteral("max_results")).toInt(5);
        if (maxResults < 1 || maxResults > 10) {
            maxResults = 5;
        }
        return executeWebSearch(argumentsObj.value(QStringLiteral("query")).toString(), maxResults);
    }

    QJsonObject error;
    error.insert(QStringLiteral("error"), i18nc("Error message", "Unknown tool: %1", name));
    return QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Compact));
}

QString SKGAssistantPluginDockWidget::extractAssistantMessage(const QByteArray &iPayload, QString &oErrorMessage, bool &oNeedsFollowUp)
{
    oNeedsFollowUp = false;
    m_pendingReasoningContent.clear();
    QJsonParseError parseError{};
    QJsonDocument response = QJsonDocument::fromJson(iPayload, &parseError);
    if (parseError.error != QJsonParseError::NoError || !response.isObject()) {
        oErrorMessage = i18nc("Error message", "Invalid JSON response from server.");
        return QString();
    }

    const QJsonObject root = response.object();
    if (root.contains(QStringLiteral("error"))) {
        const QJsonValue errorValue = root.value(QStringLiteral("error"));
        QString details = contentToText(errorValue);
        if (details.isEmpty() && errorValue.isObject()) {
            details = errorValue.toObject().value(QStringLiteral("message")).toString();
        }
        if (details.isEmpty()) {
            details = QString::fromUtf8(iPayload);
        }
        oErrorMessage = i18nc("Error message", "Server error: %1", details);
        return QString();
    }

    QJsonArray choices = root.value(QStringLiteral("choices")).toArray();
    if (choices.isEmpty()) {
        oErrorMessage = i18nc("Error message", "No answer returned by the model.");
        return QString();
    }

    const QJsonObject firstChoice = choices.at(0).toObject();
    QJsonObject message = firstChoice.value(QStringLiteral("message")).toObject();
    m_pendingReasoningContent = reasoningContentToText(message, firstChoice);
    QJsonArray toolCalls = message.value(QStringLiteral("tool_calls")).toArray();
    if (toolCalls.isEmpty()) {
        toolCalls = firstChoice.value(QStringLiteral("tool_calls")).toArray();
    }

    if (!toolCalls.isEmpty()) {
        ++m_toolCallDepth;
        if (m_toolCallDepth > 8) {
            oErrorMessage = i18nc("Error message", "Too many consecutive tool calls.");
            return QString();
        }

        if (message.isEmpty()) {
            message.insert(QStringLiteral("role"), QStringLiteral("assistant"));
            message.insert(QStringLiteral("content"), QJsonValue::Null);
            message.insert(QStringLiteral("tool_calls"), toolCalls);
        }
        if (!m_pendingReasoningContent.isEmpty() && !message.contains(QStringLiteral("reasoning_content"))) {
            message.insert(QStringLiteral("reasoning_content"), m_pendingReasoningContent);
        }
        m_messages.push_back(message);

        for (const auto &toolCallValue : toolCalls) {
            const QJsonObject toolCall = toolCallValue.toObject();
            QString toolCallId = toolCall.value(QStringLiteral("id")).toString();
            if (toolCallId.isEmpty()) {
                toolCallId = toolCall.value(QStringLiteral("tool_id")).toString();
            }

            QJsonObject toolResult;
            toolResult.insert(QStringLiteral("role"), QStringLiteral("tool"));
            toolResult.insert(QStringLiteral("tool_call_id"), toolCallId);
            toolResult.insert(QStringLiteral("content"), executeToolCall(toolCall));
            m_messages.push_back(toolResult);
        }

        oNeedsFollowUp = true;
        return QString();
    }

    QString answer = contentToText(message.value(QStringLiteral("content"))).trimmed();
    if (answer.isEmpty()) {
        answer = contentToText(firstChoice.value(QStringLiteral("text"))).trimmed();
    }
    if (answer.isEmpty()) {
        answer = contentToText(firstChoice.value(QStringLiteral("delta"))).trimmed();
    }
    const QString reasoningFromAnswer = extractReasoningFromContent(answer);
    if (!reasoningFromAnswer.isEmpty()) {
        if (m_pendingReasoningContent.isEmpty()) {
            m_pendingReasoningContent = reasoningFromAnswer;
        } else if (!m_pendingReasoningContent.contains(reasoningFromAnswer)) {
            m_pendingReasoningContent += QStringLiteral("\n\n") + reasoningFromAnswer;
        }
    }
    if (answer.isEmpty()) {
        oErrorMessage = i18nc("Error message", "The model returned an empty answer.");
    }
    return answer;
}

void SKGAssistantPluginDockWidget::onReplyFinished()
{
    SKGTRACEINFUNC(10)
    QPointer<QNetworkReply> reply = m_pendingReply;
    m_pendingReply = nullptr;
    setBusy(false);

    if (reply == nullptr) {
        return;
    }

    const QByteArray payload = reply->readAll();
    QString serverMessage;
    bool needsFollowUp = false;
    QString answer = extractAssistantMessage(payload, serverMessage, needsFollowUp);

    if (reply->error() == QNetworkReply::ConnectionRefusedError) {
        auto *mainPanel = SKGMainPanel::getMainPanel();
        if (mainPanel != nullptr) {
            mainPanel->displayMessage(i18nc("Error message", "Connection refused. Are you sure the server is running ?"), SKGDocument::Error);
        }
        needsFollowUp = false;
    } else if (reply->error() != QNetworkReply::NoError) {
        QString errorMsg = i18nc("Error message", "Network error (%1): %2", reply->error(), reply->errorString());
        if (!serverMessage.isEmpty()) {
            errorMsg += QStringLiteral("\n") + serverMessage;
        } else if (!payload.isEmpty()) {
            errorMsg += QStringLiteral("\n") + QString::fromUtf8(payload);
        }
        auto *mainPanel = SKGMainPanel::getMainPanel();
        if (mainPanel != nullptr) {
            mainPanel->displayMessage(errorMsg, SKGDocument::Error);
        }
        needsFollowUp = false;
    } else if (answer.isEmpty() && !serverMessage.isEmpty()) {
        auto *mainPanel = SKGMainPanel::getMainPanel();
        if (mainPanel != nullptr) {
            mainPanel->displayMessage(serverMessage, SKGDocument::Error);
        }
    }

    refreshConversation();

    reply->deleteLater();

    if (needsFollowUp) {
        sendCompletionRequest();
        return;
    }

    if (!answer.isEmpty()) {
        // Add assistant answer to the conversation history
        QJsonObject msg;
        msg.insert(QStringLiteral("role"), QStringLiteral("assistant"));
        msg.insert(QStringLiteral("content"), answer);
        if (!m_pendingReasoningContent.isEmpty()) {
            msg.insert(QStringLiteral("reasoning_content"), m_pendingReasoningContent);
        }
        m_messages.push_back(msg);
        refreshConversation();
    }

    ui.kUserPrompt->clear();
}

void SKGAssistantPluginDockWidget::refreshConversation() const
{
    ui.kSaveCurrentAiSession->setEnabled(!m_messages.isEmpty() && ui.kAiSessions->currentIndex() == 0);

    // Build the answer text from the conversation history
    auto displayThinking = SKGServices::stringToInt(ui.kDisplayReasoning->getState().replace(QStringLiteral("\""), QStringLiteral("")));
    QStringList parts;
    QString lastReasoningContent;
    for (const QJsonValue &messageValue : m_messages) {
        if (!messageValue.isObject()) {
            continue;
        }
        QJsonObject messageObj = messageValue.toObject();
        QString role = messageObj.value(QStringLiteral("role")).toString();
        QString content = contentToText(messageObj.value(QStringLiteral("content"))).trimmed();
        QString reasoning = contentToText(messageObj.value(QStringLiteral("reasoning_content"))).trimmed();

        if (role == QStringLiteral("user")) {
            parts.push_back(i18nc("User question prefix", "<b>You:</b>%1", markdownToHtml(content)));
        } else if (role == QStringLiteral("assistant") && (!content.isEmpty() || !reasoning.isEmpty())) {
            QStringList assistantParts;
            if (!reasoning.isEmpty() && displayThinking > 0) {
                // Build raisoning
                lastReasoningContent = displayThinking == 1 ? i18nc("Assistant reasoning prefix", "<b>Reasoning...</b>")
                                                            : i18nc("Assistant reasoning prefix", "<b>Reasoning:</b>%1", markdownToHtml(reasoning));
                if (displayThinking >= 3) {
                    // Display all reasoning
                    assistantParts.push_back("<font color='gray' size='2'>" + lastReasoningContent + "</font>");
                    lastReasoningContent.clear();
                }
            }
            if (!content.isEmpty()) {
                assistantParts.push_back(i18nc("Assistant answer prefix", "<b>AI:</b>%1", markdownToHtml(content)));
                lastReasoningContent.clear();
            }
            if (!assistantParts.isEmpty()) {
                parts.push_back(assistantParts.join(QStringLiteral("\n\n")));
            }
        } else if (role == QStringLiteral("tool") && displayThinking == 4) {
            parts.push_back("<font color='blue' size='2'>" + i18nc("Tool answer prefix", "<b>Tool:</b>%1", markdownToHtml(content)) + "</font>");
        }
    }

    // Add last reasoning content if any
    if (lastReasoningContent.isEmpty() == false) {
        parts.push_back("<font color='gray' size='2'>" + lastReasoningContent + "</font>");
    }
    ui.kAnswer->setHtml(parts.join(""));
    ui.kAnswer->moveCursor(QTextCursor::End);
}

void SKGAssistantPluginDockWidget::onSaveCurrentAiSession()
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err)
    auto document = getDocument();
    SKGBEGINTRANSACTION(*document, i18nc("Noun, name of the user action", "Save AI Session"), err);
    IFOKDO(err,
           document->setParameter(PREFIX_AIASSISTANT_SESSION + SKGServices::timeToString(QDateTime::currentDateTime()),
                                  QJsonDocument(m_messages).toJson(QJsonDocument::Compact)));
    refreshSessionsList(true);
}

void SKGAssistantPluginDockWidget::onDeleteAiSession()
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err)
    auto document = getDocument();
    SKGBEGINTRANSACTION(*document, i18nc("Noun, name of the user action", "Delete AI Session"), err);
    SKGPropertyObject param(document);
    IFOKDO(err, param.setName(PREFIX_AIASSISTANT_SESSION + ui.kAiSessions->currentText()));
    IFOKDO(err, param.load());
    IFOKDO(err, param.remove());
    refreshSessionsList();
}

void SKGAssistantPluginDockWidget::onAiSessionChanged(int iIndex)
{
    auto document = getDocument();
    int index = ui.kAiSessions->currentIndex();
    if (index <= 0) {
        onClear();
        ui.kDeleteAiSession->setEnabled(false);
    } else {
        QString json = document->getParameter(PREFIX_AIASSISTANT_SESSION + ui.kAiSessions->currentText());
        m_messages = QJsonDocument::fromJson(json.toUtf8()).array();
        refreshConversation();
        ui.kDeleteAiSession->setEnabled(true);
    }
}

void SKGAssistantPluginDockWidget::slotRefreshSessionsList() const
{
    refreshSessionsList(false);
}

void SKGAssistantPluginDockWidget::refreshSessionsList(bool iSelectFirst) const
{
    SKGTRACEINFUNC(10)
    auto document = getDocument();
    ui.kAiSessions->clear();
    ui.kAiSessions->addItem(i18nc("AI session list", "New session"));
    // Get all document parameters and filter those starting with "SKG_AI_SESSION_"
    auto list = document->getParameters("document", "t_name like '" + PREFIX_AIASSISTANT_SESSION + "%'");
    std::sort(list.begin(), list.end());
    std::reverse(list.begin(), list.end());
    for (const QString &aiSession : list) {
        ui.kAiSessions->addItem(aiSession.right(aiSession.length() - PREFIX_AIASSISTANT_SESSION.length()));
    }
    if (iSelectFirst && ui.kAiSessions->count() > 1) {
        ui.kAiSessions->setCurrentIndex(1);
    }
}