#include "mcp_bridge.h"
#include "core.h"
#include "controller.h"
#include "generator.h"
#include "mainwindow.h"
#include <QCoreApplication>
#include <QDebug>
#include <cstring>

namespace rcx {

// ════════════════════════════════════════════════════════════════════
// Construction / lifecycle
// ════════════════════════════════════════════════════════════════════

McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent)
    : QObject(parent), m_mainWindow(mainWindow)
{}

McpBridge::~McpBridge() {
    stop();
}

void McpBridge::start() {
    if (m_server) return;

    m_server = new QLocalServer(this);
    m_server->setSocketOptions(QLocalServer::WorldAccessOption);

    // Remove stale socket (Linux/Mac leave files behind)
    QLocalServer::removeServer("ReclassMcpBridge");

    if (!m_server->listen("ReclassMcpBridge")) {
        qWarning() << "[MCP] Failed to start server:" << m_server->errorString();
        delete m_server;
        m_server = nullptr;
        return;
    }

    connect(m_server, &QLocalServer::newConnection,
            this, &McpBridge::onNewConnection);
    qDebug() << "[MCP] Server listening on pipe: ReclassMcpBridge";
}

void McpBridge::stop() {
    if (m_client) {
        m_client->disconnectFromServer();
        m_client = nullptr;
    }
    if (m_server) {
        m_server->close();
        delete m_server;
        m_server = nullptr;
    }
}

// ════════════════════════════════════════════════════════════════════
// Connection handling
// ════════════════════════════════════════════════════════════════════

void McpBridge::onNewConnection() {
    auto* pending = m_server->nextPendingConnection();
    if (!pending) return;

    // Single client — disconnect previous
    if (m_client) {
        m_client->disconnectFromServer();
        m_client->deleteLater();
    }

    m_client = pending;
    m_readBuffer.clear();
    m_initialized = false;

    connect(m_client, &QLocalSocket::readyRead,
            this, &McpBridge::onReadyRead);
    connect(m_client, &QLocalSocket::disconnected,
            this, &McpBridge::onDisconnected);

    qDebug() << "[MCP] Client connected";
}

void McpBridge::onReadyRead() {
    m_readBuffer.append(m_client->readAll());

    // Newline-delimited JSON framing
    while (true) {
        int idx = m_readBuffer.indexOf('\n');
        if (idx < 0) break;
        QByteArray line = m_readBuffer.left(idx).trimmed();
        m_readBuffer.remove(0, idx + 1);
        if (!line.isEmpty())
            processLine(line);
    }
}

void McpBridge::onDisconnected() {
    qDebug() << "[MCP] Client disconnected";
    m_client = nullptr;
    m_initialized = false;
}

// ════════════════════════════════════════════════════════════════════
// JSON-RPC plumbing
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::okReply(const QJsonValue& id, const QJsonObject& result) {
    return QJsonObject{
        {"jsonrpc", "2.0"},
        {"id", id},
        {"result", result}
    };
}

QJsonObject McpBridge::errReply(const QJsonValue& id, int code, const QString& msg) {
    return QJsonObject{
        {"jsonrpc", "2.0"},
        {"id", id},
        {"error", QJsonObject{{"code", code}, {"message", msg}}}
    };
}

void McpBridge::sendJson(const QJsonObject& obj) {
    if (!m_client) return;
    QByteArray data = QJsonDocument(obj).toJson(QJsonDocument::Compact);
    qDebug() << "[MCP] >>" << data.left(200);
    data.append('\n');
    m_client->write(data);
    m_client->flush();
}

void McpBridge::sendNotification(const QString& method, const QJsonObject& params) {
    QJsonObject n{{"jsonrpc", "2.0"}, {"method", method}};
    if (!params.isEmpty()) n["params"] = params;
    sendJson(n);
}

QJsonObject McpBridge::makeTextResult(const QString& text, bool isError) {
    QJsonObject entry;
    entry["type"] = QStringLiteral("text");
    entry["text"] = text;
    QJsonArray content;
    content.append(entry);
    QJsonObject result;
    result["content"] = content;
    if (isError) result["isError"] = true;
    return result;
}

// ════════════════════════════════════════════════════════════════════
// Dispatch
// ════════════════════════════════════════════════════════════════════

void McpBridge::processLine(const QByteArray& line) {
    qDebug() << "[MCP] <<" << line.trimmed().left(200);
    auto doc = QJsonDocument::fromJson(line);
    if (!doc.isObject()) {
        sendJson(errReply(QJsonValue(), -32700, "Parse error"));
        return;
    }

    QJsonObject req = doc.object();
    QJsonValue id = req.value("id");
    QString method = req.value("method").toString();

    // Client notifications (no response)
    if (method == "notifications/initialized" ||
        method == "notifications/cancelled") {
        return;
    }

    if (method == "initialize") {
        sendJson(handleInitialize(id, req.value("params").toObject()));
    } else if (method == "tools/list") {
        sendJson(handleToolsList(id));
    } else if (method == "tools/call") {
        sendJson(handleToolsCall(id, req.value("params").toObject()));
    } else {
        sendJson(errReply(id, -32601, "Method not found: " + method));
    }
}

// ════════════════════════════════════════════════════════════════════
// MCP: initialize
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject&) {
    m_initialized = true;

    QJsonObject caps;
    caps["tools"] = QJsonObject{{"listChanged", false}};

    QJsonObject result{
        {"protocolVersion", "2024-11-05"},
        {"capabilities", caps},
        {"serverInfo", QJsonObject{
            {"name", "reclass-mcp"},
            {"version", "1.0.0"}
        }}
    };
    return okReply(id, result);
}

// ════════════════════════════════════════════════════════════════════
// MCP: tools/list
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
    QJsonArray tools;

    // 1. project.state
    tools.append(QJsonObject{
        {"name", "project.state"},
        {"description", "Returns project state: node tree, base address, sources, provider info. "
                        "Use depth/parentId to avoid dumping the whole tree. "
                        "Call with depth:1 first to see top-level structs, then drill in with parentId."},
        {"inputSchema", QJsonObject{
            {"type", "object"},
            {"properties", QJsonObject{
                {"tabIndex", QJsonObject{{"type", "integer"},
                    {"description", "MDI tab index (0-based). Omit for active tab."}}},
                {"depth", QJsonObject{{"type", "integer"},
                    {"description", "Max tree depth to return (default 1 = top-level structs only)."}}},
                {"parentId", QJsonObject{{"type", "string"},
                    {"description", "Only return children of this node."}}},
                {"includeTree", QJsonObject{{"type", "boolean"},
                    {"description", "If false, return only provider/source info, no tree. Default true."}}}
            }}
        }}
    });

    // 2. tree.apply
    tools.append(QJsonObject{
        {"name", "tree.apply"},
        {"description", "Apply batch of tree operations atomically (undo macro). "
                        "Each op is a JSON object with an 'op' field for the operation type and 'nodeId' (string) for the target node. "
                        "Operations: "
                        "remove: {op:'remove', nodeId:'ID'}. "
                        "rename: {op:'rename', nodeId:'ID', name:'newName'}. "
                        "insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0}. "
                        "change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. "
                        "change_offset: {op:'change_offset', nodeId:'ID', offset:16}. "
                        "change_base: {op:'change_base', baseAddress:'0x400000'}. "
                        "change_struct_type: {op:'change_struct_type', nodeId:'ID', structTypeName:'Name'}. "
                        "change_class_keyword: {op:'change_class_keyword', nodeId:'ID', classKeyword:'class'}. "
                        "change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. "
                        "change_array_meta: {op:'change_array_meta', nodeId:'ID', elementKind:'UInt32', arrayLen:10}. "
                        "collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
                        "Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. "
                        "Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 "
                        "Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"},
        {"inputSchema", QJsonObject{
            {"type", "object"},
            {"properties", QJsonObject{
                {"tabIndex", QJsonObject{{"type", "integer"},
                    {"description", "MDI tab index (0-based). Omit for active tab."}}},
                {"operations", QJsonObject{{"type", "array"}, {"items", QJsonObject{{"type", "object"}}}}},
                {"macroName", QJsonObject{{"type", "string"}}}
            }},
            {"required", QJsonArray{"operations"}}
        }}
    });

    // 3. source.switch
    tools.append(QJsonObject{
        {"name", "source.switch"},
        {"description", "Switch active data source (provider). Use sourceIndex for saved sources, "
                        "filePath to load a binary file, or pid to attach to a live process."},
        {"inputSchema", QJsonObject{
            {"type", "object"},
            {"properties", QJsonObject{
                {"tabIndex", QJsonObject{{"type", "integer"},
                    {"description", "MDI tab index (0-based). Omit for active tab."}}},
                {"sourceIndex", QJsonObject{{"type", "integer"}}},
                {"filePath", QJsonObject{{"type", "string"}}},
                {"pid", QJsonObject{{"type", "integer"},
                    {"description", "Process ID to attach to for live memory reading."}}},
                {"processName", QJsonObject{{"type", "string"},
                    {"description", "Display name for the process (optional with pid)."}}},
                {"allViews", QJsonObject{{"type", "boolean"}}}
            }}
        }}
    });

    // 4. hex.read
    tools.append(QJsonObject{
        {"name", "hex.read"},
        {"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
                        "interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
                        "Offset is tree-relative (0-based, baseAddress added automatically) "
                        "unless baseRelative=true (offset is absolute)."},
        {"inputSchema", QJsonObject{
            {"type", "object"},
            {"properties", QJsonObject{
                {"tabIndex", QJsonObject{{"type", "integer"},
                    {"description", "MDI tab index (0-based). Omit for active tab."}}},
                {"offset", QJsonObject{{"type", "integer"}}},
                {"length", QJsonObject{{"type", "integer"}}},
                {"baseRelative", QJsonObject{{"type", "boolean"}}}
            }},
            {"required", QJsonArray{"offset", "length"}}
        }}
    });

    // 5. hex.write
    tools.append(QJsonObject{
        {"name", "hex.write"},
        {"description", "Write raw bytes to provider (through undo stack). Hex string format: '4D5A9000'"},
        {"inputSchema", QJsonObject{
            {"type", "object"},
            {"properties", QJsonObject{
                {"tabIndex", QJsonObject{{"type", "integer"},
                    {"description", "MDI tab index (0-based). Omit for active tab."}}},
                {"offset", QJsonObject{{"type", "integer"}}},
                {"hexBytes", QJsonObject{{"type", "string"}}},
                {"baseRelative", QJsonObject{{"type", "boolean"}}}
            }},
            {"required", QJsonArray{"offset", "hexBytes"}}
        }}
    });

    // 6. status.set
    tools.append(QJsonObject{
        {"name", "status.set"},
        {"description", "Show status text to user. Updates command row (editor line 0) and/or "
                        "the window status bar."},
        {"inputSchema", QJsonObject{
            {"type", "object"},
            {"properties", QJsonObject{
                {"tabIndex", QJsonObject{{"type", "integer"},
                    {"description", "MDI tab index (0-based). Omit for active tab."}}},
                {"text", QJsonObject{{"type", "string"}}},
                {"target", QJsonObject{{"type", "string"},
                    {"enum", QJsonArray{"commandRow", "statusBar", "both"}}}}
            }},
            {"required", QJsonArray{"text"}}
        }}
    });

    // 7. ui.action
    tools.append(QJsonObject{
        {"name", "ui.action"},
        {"description", "Trigger a UI action. Fallback for operations without dedicated tools. "
                        "Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
                        "export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, "
                        "select_node, refresh"},
        {"inputSchema", QJsonObject{
            {"type", "object"},
            {"properties", QJsonObject{
                {"tabIndex", QJsonObject{{"type", "integer"},
                    {"description", "MDI tab index (0-based). Omit for active tab."}}},
                {"action", QJsonObject{{"type", "string"}}},
                {"nodeId", QJsonObject{{"type", "string"}}},
                {"filePath", QJsonObject{{"type", "string"}}}
            }},
            {"required", QJsonArray{"action"}}
        }}
    });

    return okReply(id, QJsonObject{{"tools", tools}});
}

// ════════════════════════════════════════════════════════════════════
// MCP: tools/call — dispatch to tool implementations
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject& params) {
    QString toolName = params.value("name").toString();
    QJsonObject args = params.value("arguments").toObject();

    QJsonObject result;
    if      (toolName == "project.state")  result = toolProjectState(args);
    else if (toolName == "tree.apply")     result = toolTreeApply(args);
    else if (toolName == "source.switch")  result = toolSourceSwitch(args);
    else if (toolName == "hex.read")       result = toolHexRead(args);
    else if (toolName == "hex.write")      result = toolHexWrite(args);
    else if (toolName == "status.set")     result = toolStatusSet(args);
    else if (toolName == "ui.action")      result = toolUiAction(args);
    else return errReply(id, -32601, "Unknown tool: " + toolName);

    return okReply(id, result);
}

// ════════════════════════════════════════════════════════════════════
// Helper: resolve "$N" placeholder references
// ════════════════════════════════════════════════════════════════════

QString McpBridge::resolvePlaceholder(const QString& ref,
                                       const QHash<QString, uint64_t>& placeholderMap) {
    if (ref.startsWith('$')) {
        auto it = placeholderMap.find(ref);
        if (it != placeholderMap.end())
            return QString::number(it.value());
    }
    return ref;  // not a placeholder — return as-is
}

// ════════════════════════════════════════════════════════════════════
// Smart tab resolution
// ════════════════════════════════════════════════════════════════════

MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args) {
    // 1) Explicit tab index from args
    if (args.contains("tabIndex")) {
        int idx = args.value("tabIndex").toInt();
        auto* t = m_mainWindow->tabByIndex(idx);
        if (t) return t;
    }

    // 2) Active sub-window (user clicked on it)
    auto* t = m_mainWindow->activeTab();
    if (t) return t;

    // 3) Fall back to first available tab
    if (m_mainWindow->tabCount() > 0) {
        t = m_mainWindow->tabByIndex(0);
        if (t) return t;
    }

    // 4) No tabs at all — auto-create a project
    m_mainWindow->project_new();
    return m_mainWindow->tabByIndex(0);
}

// ════════════════════════════════════════════════════════════════════
// TOOL: project.state
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
    auto* tab = resolveTab(args);
    if (!tab) return makeTextResult("No active tab", true);

    auto* doc = tab->doc;
    auto* ctrl = tab->ctrl;
    const auto& tree = doc->tree;

    int maxDepth = args.value("depth").toInt(1);
    bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true;
    QString parentIdStr = args.value("parentId").toString();
    uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();

    QJsonObject state;
    state["baseAddress"] = "0x" + QString::number(tree.baseAddress, 16).toUpper();
    state["viewRootId"] = QString::number(ctrl->viewRootId());
    state["nodeCount"] = tree.nodes.size();

    // Provider info
    QJsonObject provInfo;
    if (doc->provider) {
        provInfo["name"] = doc->provider->name();
        provInfo["writable"] = doc->provider->isWritable();
        provInfo["live"] = doc->provider->isLive();
        provInfo["size"] = doc->provider->size();
        provInfo["kind"] = doc->provider->kind();
    }
    state["provider"] = provInfo;

    // Saved sources
    QJsonArray srcs;
    const auto& savedSources = ctrl->savedSources();
    int activeIdx = ctrl->activeSourceIndex();
    for (int i = 0; i < savedSources.size(); i++) {
        const auto& s = savedSources[i];
        srcs.append(QJsonObject{
            {"index", i},
            {"kind", s.kind},
            {"displayName", s.displayName},
            {"active", i == activeIdx}
        });
    }
    state["sources"] = srcs;

    // Selection
    QJsonArray selArr;
    for (uint64_t sid : ctrl->selectedIds())
        selArr.append(QString::number(sid));
    state["selectedNodeIds"] = selArr;

    // Document info
    state["filePath"] = doc->filePath;
    state["modified"] = doc->modified;
    state["undoAvailable"] = doc->undoStack.canUndo();
    state["redoAvailable"] = doc->undoStack.canRedo();

    // Filtered tree: only emit nodes up to maxDepth from the filter root
    if (includeTree) {
        // Build parent→children map once
        QHash<uint64_t, QVector<int>> childMap;
        for (int i = 0; i < tree.nodes.size(); i++)
            childMap[tree.nodes[i].parentId].append(i);

        // BFS from filterParentId, respecting maxDepth
        QJsonArray nodeArr;
        struct QueueEntry { uint64_t parentId; int depth; };
        QVector<QueueEntry> queue;
        queue.append({filterParentId, 0});

        while (!queue.isEmpty()) {
            auto entry = queue.takeFirst();
            if (entry.depth > maxDepth) continue;

            const auto& kids = childMap.value(entry.parentId);
            for (int ci : kids) {
                const Node& n = tree.nodes[ci];
                QJsonObject nj = n.toJson();
                // Add computed size for containers
                if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) {
                    nj["computedSize"] = tree.structSpan(n.id, &childMap);
                    nj["childCount"] = childMap.value(n.id).size();
                }
                nodeArr.append(nj);

                // Enqueue children if we haven't hit depth limit
                if (entry.depth + 1 <= maxDepth)
                    queue.append({n.id, entry.depth + 1});
            }
        }

        QJsonObject treeObj;
        treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
        treeObj["nextId"] = QString::number(tree.m_nextId);
        treeObj["nodes"] = nodeArr;
        state["tree"] = treeObj;
    }

    return makeTextResult(QString::fromUtf8(
        QJsonDocument(state).toJson(QJsonDocument::Indented)));
}

// ════════════════════════════════════════════════════════════════════
// TOOL: tree.apply
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
    auto* tab = resolveTab(args);
    if (!tab) return makeTextResult("No active tab", true);

    auto* doc = tab->doc;
    auto* ctrl = tab->ctrl;
    auto& tree = doc->tree;

    QJsonArray ops = args.value("operations").toArray();
    QString macroName = args.value("macroName").toString("MCP batch");

    if (ops.isEmpty())
        return makeTextResult("No operations provided", true);

    // Phase 1: Pre-scan inserts and reserve IDs
    QHash<QString, uint64_t> placeholders;  // "$0" → reserved ID
    for (int i = 0; i < ops.size(); i++) {
        QJsonObject op = ops[i].toObject();
        if (op.value("op").toString() == "insert") {
            uint64_t newId = tree.reserveId();
            placeholders[QStringLiteral("$%1").arg(i)] = newId;
        }
    }

    // Phase 2: Execute in undo macro
    if (!m_slowMode)
        ctrl->setSuppressRefresh(true);
    doc->undoStack.beginMacro(macroName);

    int applied = 0;
    uint64_t lastRootStructId = 0;  // track root-level struct inserts
    QStringList skippedOps;
    for (int i = 0; i < ops.size(); i++) {
        // Safety valve: keep paint events flowing for large batches
        if (i % 100 == 0 && ops.size() > 200)
            QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 5);

        QJsonObject op = ops[i].toObject();
        QString opType = op.value("op").toString();

        if (opType == "insert") {
            Node n;
            n.id = placeholders.value(QStringLiteral("$%1").arg(i), tree.reserveId());
            n.kind = kindFromString(op.value("kind").toString("Hex64"));
            n.name = op.value("name").toString();
            QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders);
            n.parentId = pid.toULongLong();
            n.offset = op.value("offset").toInt(0);
            n.structTypeName = op.value("structTypeName").toString();
            n.classKeyword = op.value("classKeyword").toString();
            n.strLen = op.value("strLen").toInt(64);
            n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
            n.arrayLen = op.value("arrayLen").toInt(1);
            QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders);
            n.refId = refStr.toULongLong();

            // Auto-place: offset -1 means "after last sibling"
            if (n.offset < 0) {
                int maxEnd = 0;
                auto siblings = tree.childrenOf(n.parentId);
                for (int si : siblings) {
                    auto& sn = tree.nodes[si];
                    int sz = (sn.kind == NodeKind::Struct || sn.kind == NodeKind::Array)
                        ? tree.structSpan(sn.id) : sn.byteSize();
                    int end = sn.offset + sz;
                    if (end > maxEnd) maxEnd = end;
                }
                int align = alignmentFor(n.kind);
                n.offset = (maxEnd + align - 1) / align * align;
            }

            doc->undoStack.push(new RcxCommand(ctrl, cmd::Insert{n, {}}));
            if (n.parentId == 0 && n.kind == NodeKind::Struct)
                lastRootStructId = n.id;
            applied++;
        }
        else if (opType == "remove") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                const Node& node = tree.nodes[idx];
                QVector<int> indices = tree.subtreeIndices(node.id);
                QVector<Node> subtree;
                for (int si : indices) subtree.append(tree.nodes[si]);
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::Remove{node.id, subtree, {}}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: remove nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "rename") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::Rename{tree.nodes[idx].id, tree.nodes[idx].name,
                                op.value("name").toString()}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: rename nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "change_kind") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                NodeKind newKind = kindFromString(op.value("kind").toString());
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::ChangeKind{tree.nodes[idx].id, tree.nodes[idx].kind, newKind, {}}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: change_kind nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "change_offset") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                int newOff = op.value("offset").toInt();
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::ChangeOffset{tree.nodes[idx].id, tree.nodes[idx].offset, newOff}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: change_offset nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "change_base") {
            uint64_t newBase = op.value("baseAddress").toString().toULongLong(nullptr, 16);
            doc->undoStack.push(new RcxCommand(ctrl,
                cmd::ChangeBase{tree.baseAddress, newBase}));
            applied++;
        }
        else if (opType == "change_struct_type") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::ChangeStructTypeName{tree.nodes[idx].id,
                        tree.nodes[idx].structTypeName,
                        op.value("structTypeName").toString()}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: change_struct_type nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "change_class_keyword") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::ChangeClassKeyword{tree.nodes[idx].id,
                        tree.nodes[idx].classKeyword,
                        op.value("classKeyword").toString()}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: change_class_keyword nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "change_pointer_ref") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::ChangePointerRef{tree.nodes[idx].id,
                        tree.nodes[idx].refId, refStr.toULongLong()}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: change_pointer_ref nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "change_array_meta") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                NodeKind newElemKind = kindFromString(op.value("elementKind").toString());
                int newLen = op.value("arrayLen").toInt(1);
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::ChangeArrayMeta{tree.nodes[idx].id,
                        tree.nodes[idx].elementKind, newElemKind,
                        tree.nodes[idx].arrayLen, newLen}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: change_array_meta nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else if (opType == "collapse") {
            QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
            int idx = tree.indexOfId(nid.toULongLong());
            if (idx >= 0) {
                bool newState = op.value("collapsed").toBool();
                doc->undoStack.push(new RcxCommand(ctrl,
                    cmd::Collapse{tree.nodes[idx].id, tree.nodes[idx].collapsed, newState}));
                applied++;
            } else {
                skippedOps.append(QStringLiteral("op[%1]: collapse nodeId '%2' not found").arg(i).arg(nid));
            }
        }
        else {
            skippedOps.append(QStringLiteral("op[%1]: unknown op '%2'").arg(i).arg(opType));
        }

        // Slow mode: refresh after each operation for visual feedback
        if (m_slowMode && applied > 0) {
            ctrl->refresh();
            QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 16);
        }
    }

    doc->undoStack.endMacro();
    if (!m_slowMode)
        ctrl->setSuppressRefresh(false);

    // Auto-switch view to newly created root struct
    if (lastRootStructId)
        ctrl->setViewRootId(lastRootStructId);

    ctrl->refresh();

    // Build response with assigned placeholder IDs
    QJsonObject assignedIds;
    for (auto it = placeholders.begin(); it != placeholders.end(); ++it)
        assignedIds[it.key()] = QString::number(it.value());

    QString msg = QStringLiteral("Applied %1 operations").arg(applied);
    if (!skippedOps.isEmpty())
        msg += QStringLiteral("\nSkipped %1:\n").arg(skippedOps.size()) + skippedOps.join('\n');

    QJsonObject result = makeTextResult(msg, !skippedOps.isEmpty() && applied == 0);
    result["assignedIds"] = assignedIds;
    return result;
}

// ════════════════════════════════════════════════════════════════════
// TOOL: source.switch
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
    auto* tab = resolveTab(args);
    if (!tab) return makeTextResult("No active tab", true);

    auto* ctrl = tab->ctrl;
    auto* doc = tab->doc;

    if (args.contains("sourceIndex")) {
        int idx = args.value("sourceIndex").toInt();
        const auto& sources = ctrl->savedSources();
        if (idx < 0 || idx >= sources.size())
            return makeTextResult("Source index out of range: " + QString::number(idx), true);

        if (args.value("allViews").toBool()) {
            // Switch all tabs to this source
            for (auto& t : m_mainWindow->m_tabs)
                t.ctrl->switchSource(idx);
        } else {
            ctrl->switchSource(idx);
        }
        return makeTextResult("Switched to source " + QString::number(idx) +
                              " (" + sources[idx].displayName + ")");
    }

    if (args.contains("pid")) {
        uint32_t pid = (uint32_t)args.value("pid").toInt();
        QString name = args.value("processName").toString();
        if (name.isEmpty()) name = QString("PID %1").arg(pid);
        QString target = QString("%1:%2").arg(pid).arg(name);
        ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
        return makeTextResult("Attached to process " + name + " (PID " + QString::number(pid) + ")");
    }

    if (args.contains("filePath")) {
        QString path = args.value("filePath").toString();
        doc->loadData(path);
        ctrl->refresh();
        return makeTextResult("Loaded file: " + path);
    }

    return makeTextResult("Provide sourceIndex, filePath, or pid", true);
}

// ════════════════════════════════════════════════════════════════════
// TOOL: hex.read
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
    auto* tab = resolveTab(args);
    if (!tab) return makeTextResult("No active tab", true);

    auto* prov = tab->doc->provider.get();
    if (!prov) return makeTextResult("No provider", true);

    int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
    int length = qMin(args.value("length").toInt(64), 4096);

    if (!args.value("baseRelative").toBool())
        offset += (int64_t)tab->doc->tree.baseAddress;

    if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
        return makeTextResult("Cannot read at offset " + QString::number(offset), true);

    QByteArray data = prov->readBytes((uint64_t)offset, length);

    // Format hex dump (16 bytes per line)
    QString dump;
    for (int i = 0; i < data.size(); i += 16) {
        int lineLen = qMin(16, data.size() - i);
        dump += QString("%1: ").arg((uint64_t)(offset + i), 8, 16, QChar('0'));
        for (int j = 0; j < 16; j++) {
            if (j < lineLen)
                dump += QString("%1 ").arg((uint8_t)data[i+j], 2, 16, QChar('0'));
            else
                dump += "   ";
            if (j == 7) dump += " ";
        }
        dump += " |";
        for (int j = 0; j < lineLen; j++) {
            uint8_t c = (uint8_t)data[i+j];
            dump += (c >= 0x20 && c <= 0x7e) ? QChar(c) : QChar('.');
        }
        dump += "|\n";
    }

    // Type interpretations at start of read
    if (data.size() >= 1) {
        dump += "\n--- Interpretations at offset ---\n";
        dump += "u8:  " + QString::number((uint8_t)data[0]) + "\n";
        if (data.size() >= 2) {
            uint16_t v; memcpy(&v, data.data(), 2);
            dump += "u16: " + QString::number(v) + "\n";
        }
        if (data.size() >= 4) {
            uint32_t v; memcpy(&v, data.data(), 4);
            int32_t iv; memcpy(&iv, data.data(), 4);
            float fv; memcpy(&fv, data.data(), 4);
            dump += "u32: " + QString::number(v) + " (0x" + QString::number(v, 16) + ")\n";
            dump += "i32: " + QString::number(iv) + "\n";
            dump += "f32: " + QString::number((double)fv) + "\n";
        }
        if (data.size() >= 8) {
            uint64_t v; memcpy(&v, data.data(), 8);
            double dv; memcpy(&dv, data.data(), 8);
            dump += "u64: " + QString::number(v) + " (0x" + QString::number(v, 16) + ")\n";
            dump += "f64: " + QString::number(dv) + "\n";

            // Pointer-likeness
            uint64_t base = tab->doc->tree.baseAddress;
            int provSize = prov->size();
            if (v >= base && v < base + (uint64_t)provSize)
                dump += "ptr?: LIKELY (within provider range)\n";
        }
        // String-likeness
        int printable = 0;
        for (int i = 0; i < data.size() && (uint8_t)data[i] >= 0x20 && (uint8_t)data[i] <= 0x7e; i++)
            printable++;
        if (printable >= 4)
            dump += "str?: " + QString::number(printable) + " printable ASCII bytes\n";
    }

    return makeTextResult(dump);
}

// ════════════════════════════════════════════════════════════════════
// TOOL: hex.write
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
    auto* tab = resolveTab(args);
    if (!tab) return makeTextResult("No active tab", true);

    auto* ctrl = tab->ctrl;
    auto* doc = tab->doc;
    auto* prov = doc->provider.get();

    int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
    QString hexStr = args.value("hexBytes").toString().remove(' ');

    if (!args.value("baseRelative").toBool())
        offset += (int64_t)doc->tree.baseAddress;

    if (hexStr.size() % 2 != 0)
        return makeTextResult("Hex string must have even length", true);

    QByteArray newBytes;
    for (int i = 0; i < hexStr.size(); i += 2) {
        bool ok;
        uint8_t byte = hexStr.mid(i, 2).toUInt(&ok, 16);
        if (!ok) return makeTextResult("Invalid hex at position " + QString::number(i), true);
        newBytes.append((char)byte);
    }

    if (!prov || !prov->isWritable())
        return makeTextResult("Provider is not writable", true);
    if (!prov->isReadable((uint64_t)offset, newBytes.size()))
        return makeTextResult("Offset out of range", true);

    QByteArray oldBytes = prov->readBytes((uint64_t)offset, newBytes.size());
    doc->undoStack.push(new RcxCommand(ctrl,
        cmd::WriteBytes{(uint64_t)offset, oldBytes, newBytes}));

    return makeTextResult("Wrote " + QString::number(newBytes.size()) + " bytes at offset 0x"
                          + QString::number(offset, 16));
}

// ════════════════════════════════════════════════════════════════════
// TOOL: status.set
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::toolStatusSet(const QJsonObject& args) {
    QString text = args.value("text").toString();
    QString target = args.value("target").toString("both");

    auto* tab = resolveTab(args);

    if (target == "commandRow" || target == "both") {
        if (tab) {
            for (auto& pane : tab->panes) {
                if (pane.editor) {
                    pane.editor->setCommandRowText(
                        QStringLiteral("[\xE2\x96\xB8] [Claude: %1]").arg(text));
                }
            }
        }
    }
    if (target == "statusBar" || target == "both") {
        m_mainWindow->m_statusLabel->setText(text);
    }

    return makeTextResult("Status set: " + text);
}

// ════════════════════════════════════════════════════════════════════
// TOOL: ui.action
// ════════════════════════════════════════════════════════════════════

QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
    QString action = args.value("action").toString();
    QString nodeIdStr = args.value("nodeId").toString();

    auto* tab = resolveTab(args);
    auto* doc = tab ? tab->doc : nullptr;
    auto* ctrl = tab ? tab->ctrl : nullptr;

    if (action == "undo") {
        if (!doc) return makeTextResult("No active tab", true);
        if (!doc->undoStack.canUndo()) return makeTextResult("Nothing to undo", true);
        doc->undoStack.undo();
        return makeTextResult("Undo performed");
    }
    if (action == "redo") {
        if (!doc) return makeTextResult("No active tab", true);
        if (!doc->undoStack.canRedo()) return makeTextResult("Nothing to redo", true);
        doc->undoStack.redo();
        return makeTextResult("Redo performed");
    }
    if (action == "refresh") {
        if (!ctrl) return makeTextResult("No active tab", true);
        ctrl->refresh();
        return makeTextResult("Refreshed");
    }
    if (action == "set_view_root") {
        if (!ctrl) return makeTextResult("No active tab", true);
        ctrl->setViewRootId(nodeIdStr.toULongLong());
        return makeTextResult("View root set to " + nodeIdStr);
    }
    if (action == "scroll_to_node") {
        if (!ctrl) return makeTextResult("No active tab", true);
        ctrl->scrollToNodeId(nodeIdStr.toULongLong());
        return makeTextResult("Scrolled to node " + nodeIdStr);
    }
    if (action == "export_cpp") {
        if (!doc) return makeTextResult("No active tab", true);
        const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
        QString code = renderCppAll(doc->tree, aliases);
        return makeTextResult(code);
    }
    if (action == "save_file") {
        m_mainWindow->project_save();
        return makeTextResult("Saved");
    }
    if (action == "new_file") {
        m_mainWindow->project_new();
        return makeTextResult("New project created");
    }
    if (action == "open_file") {
        QString path = args.value("filePath").toString();
        if (path.isEmpty())
            return makeTextResult("filePath required for open_file", true);
        m_mainWindow->project_open(path);
        return makeTextResult("Opened: " + path);
    }
    if (action == "collapse_node") {
        if (!ctrl || !doc) return makeTextResult("No active tab", true);
        int idx = doc->tree.indexOfId(nodeIdStr.toULongLong());
        if (idx < 0) return makeTextResult("Node not found: " + nodeIdStr, true);
        doc->undoStack.push(new RcxCommand(ctrl,
            cmd::Collapse{doc->tree.nodes[idx].id, doc->tree.nodes[idx].collapsed, true}));
        ctrl->refresh();
        return makeTextResult("Collapsed " + nodeIdStr);
    }
    if (action == "expand_node") {
        if (!ctrl || !doc) return makeTextResult("No active tab", true);
        int idx = doc->tree.indexOfId(nodeIdStr.toULongLong());
        if (idx < 0) return makeTextResult("Node not found: " + nodeIdStr, true);
        doc->undoStack.push(new RcxCommand(ctrl,
            cmd::Collapse{doc->tree.nodes[idx].id, doc->tree.nodes[idx].collapsed, false}));
        ctrl->refresh();
        return makeTextResult("Expanded " + nodeIdStr);
    }
    if (action == "select_node") {
        if (!ctrl) return makeTextResult("No active tab", true);
        uint64_t nid = nodeIdStr.toULongLong();
        ctrl->clearSelection();
        auto* editor = ctrl->primaryEditor();
        if (editor)
            ctrl->handleNodeClick(editor, -1, nid, Qt::NoModifier);
        return makeTextResult("Selected node " + nodeIdStr);
    }

    return makeTextResult("Unknown action: " + action, true);
}

// ════════════════════════════════════════════════════════════════════
// Notifications (call from MainWindow/Controller hooks)
// ════════════════════════════════════════════════════════════════════

void McpBridge::notifyTreeChanged() {
    if (!m_client || !m_initialized) return;
    sendNotification("notifications/resources/updated",
                     QJsonObject{{"uri", "project://tree"}});
}

void McpBridge::notifyDataChanged() {
    if (!m_client || !m_initialized) return;
    sendNotification("notifications/resources/updated",
                     QJsonObject{{"uri", "project://data"}});
}

} // namespace rcx
