// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE

#include "EditFileTool.hpp"

#include <LLMQore/ToolExceptions.hpp>

#include <context/ChangesManager.h>
#include <context/ProjectUtils.hpp>
#include <logger/Logger.hpp>
#include <settings/GeneralSettings.hpp>
#include <settings/ToolsSettings.hpp>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QtConcurrent>

namespace QodeAssist::Tools {

EditFileTool::EditFileTool(QObject *parent)
    : BaseTool(parent)
{}

QString EditFileTool::id() const
{
    return "edit_file";
}

QString EditFileTool::displayName() const
{
    return {"Editing file"};
}

QString EditFileTool::description() const
{
    return "Edit a file by replacing old content with new content. "
           "Provide the file path (absolute or relative to project root), old_content to find and replace, "
           "and new_content to replace it with. Changes are applied immediately if auto-apply "
           "is enabled in settings. The user can undo or reapply changes at any time. "
           "\n\nIMPORTANT:"
           "\n- ALWAYS use read_file to get current file content before editing to ensure accuracy."
           "\n- Path can be absolute (e.g., /path/to/file.cpp) or relative to project root (e.g., src/main.cpp)."
           "\n- For EMPTY files: use empty old_content (empty string or omit parameter)."
           "\n- To append at the END of file: use empty old_content."
           "\n- To insert at the BEGINNING of a file (e.g., copyright header), you MUST provide "
           "the EXACT first few lines of the file as old_content (at least 3-5 lines), "
           "then put those lines + new header in new_content."
           "\n- For replacements in the middle, provide EXACT matching text with sufficient "
           "context (at least 5-10 lines) to ensure correct placement."
           "\n- The system uses fuzzy matching with 85% similarity threshold for first-time edits. "
           "Provide accurate old_content to avoid incorrect placement."
           "\n- If changes remain 'pending' and file content hasn't changed, the user likely "
           "disabled auto-apply. DO NOT retry the same edit - wait for user action.";
}

QJsonObject EditFileTool::parametersSchema() const
{
    QJsonObject properties;

    QJsonObject filenameProperty;
    filenameProperty["type"] = "string";
    filenameProperty["description"]
        = "The path of the file to edit. Can be an absolute path (e.g., /path/to/file.cpp) "
          "or a relative path from the project root (e.g., src/main.cpp)";
    properties["filename"] = filenameProperty;

    QJsonObject oldContentProperty;
    oldContentProperty["type"] = "string";
    oldContentProperty["description"]
        = "The content to find and replace. For exact matches, provide precise text "
          "(including whitespace). For changed files, the system uses fuzzy matching with "
          "85% similarity threshold for first-time edits. If empty, new_content will be "
          "appended to the end of the file";
    properties["old_content"] = oldContentProperty;

    QJsonObject newContentProperty;
    newContentProperty["type"] = "string";
    newContentProperty["description"] = "The new content to replace the old content with";
    properties["new_content"] = newContentProperty;

    QJsonObject definition;
    definition["type"] = "object";
    definition["properties"] = properties;
    QJsonArray required;
    required.append("filename");
    required.append("new_content");
    definition["required"] = required;

    return definition;
}

QFuture<LLMQore::ToolResult> EditFileTool::executeAsync(const QJsonObject &input)
{
    return QtConcurrent::run([this, input]() -> LLMQore::ToolResult {
        QString filename = input["filename"].toString().trimmed();
        QString oldContent = input["old_content"].toString();
        QString newContent = input["new_content"].toString();
        QString requestId = input["_request_id"].toString();

        if (filename.isEmpty()) {
            throw LLMQore::ToolInvalidArgument("'filename' parameter is required and cannot be empty");
        }

        if (newContent.isEmpty()) {
            throw LLMQore::ToolInvalidArgument("'new_content' parameter is required and cannot be empty");
        }


        QFileInfo fileInfo(filename);
        QString filePath;

        if (fileInfo.isAbsolute()) {
            filePath = filename;
        } else {
            QString projectRoot = Context::ProjectUtils::getProjectRoot();
            if (projectRoot.isEmpty()) {
                throw LLMQore::ToolRuntimeError(
                    QString("Cannot resolve relative path '%1': no project is open. "
                            "Please provide an absolute path or open a project.")
                        .arg(filename));
            }

            filePath = QDir(projectRoot).absoluteFilePath(filename);
            LOG_MESSAGE(QString("EditFileTool: Resolved relative path '%1' to '%2'")
                            .arg(filename, filePath));
        }

        QFile file(filePath);
        if (!file.exists()) {
            throw LLMQore::ToolRuntimeError(QString("File does not exist: %1").arg(filePath));
        }

        QFileInfo finalFileInfo(filePath);
        if (!finalFileInfo.isWritable()) {
            throw LLMQore::ToolRuntimeError(
                QString("File is not writable (read-only or permission denied): %1").arg(filePath));
        }

        bool isInProject = Context::ProjectUtils::isFileInProject(filePath);
        if (!isInProject) {
            const auto &settings = Settings::toolsSettings();
            if (!settings.allowAccessOutsideProject()) {
                const QString projectRoot = Context::ProjectUtils::getProjectRoot();
                const QString hint = projectRoot.isEmpty()
                    ? QStringLiteral(
                          "No project is currently open. Open a project in Qt Creator or "
                          "enable 'Allow file access outside project' in QodeAssist settings.")
                    : QString(
                          "Retry with a path under the active project root: '%1'. The build "
                          "directory is for compiler output only — source files must live under "
                          "the project root. If you really need to edit outside the project, "
                          "enable 'Allow file access outside project' in QodeAssist settings.")
                          .arg(projectRoot);
                throw LLMQore::ToolRuntimeError(
                    QString("File path '%1' is not within the current project. %2")
                        .arg(filePath, hint));
            }
            LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath));
        }

        QString editId = QUuid::createUuid().toString(QUuid::WithoutBraces);
        bool autoApply = Settings::toolsSettings().autoApplyFileEdits();

        LOG_MESSAGE(QString("EditFileTool: Edit details for %1:").arg(filePath));
        LOG_MESSAGE(QString("  oldContent length: %1 chars").arg(oldContent.length()));
        LOG_MESSAGE(QString("  newContent length: %1 chars").arg(newContent.length()));
        if (oldContent.length() <= 200) {
            LOG_MESSAGE(QString("  oldContent: '%1'").arg(oldContent));
        } else {
            LOG_MESSAGE(QString("  oldContent (first 200 chars): '%1...'")
                           .arg(oldContent.left(200)));
        }
        if (newContent.length() <= 200) {
            LOG_MESSAGE(QString("  newContent: '%1'").arg(newContent));
        } else {
            LOG_MESSAGE(QString("  newContent (first 200 chars): '%1...'")
                           .arg(newContent.left(200)));
        }

        Context::ChangesManager::instance().addFileEdit(
            editId,
            filePath,
            oldContent,
            newContent,
            autoApply,
            false,
            requestId
        );

        auto edit = Context::ChangesManager::instance().getFileEdit(editId);
        QString status = "pending";
        if (edit.status == Context::ChangesManager::Applied) {
            status = "applied";
        } else if (edit.status == Context::ChangesManager::Rejected) {
            status = "rejected";
        } else if (edit.status == Context::ChangesManager::Archived) {
            status = "archived";
        }

        QString statusMessage = edit.statusMessage;

        QJsonObject result;
        result["edit_id"] = editId;
        result["file"] = filePath;
        result["old_content"] = oldContent;
        result["new_content"] = newContent;
        result["status"] = status;
        result["status_message"] = statusMessage;

        LOG_MESSAGE(QString("File edit created: %1 (ID: %2, Status: %3, Deferred: %4)")
                        .arg(filePath, editId, status, requestId.isEmpty() ? QString("no") : QString("yes")));

        QString resultStr = "QODEASSIST_FILE_EDIT:"
                            + QString::fromUtf8(QJsonDocument(result).toJson(QJsonDocument::Compact));
        return LLMQore::ToolResult::text(resultStr);
    });
}

} // namespace QodeAssist::Tools

