/*
 * Copyright (C) 2023 The Qt Company Ltd.
 * Copyright (C) 2024-2026 Petr Mironychev
 *
 * This file is part of QodeAssist.
 *
 * The Qt Company portions:
 * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
 *
 * Petr Mironychev portions:
 * QodeAssist is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * QodeAssist is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
 *
 * Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
 */

#include "QodeAssistClient.hpp"

#include <QApplication>
#include <QInputDialog>
#include <QKeyEvent>
#include <QTimer>

#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h>
#include <projectexplorer/projectmanager.h>

#include "LLMClientInterface.hpp"
#include "LLMSuggestion.hpp"
#include "RefactorSuggestion.hpp"
#include "RefactorSuggestionHoverHandler.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include "RefactorContextHelper.hpp"
#include <context/ChangesManager.h>
#include <logger/Logger.hpp>

using namespace LanguageServerProtocol;
using namespace TextEditor;
using namespace Utils;
using namespace ProjectExplorer;
using namespace Core;

namespace QodeAssist {

namespace {
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
{
    return Utils::Text::Position{pos.line, pos.column};
}

bool isIdentifierChar(QChar c)
{
    return c.isLetterOrNumber() || c == QLatin1Char('_');
}

bool isInsideIdentifier(const QTextCursor &cursor)
{
    const QTextBlock block = cursor.block();
    const int col = cursor.positionInBlock();
    const QString text = block.text();
    if (col <= 0 || col > text.size())
        return false;
    if (!isIdentifierChar(text.at(col - 1)))
        return false;
    return col < text.size() && isIdentifierChar(text.at(col));
}

bool isAfterMemberAccess(const QTextCursor &cursor)
{
    const QTextBlock block = cursor.block();
    const int col = cursor.positionInBlock();
    const QString text = block.text();
    if (col <= 0)
        return false;

    int i = col - 1;
    while (i >= 0 && isIdentifierChar(text.at(i)))
        --i;

    if (i < 0)
        return false;

    const QChar c = text.at(i);
    if (c == QLatin1Char('.'))
        return true;
    if (c == QLatin1Char('>') && i >= 1 && text.at(i - 1) == QLatin1Char('-'))
        return true;
    if (c == QLatin1Char(':') && i >= 1 && text.at(i - 1) == QLatin1Char(':'))
        return true;
    return false;
}

bool isFreshIndentedLine(const QTextCursor &cursor)
{
    const QTextBlock block = cursor.block();
    const int col = cursor.positionInBlock();
    if (col == 0)
        return false;
    const QString leftText = block.text().left(col);
    for (const QChar &ch : leftText) {
        if (!ch.isSpace())
            return false;
    }
    return true;
}

bool isAfterEagerTrigger(const QTextCursor &cursor)
{
    const QTextBlock block = cursor.block();
    const int col = cursor.positionInBlock();
    const QString text = block.text();
    int i = col - 1;
    while (i >= 0 && text.at(i).isSpace())
        --i;
    if (i < 0)
        return false;
    const QChar c = text.at(i);
    return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(',')
           || c == QLatin1Char('=') || c == QLatin1Char('[') || c == QLatin1Char(';')
           || c == QLatin1Char(':') || c == QLatin1Char('>');
}

bool isManualMode()
{
    return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
}
} // anonymous namespace

QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
    : LanguageClient::Client(clientInterface)
    , m_llmClient(clientInterface)
    , m_recentCharCount(0)
{
    setName("QodeAssist");
    LanguageClient::LanguageFilter filter;
    filter.mimeTypes = QStringList() << "*";
    setSupportedLanguage(filter);

    start();
    setupConnections();

    m_typingTimer.start();

    m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
    m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}

QodeAssistClient::~QodeAssistClient()
{
    cleanupConnections();
    delete m_refactorHoverHandler;
    delete m_refactorWidgetHandler;
}

void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
{
    auto project = ProjectManager::projectForFile(document->filePath());
    if (!isEnabled(project))
        return;

    Client::openDocument(document);

    auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
    for (auto *editor : editors) {
        if (auto *widget = editor->editorWidget()) {
            widget->addHoverHandler(m_refactorHoverHandler);
            widget->installEventFilter(this);
        }
    }

    connect(
        document,
        &TextDocument::contentsChangedWithPosition,
        this,
        [this, document](int position, int charsRemoved, int charsAdded) {
            if (!Settings::codeCompletionSettings().autoCompletion())
                return;

            if (isManualMode())
                return;

            auto project = ProjectManager::projectForFile(document->filePath());
            if (!isEnabled(project))
                return;

            auto textEditor = BaseTextEditor::currentTextEditor();
            if (!textEditor || textEditor->document() != document)
                return;

            if (Settings::codeCompletionSettings().useProjectChangesCache())
                Context::ChangesManager::instance()
                    .addChange(document, position, charsRemoved, charsAdded);

            TextEditorWidget *widget = textEditor->editorWidget();
            if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
                return;

            const int cursorPosition = widget->textCursor().position();
            if (cursorPosition < position || cursorPosition > position + charsAdded)
                return;

            if (charsRemoved > 0 || charsAdded <= 0) {
                m_recentCharCount = 0;
                m_typingTimer.restart();
                return;
            }

            QTextCursor cursor = widget->textCursor();
            cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
            const QString lastChar = cursor.selectedText();
            if (lastChar.isEmpty())
                return;

            const QChar lastCh = lastChar[0];
            if (lastCh == QLatin1Char('\n') || lastCh == QChar::ParagraphSeparator
                || lastCh == QChar::LineSeparator) {
                m_recentCharCount = 0;
                m_typingTimer.restart();
                return;
            }

            const bool isSpaceOrTab = lastCh.isSpace();
            const bool ignoreWhitespace
                = Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();

            if (!ignoreWhitespace || !isSpaceOrTab)
                m_recentCharCount += charsAdded;

            if (m_typingTimer.elapsed()
                > Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
                m_recentCharCount = (ignoreWhitespace && isSpaceOrTab) ? 0 : charsAdded;
                m_typingTimer.restart();
            }

            handleAutoRequestTrigger(widget);
        });
}

bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
{
    return isEnabled(project);
}

void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
{
    auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());

    if (!isEnabled(project))
        return;


    if (m_llmClient->contextManager()
            ->ignoreManager()
            ->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
        LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
                        .arg(editor->textDocument()->filePath().toUrlishString()));
        return;
    }

    MultiTextCursor cursor = editor->multiTextCursor();
    if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
        return;

    const auto &settings = Settings::codeCompletionSettings();
    if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
        editor->abortAssist();

    const FilePath filePath = editor->textDocument()->filePath();
    GetCompletionRequest request{
        {TextDocumentIdentifier(hostPathToServerUri(filePath)),
         documentVersion(filePath),
         Position(cursor.mainCursor())}};
    if (Settings::codeCompletionSettings().showProgressWidget()) {
        m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
            if (editor) {
                cancelRunningRequest(editor);
            }
        });
        m_progressHandler.showProgress(editor);
    }
    request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
                                    const GetCompletionRequest::Response &response) {
        QTC_ASSERT(editor, return);
        handleCompletions(response, editor);
    });
    m_runningRequests[editor] = request;
    sendMessage(request);
}

void QodeAssistClient::requestQuickRefactor(
    TextEditor::TextEditorWidget *editor, const QString &instructions)
{
    auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());

    if (!isEnabled(project))
        return;

    if (m_llmClient->contextManager()
            ->ignoreManager()
            ->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
        LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
                        .arg(editor->textDocument()->filePath().toUrlishString()));
        return;
    }

    if (!m_refactorHandler) {
        m_refactorHandler = new QuickRefactorHandler(this);
        connect(
            m_refactorHandler,
            &QuickRefactorHandler::refactoringCompleted,
            this,
            &QodeAssistClient::handleRefactoringResult);
    }

    m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
        if (editor && m_refactorHandler) {
            m_refactorHandler->cancelRequest();
            m_progressHandler.hideProgress();
        }
    });
    m_progressHandler.showProgress(editor);
    m_refactorHandler->sendRefactorRequest(editor, instructions);
}

void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
{
    if (m_runningRequests.contains(editor)) {
        if (Settings::codeCompletionSettings().cancelOnInput())
            cancelRunningRequest(editor);
        else
            return;
    }

    auto it = m_scheduledRequests.find(editor);
    if (it == m_scheduledRequests.end()) {
        auto timer = new QTimer(this);
        timer->setSingleShot(true);
        connect(timer, &QTimer::timeout, this, [this, editor]() {
            if (!editor || m_runningRequests.contains(editor))
                return;
            if (editor->textCursor().position()
                    != m_scheduledRequests[editor]->property("cursorPosition").toInt())
                return;
            requestCompletions(editor);
        });
        connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
            delete m_scheduledRequests.take(editor);
            cancelRunningRequest(editor);
        });
        it = m_scheduledRequests.insert(editor, timer);
    }

    it.value()->setProperty("cursorPosition", editor->textCursor().position());
    it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
}
void QodeAssistClient::handleCompletions(
    const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
{
    m_progressHandler.hideProgress();
    const auto &settings = Settings::codeCompletionSettings();
    if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
        editor->abortAssist();

    if (response.error()) {
        log(*response.error());
        m_errorHandler
            .showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
        return;
    }

    int requestPosition = -1;
    if (const auto requestParams = m_runningRequests.take(editor).params())
        requestPosition = requestParams->position().toPositionInDocument(editor->document());

    const MultiTextCursor cursors = editor->multiTextCursor();
    if (cursors.hasMultipleCursors() || cursors.hasSelection())
        return;

    const int currentPosition = cursors.mainCursor().position();
    if (requestPosition < 0 || currentPosition < requestPosition)
        return;

    QString typedSinceRequest;
    if (currentPosition > requestPosition) {
        QTextCursor diffCursor(editor->document());
        diffCursor.setPosition(requestPosition);
        diffCursor.setPosition(currentPosition, QTextCursor::KeepAnchor);
        typedSinceRequest = diffCursor.selectedText();
        if (typedSinceRequest.contains(QChar::ParagraphSeparator)
            || typedSinceRequest.contains(QLatin1Char('\n'))) {
            return;
        }
    }

    if (const std::optional<GetCompletionResponse> result = response.result()) {
        auto isValidCompletion = [](const Completion &completion) {
            return completion.isValid() && !completion.text().trimmed().isEmpty();
        };
        QList<Completion> completions
            = Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);

        QList<Completion> matchedCompletions;
        matchedCompletions.reserve(completions.size());
        for (Completion &completion : completions) {
            const LanguageServerProtocol::Range range = completion.range();
            if (range.start().line() != range.end().line())
                continue;

            QString completionText = completion.text();
            const int end = int(completionText.size()) - 1;
            int delta = 0;
            while (delta <= end && completionText[end - delta].isSpace())
                ++delta;
            if (delta > 0)
                completionText.chop(delta);

            if (!typedSinceRequest.isEmpty()) {
                if (!completionText.startsWith(typedSinceRequest))
                    continue;
                completionText = completionText.mid(typedSinceRequest.size());
                if (completionText.isEmpty())
                    continue;
            }

            completion.setText(completionText);
            matchedCompletions.append(completion);
        }

        if (matchedCompletions.isEmpty()) {
            LOG_MESSAGE("No valid completions received");
            return;
        }

        const Text::Position anchor = typedSinceRequest.isEmpty()
            ? Text::Position{}
            : Text::Position::fromPositionInDocument(editor->document(), currentPosition);
        const bool useAnchor = !typedSinceRequest.isEmpty();

        auto suggestions = Utils::transform(matchedCompletions,
                                            [useAnchor, &anchor](const Completion &c) {
            auto toTextPos = [](const LanguageServerProtocol::Position pos) {
                return Text::Position{pos.line() + 1, pos.character()};
            };

            if (useAnchor) {
                return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
            }

            Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
            Text::Position pos{toTextPos(c.position())};
            return TextSuggestion::Data{range, pos, c.text()};
        });

        editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
    }
}

void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor)
{
    const auto it = m_runningRequests.constFind(editor);
    if (it == m_runningRequests.constEnd())
        return;
    m_progressHandler.hideProgress();
    cancelRequest(it->id());
    m_runningRequests.erase(it);
}

bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
{
    if (!project)
        return Settings::generalSettings().enableQodeAssist();

    Settings::ProjectSettings settings(project);
    return settings.isEnabled();
}

void QodeAssistClient::setupConnections()
{
    auto openDoc = [this](IDocument *document) {
        if (auto *textDocument = qobject_cast<TextDocument *>(document))
            openDocument(textDocument);
    };

    m_documentOpenedConnection
        = connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc);
    m_documentClosedConnection = connect(
        EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) {
            if (auto textDocument = qobject_cast<TextDocument *>(document))
                closeDocument(textDocument);
        });

    for (IDocument *doc : DocumentModel::openedDocuments())
        openDoc(doc);
}

void QodeAssistClient::cleanupConnections()
{
    disconnect(m_documentOpenedConnection);
    disconnect(m_documentClosedConnection);

    qDeleteAll(m_scheduledRequests);
    m_scheduledRequests.clear();
}

void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
    m_progressHandler.hideProgress();

    if (!result.success) {
        QString errorMessage = result.errorMessage.isEmpty()
                                   ? tr("Quick refactor failed")
                                   : tr("Quick refactor failed: %1").arg(result.errorMessage);

        if (result.editor) {
            m_errorHandler.showError(result.editor, errorMessage);
        }

        LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
        return;
    }

    if (!result.editor) {
        LOG_MESSAGE("Refactoring result has no editor");
        return;
    }

    int displayMode = Settings::quickRefactorSettings().displayMode();
    
    if (displayMode == 0) {
        displayRefactoringWidget(result);
    } else {
        displayRefactoringSuggestion(result);
    }
}

void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
{
    TextEditorWidget *editorWidget = result.editor;

    Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
    Utils::Text::Position pos = toTextPos(result.insertRange.begin);

    int startPos = range.begin.toPositionInDocument(editorWidget->document());
    int endPos = range.end.toPositionInDocument(editorWidget->document());

    if (startPos != endPos) {
        QTextCursor startCursor(editorWidget->document());
        startCursor.setPosition(startPos);
        if (startCursor.positionInBlock() > 0) {
            startCursor.movePosition(QTextCursor::StartOfBlock);
        }

        QTextCursor endCursor(editorWidget->document());
        endCursor.setPosition(endPos);
        if (endCursor.positionInBlock() > 0) {
            endCursor.movePosition(QTextCursor::EndOfBlock);
            if (!endCursor.atEnd()) {
                endCursor.movePosition(QTextCursor::NextCharacter);
            }
        }

        Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
            editorWidget->document(), startCursor.position());
        Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
            editorWidget->document(), endCursor.position());

        range = Utils::Text::Range(expandedBegin, expandedEnd);
    }

    TextEditor::TextSuggestion::Data suggestionData{
        Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
        pos,
        result.newText};
    editorWidget->insertSuggestion(
        std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));

    m_refactorHoverHandler->setSuggestionRange(range);

    m_refactorHoverHandler->setApplyCallback([this, editorWidget]() {
        QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier);
        QApplication::sendEvent(editorWidget, &tabEvent);
        m_refactorHoverHandler->clearSuggestionRange();
    });

    m_refactorHoverHandler->setDismissCallback([this, editorWidget]() {
        editorWidget->clearSuggestion();
        m_refactorHoverHandler->clearSuggestionRange();
    });

    LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
}

void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
{
    TextEditorWidget *editorWidget = result.editor;
    Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
    
    RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
    
    QString displayOriginal;
    QString displayRefactored;
    QString textToApply = result.newText;
    
    if (ctx.isInsertion) {
        bool isMultiline = result.newText.contains('\n');
        
        if (isMultiline) {
            displayOriginal = ctx.textBeforeCursor;
            displayRefactored = ctx.textBeforeCursor + result.newText;
        } else {
            displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
            displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
        }
        
        if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
            textToApply = result.newText;
        }
    } else {
        displayOriginal = ctx.originalText;
        displayRefactored = result.newText;
    }

    m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
        applyRefactoringEdit(editorWidget, result.insertRange, editedText);
    });

    m_refactorWidgetHandler->setDeclineCallback([]() {});

    m_refactorWidgetHandler->showRefactorWidget(
        editorWidget, displayOriginal, displayRefactored, range,
        ctx.contextBefore, ctx.contextAfter);
    
    m_refactorWidgetHandler->setTextToApply(textToApply);
}

void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
                                             const Utils::Text::Range &range,
                                             const QString &text)
{
    const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
    const QTextCursor endCursor = range.end.toTextCursor(editor->document());
    const int startPos = startCursor.position();
    const int endPos = endCursor.position();
    
    QTextCursor editCursor(editor->document());
    editCursor.beginEditBlock();

    if (startPos == endPos) {
        bool isMultiline = text.contains('\n');
        editCursor.setPosition(startPos);
        
        if (isMultiline) {
            editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
            editCursor.removeSelectedText();
        }
        
        editCursor.insertText(text);
    } else {
        editCursor.setPosition(startPos);
        editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
        editCursor.removeSelectedText();
        editCursor.insertText(text);
    }

    editCursor.endEditBlock();
}

void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
{
    const QTextCursor cursor = widget->textCursor();
    const auto &settings = Settings::codeCompletionSettings();
    const bool smart = settings.smartContextTrigger();

    if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
        return;

    const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
    const int charThreshold = settings.autoCompletionCharThreshold();

    if (eager || m_recentCharCount > charThreshold)
        scheduleRequest(widget);
}

bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
{
    auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
    if (!editor)
        return LanguageClient::Client::eventFilter(watched, event);

    if (event->type() == QEvent::KeyPress) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Escape) {
            if (m_runningRequests.contains(editor)) {
                cancelRunningRequest(editor);
            }

            if (m_scheduledRequests.contains(editor)) {
                auto *timer = m_scheduledRequests.value(editor);
                if (timer && timer->isActive()) {
                    timer->stop();
                }
            }

            if (m_refactorHandler && m_refactorHandler->isProcessing()) {
                m_refactorHandler->cancelRequest();
            }

            m_progressHandler.hideProgress();
        }
    }

    return LanguageClient::Client::eventFilter(watched, event);
}

} // namespace QodeAssist
