#include <emscripten/bind.h>

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>

#include <App/Application.h>
#include <App/ElementMap.h>
#include <App/ElementNamingUtils.h>
#include <App/IndexedName.h>
#include <App/MappedName.h>
#include <App/StringHasher.h>
#include <Base/Exception.h>

#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

#ifdef __EMSCRIPTEN__
#include <pwd.h>
#include <limits.h>
#include <sys/types.h>
#include <unistd.h>

extern "C" ssize_t readlink(const char* path, char* buffer, size_t size)
{
    constexpr const char* executable = "/freecad/bin/freecad-naming-bridge";
    if (std::strcmp(path, "/proc/self/exe") != 0) {
        return -1;
    }
    const size_t length = std::strlen(executable);
    if (size < length) {
        return -1;
    }
    std::memcpy(buffer, executable, length);
    return static_cast<ssize_t>(length);
}

extern "C" char* realpath(const char* path, char* resolvedPath)
{
    static constexpr const char* allowed[] = {
        "/freecad-user",
        "/freecad-user/config",
        "/freecad-user/data",
        "/freecad-user/cache",
        "/freecad-user/temp",
    };
    if (!path || std::none_of(std::begin(allowed), std::end(allowed), [path](const char* entry) {
            return std::strcmp(path, entry) == 0;
        })) {
        return nullptr;
    }
    const size_t length = std::strlen(path);
    if (length >= PATH_MAX) {
        return nullptr;
    }
    char* output = resolvedPath ? resolvedPath : static_cast<char*>(std::malloc(length + 1));
    if (!output) {
        return nullptr;
    }
    std::memcpy(output, path, length + 1);
    return output;
}

extern "C" int getpwuid_r(uid_t uid,
                           struct passwd* password,
                           char* buffer,
                           size_t bufferSize,
                           struct passwd** result)
{
    constexpr const char identity[] = "freecad\0/freecad-user\0/bin/false";
    if (bufferSize < sizeof(identity)) {
        *result = nullptr;
        return -1;
    }
    std::memcpy(buffer, identity, sizeof(identity));
    password->pw_name = buffer;
    password->pw_passwd = const_cast<char*>("");
    password->pw_uid = uid;
    password->pw_gid = getgid();
    password->pw_gecos = buffer;
    password->pw_dir = buffer + sizeof("freecad");
    password->pw_shell = password->pw_dir + sizeof("/freecad-user");
    *result = password;
    return 0;
}

#endif

namespace
{
constexpr int ABI_VERSION = 1;
constexpr const char* FREECAD_VERSION = "1.1.1";
constexpr const char* FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d";

void ensureApplication()
{
    static bool initialized = false;
    if (initialized) {
        return;
    }
    char executable[] = "freecad-naming-bridge";
    char console[] = "--console";
    char* argv[] = {executable, console};
    App::Application::init(2, argv);
    initialized = true;
}

QJsonObject requireObject(const QJsonObject& object, const char* key)
{
    const QJsonValue value = object.value(key);
    if (!value.isObject()) {
        throw std::runtime_error(std::string("FreeCAD naming request requires object ") + key);
    }
    return value.toObject();
}

QString requireString(const QJsonObject& object, const char* key)
{
    const QJsonValue value = object.value(key);
    if (!value.isString() || value.toString().isEmpty()) {
        throw std::runtime_error(std::string("FreeCAD naming request requires string ") + key);
    }
    return value.toString();
}

long requirePositiveTag(const QJsonObject& object, const char* key)
{
    const double value = object.value(key).toDouble(-1.0);
    if (!std::isfinite(value) || value <= 0.0 || std::floor(value) != value
        || value > static_cast<double>(0x7fffffff)) {
        throw std::runtime_error(std::string("FreeCAD naming request requires positive tag ") + key);
    }
    return static_cast<long>(value);
}

int requireIndex(const QJsonObject& object, const char* key)
{
    const double value = object.value(key).toDouble(-1.0);
    if (!std::isfinite(value) || value < 0.0 || std::floor(value) != value
        || value > static_cast<double>(0x7ffffffe)) {
        throw std::runtime_error(std::string("FreeCAD naming request requires non-negative index ")
                                 + key);
    }
    return static_cast<int>(value);
}

QString titleKind(const QString& value)
{
    if (value == "face") {
        return "Face";
    }
    if (value == "edge") {
        return "Edge";
    }
    if (value == "vertex") {
        return "Vertex";
    }
    throw std::runtime_error("FreeCAD naming history supports only face, edge, and vertex");
}

QString canonicalOperation(const QString& operation)
{
    QString output;
    output.reserve(operation.size());
    for (const QChar character : operation) {
        if (character.isLetterOrNumber()) {
            output.append(character.toUpper());
        }
        else if (character == '-') {
            output.append('_');
        }
        else {
            throw std::runtime_error("FreeCAD naming operation contains an unsupported character");
        }
    }
    if (output.isEmpty()) {
        throw std::runtime_error("FreeCAD naming operation is empty");
    }
    return output;
}

QJsonObject parseNameToken(const std::string& raw)
{
    if (raw.empty() || (raw[0] != ':' && raw[0] != '$' && raw[0] != ';')) {
        throw std::runtime_error("FreeCAD ElementMap emitted an invalid name token");
    }
    std::vector<std::string> parts;
    std::size_t start = 0;
    while (true) {
        const std::size_t dot = raw.find('.', start);
        parts.push_back(raw.substr(start, dot == std::string::npos ? dot : dot - start));
        if (dot == std::string::npos) {
            break;
        }
        start = dot + 1;
    }
    const QString marker = QString(QChar(raw[0]));
    QJsonObject token {{"raw", QString::fromStdString(raw)}, {"marker", marker}};
    QJsonArray suffix;
    if (raw[0] == ':') {
        if (parts.size() < 3) {
            throw std::runtime_error("FreeCAD ElementMap emitted a truncated indexed token");
        }
        bool postfixOk = false;
        bool indexOk = false;
        const int postfixIndex = QString::fromStdString(parts[0].substr(1)).toInt(&postfixOk, 16);
        const int elementIndex = QString::fromStdString(parts[1]).toInt(&indexOk, 16);
        if (!postfixOk || !indexOk) {
            throw std::runtime_error("FreeCAD ElementMap emitted a malformed indexed token");
        }
        token.insert("postfixIndex", postfixIndex);
        token.insert("elementIndex", elementIndex);
        for (std::size_t index = 2; index < parts.size(); ++index) {
            suffix.append(QString::fromStdString(parts[index]));
        }
    }
    else {
        token.insert("name", QString::fromStdString(parts[0].substr(1)));
        for (std::size_t index = 1; index < parts.size(); ++index) {
            suffix.append(QString::fromStdString(parts[index]));
        }
    }
    token.insert("suffix", suffix);
    return token;
}

QJsonObject parseElementMap(const std::string& saved)
{
    std::istringstream stream(saved);
    unsigned rootId = 0;
    int postfixCount = 0;
    std::string label;
    if (!(stream >> rootId >> label >> postfixCount) || rootId == 0 || label != "PostfixCount"
        || postfixCount < 0) {
        throw std::runtime_error("FreeCAD ElementMap root header is invalid");
    }
    QJsonArray postfixes;
    for (int index = 0; index < postfixCount; ++index) {
        std::string postfix;
        if (!(stream >> postfix)) {
            throw std::runtime_error("FreeCAD ElementMap postfix list is truncated");
        }
        postfixes.append(QString::fromStdString(postfix));
    }
    int mapCount = 0;
    if (!(stream >> label >> mapCount) || label != "MapCount" || mapCount <= 0) {
        throw std::runtime_error("FreeCAD ElementMap map count is invalid");
    }
    QJsonArray maps;
    int rootMapIndex = 0;
    for (int mapOrdinal = 0; mapOrdinal < mapCount; ++mapOrdinal) {
        int mapIndex = 0;
        unsigned mapId = 0;
        int typeCount = 0;
        if (!(stream >> label >> mapIndex >> mapId >> typeCount) || label != "ElementMap"
            || mapIndex <= 0 || typeCount < 0) {
            throw std::runtime_error("FreeCAD ElementMap map header is invalid");
        }
        rootMapIndex = std::max(rootMapIndex, mapIndex);
        QJsonArray sections;
        for (int typeOrdinal = 0; typeOrdinal < typeCount; ++typeOrdinal) {
            std::string sectionName;
            int childCount = 0;
            if (!(stream >> sectionName >> label >> childCount) || label != "ChildCount"
                || childCount != 0) {
                throw std::runtime_error("Candidate bridge does not accept child ElementMap records");
            }
            int nameCount = 0;
            if (!(stream >> label >> nameCount) || label != "NameCount" || nameCount < 0) {
                throw std::runtime_error("FreeCAD ElementMap name count is invalid");
            }
            QJsonArray names;
            for (int nameOrdinal = 0; nameOrdinal < nameCount; ++nameOrdinal) {
                QJsonArray tokens;
                std::string raw;
                std::string token;
                while (stream >> token) {
                    if (token == "0") {
                        break;
                    }
                    if (!raw.empty()) {
                        raw += ' ';
                    }
                    raw += token;
                    tokens.append(parseNameToken(token));
                }
                if (!stream) {
                    throw std::runtime_error("FreeCAD ElementMap name entry is truncated");
                }
                names.append(QJsonObject {
                    {"tokens", tokens},
                    {"trailing", "0"},
                    {"raw", QString::fromStdString(raw.empty() ? "0" : raw + " 0")},
                });
            }
            sections.append(QJsonObject {
                {"name", QString::fromStdString(sectionName)},
                {"children", QJsonArray {}},
                {"names", names},
            });
        }
        if (!(stream >> label) || label != "EndMap") {
            throw std::runtime_error("FreeCAD ElementMap map terminator is missing");
        }
        maps.append(QJsonObject {
            {"index", mapIndex},
            {"id", static_cast<qint64>(mapId)},
            {"typeCount", typeCount},
            {"sections", sections},
        });
    }
    return QJsonObject {
        {"schemaVersion", 2},
        {"nativeVersion", 1},
        {"rootId", static_cast<qint64>(rootId)},
        {"postfixes", postfixes},
        {"maps", maps},
        {"rootMapIndex", rootMapIndex},
    };
}

QJsonObject stringHasherJson(const App::StringHasherRef& hasher)
{
    QJsonArray entries;
    for (const auto& [id, reference] : hasher->getIDMap()) {
        const App::StringID& stringId = reference.deref();
        int flags = 0;
        flags |= stringId.isBinary() ? 1 << 0 : 0;
        flags |= stringId.isHashed() ? 1 << 1 : 0;
        flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0;
        flags |= stringId.isPostfixed() ? 1 << 3 : 0;
        flags |= stringId.isIndexed() ? 1 << 4 : 0;
        flags |= stringId.isPrefixID() ? 1 << 5 : 0;
        flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0;
        flags |= stringId.isPersistent() ? 1 << 7 : 0;
        QJsonArray relatedIds;
        for (const auto& related : reference.relatedIDs()) {
            relatedIds.append(static_cast<qint64>(related.value()));
        }
        entries.append(QJsonObject {
            {"id", static_cast<qint64>(id)},
            {"flags", flags},
            {"relatedIds", relatedIds},
            {"data", QString::fromUtf8(stringId.data())},
            {"postfix", QString::fromUtf8(stringId.postfix())},
        });
    }
    return QJsonObject {{"schemaVersion", 2}, {"nativeVersion", 1}, {"entries", entries}};
}

int stringIdFlags(const App::StringID& stringId)
{
    int flags = 0;
    flags |= stringId.isBinary() ? 1 << 0 : 0;
    flags |= stringId.isHashed() ? 1 << 1 : 0;
    flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0;
    flags |= stringId.isPostfixed() ? 1 << 3 : 0;
    flags |= stringId.isIndexed() ? 1 << 4 : 0;
    flags |= stringId.isPrefixID() ? 1 << 5 : 0;
    flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0;
    flags |= stringId.isPersistent() ? 1 << 7 : 0;
    return flags;
}

long requireStringId(const QJsonValue& value, const char* label)
{
    const double number = value.toDouble(-1.0);
    if (!std::isfinite(number) || number <= 0.0 || std::floor(number) != number
        || number > static_cast<double>(0x7fffffff)) {
        throw std::runtime_error(std::string("Prior StringHasher requires positive ") + label);
    }
    return static_cast<long>(number);
}

void restoreStringHasherTable(const QJsonObject& table, const App::StringHasherRef& hasher)
{
    if (table.value("schemaVersion").toInt() != 2
        || table.value("nativeVersion").toInt() != 1) {
        throw std::runtime_error("Prior StringHasher schema or native version is unsupported");
    }
    const QJsonArray entries = table.value("entries").toArray();
    if (entries.size() > 1000000) {
        throw std::runtime_error("Prior StringHasher entry count exceeds the candidate limit");
    }
    long previousId = 0;
    for (const QJsonValue entryValue : entries) {
        if (!entryValue.isObject()) {
            throw std::runtime_error("Prior StringHasher entry must be an object");
        }
        const QJsonObject entry = entryValue.toObject();
        const long id = requireStringId(entry.value("id"), "entry ID");
        if (id != previousId + 1) {
            throw std::runtime_error("Prior StringHasher IDs must be contiguous and ordered");
        }
        const int flags = entry.value("flags").toInt(-1);
        constexpr int knownFlags = 0xff;
        if (flags < 0 || (flags & ~knownFlags) != 0) {
            throw std::runtime_error("Prior StringHasher entry contains unknown flags");
        }
        if ((flags & ((1 << 0) | (1 << 1))) != 0) {
            throw std::runtime_error(
                "Candidate bridge cannot losslessly restore binary or one-way-hashed StringHasher entries");
        }
        if (!entry.value("data").isString() || !entry.value("postfix").isString()
            || !entry.value("relatedIds").isArray()) {
            throw std::runtime_error("Prior StringHasher entry payload is incomplete");
        }
        const QByteArray data = entry.value("data").toString().toUtf8();
        const QByteArray postfix = entry.value("postfix").toString().toUtf8();
        const QJsonArray relatedValues = entry.value("relatedIds").toArray();
        std::vector<long> relatedIds;
        relatedIds.reserve(relatedValues.size());
        for (const QJsonValue relatedValue : relatedValues) {
            const long relatedId = requireStringId(relatedValue, "related ID");
            if (relatedId >= id || !hasher->getID(relatedId)) {
                throw std::runtime_error("Prior StringHasher related ID is unresolved or forward-referenced");
            }
            relatedIds.push_back(relatedId);
        }

        App::StringIDRef restored;
        if ((flags & (1 << 3)) == 0) {
            if (flags != 0 && flags != (1 << 7)) {
                throw std::runtime_error("Prior non-postfixed StringHasher flags cannot be reconstructed");
            }
            restored = hasher->getID(data, App::StringHasher::Option::None);
        }
        else {
            QByteArray mappedData = data;
            if ((flags & ((1 << 4) | (1 << 6))) != 0) {
                mappedData += '1';
            }
            Data::MappedName mapped(mappedData);
            if (!postfix.isEmpty()) {
                mapped += postfix;
            }
            int internalCount = 0;
            internalCount += (flags & (1 << 2)) != 0 ? 1 : 0;
            internalCount += (flags & (1 << 4)) != 0 ? 1 : 0;
            if (internalCount > static_cast<int>(relatedIds.size())) {
                throw std::runtime_error("Prior StringHasher internal dependencies are truncated");
            }
            Data::ElementIDRefs externalRefs;
            for (std::size_t index = static_cast<std::size_t>(internalCount);
                 index < relatedIds.size(); ++index) {
                externalRefs.push_back(hasher->getID(relatedIds[index]));
            }
            restored = hasher->getID(mapped, externalRefs);
        }
        if (!restored || restored.value() != id) {
            throw std::runtime_error("Prior StringHasher entry did not restore to its persistent ID");
        }
        if ((flags & (1 << 7)) != 0) {
            restored.setPersistent(true);
        }
        const App::StringID& actual = restored.deref();
        std::vector<long> actualRelated;
        for (const App::StringIDRef& related : actual.relatedIDs()) {
            actualRelated.push_back(related.value());
        }
        if (stringIdFlags(actual) != flags || actual.data() != data || actual.postfix() != postfix
            || actualRelated != relatedIds) {
            throw std::runtime_error("Prior StringHasher entry changed during strict restoration");
        }
        previousId = id;
    }
}

void restorePriorStringHasher(const QJsonArray& inputs, const App::StringHasherRef& hasher)
{
    QByteArray canonical;
    QJsonObject selected;
    for (const QJsonValue inputValue : inputs) {
        const QJsonObject evidence = inputValue.toObject().value("namingEvidence").toObject();
        if (evidence.isEmpty()) {
            continue;
        }
        const QJsonValue tableValue = evidence.value("stringHasher");
        if (!tableValue.isObject()) {
            const QJsonArray mappedNames = evidence.value("mappedNames").toArray();
            const bool requiresHasher = std::any_of(
                mappedNames.begin(), mappedNames.end(), [](const QJsonValue& mappedValue) {
                    const QJsonObject reference = mappedValue.toObject().value("reference").toObject();
                    return !reference.value("stringIds").toArray().isEmpty()
                        || reference.contains("prefixStringId");
                });
            if (requiresHasher) {
                throw std::runtime_error("Prior mapped-name IDs require StringHasher evidence");
            }
            continue;
        }
        const QJsonObject table = tableValue.toObject();
        const QByteArray encoded = QJsonDocument(table).toJson(QJsonDocument::Compact);
        if (canonical.isEmpty()) {
            canonical = encoded;
            selected = table;
        }
        else if (canonical != encoded) {
            throw std::runtime_error("Candidate bridge cannot merge inconsistent prior StringHasher tables");
        }
    }
    if (!selected.isEmpty()) {
        restoreStringHasherTable(selected, hasher);
    }
}

QJsonObject findSourceInput(const QJsonArray& inputs, const QString& source)
{
    for (const QJsonValue value : inputs) {
        const QJsonObject input = value.toObject();
        if (input.value("inputId").toString() == source || input.value("role").toString() == source) {
            return input;
        }
    }
    if (source == "object" && !inputs.isEmpty()) {
        return inputs.first().toObject();
    }
    if (source == "tool" && inputs.size() > 1) {
        return inputs.at(1).toObject();
    }
    throw std::runtime_error("FreeCAD naming history references an unknown input");
}

struct SourceName
{
    Data::MappedName name;
    QString persistentId;
};

SourceName sourceNameFor(const QJsonObject& input,
                         const QString& kind,
                         int sourceIndex,
                         const App::StringHasherRef& hasher)
{
    const QString indexed = titleKind(kind) + QString::number(sourceIndex + 1);
    const QJsonValue evidenceValue = input.value("namingEvidence");
    if (!evidenceValue.isObject()) {
        return {Data::MappedName(indexed.toStdString()), indexed};
    }
    const QJsonArray mappedNames = evidenceValue.toObject().value("mappedNames").toArray();
    for (const QJsonValue value : mappedNames) {
        const QJsonObject mapped = value.toObject();
        if (mapped.value("kind").toString() != kind
            || mapped.value("resultIndex").toInt(-1) != sourceIndex) {
            continue;
        }
        const QJsonObject reference = requireObject(mapped, "reference");
        const QJsonArray stringIds = reference.value("stringIds").toArray();
        for (const QJsonValue idValue : stringIds) {
            if (!hasher->getID(requireStringId(idValue, "mapped-name string ID"))) {
                throw std::runtime_error("Prior mapped-name string ID is absent from StringHasher");
            }
        }
        if (reference.contains("prefixStringId")) {
            const long prefixId = requireStringId(reference.value("prefixStringId"),
                                                  "mapped-name prefix ID");
            if (!hasher->getID(prefixId)) {
                throw std::runtime_error("Prior mapped-name prefix ID is absent from StringHasher");
            }
        }
        Data::MappedName name(requireString(reference, "name").toStdString());
        const QString postfix = reference.value("postfix").toString();
        if (!postfix.isEmpty()) {
            name += postfix.toStdString();
        }
        return {name, mapped.value("resultPersistentId").toString(indexed)};
    }
    throw std::runtime_error("Prior naming evidence has no matching source subshape");
}

QJsonObject mappedReference(const Data::MappedName& name, const Data::ElementIDRefs& stringIds)
{
    QJsonArray ids;
    for (const App::StringIDRef& stringId : stringIds) {
        ids.append(static_cast<qint64>(stringId.value()));
    }
    QJsonObject reference {
        {"name", QString::fromUtf8(name.dataBytes())},
        {"postfix", QString::fromUtf8(name.postfixBytes())},
        {"stringIds", ids},
    };
    const App::StringID::IndexID prefix = App::StringID::fromString(name.dataBytes());
    if (prefix) {
        reference.insert("prefixStringId", static_cast<qint64>(prefix.id));
    }
    return reference;
}

std::string evidenceJsonImpl(const std::string& requestJson)
{
    ensureApplication();
    QJsonParseError parseError;
    const QJsonDocument document =
        QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &parseError);
    if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
        throw std::runtime_error("FreeCAD naming request is not valid JSON");
    }
    const QJsonObject request = document.object();
    if (request.value("schemaVersion").toInt() != ABI_VERSION) {
        throw std::runtime_error("FreeCAD naming request schemaVersion is unsupported");
    }
    const QString stageId = requireString(request, "stageId");
    const QString resultObjectId = requireString(request, "resultObjectId");
    const long resultTag = requirePositiveTag(request, "resultObjectTag");
    const QString operation = requireString(request, "operation");
    const QString operationPostfix = canonicalOperation(operation);
    const QJsonArray inputs = request.value("inputs").toArray();
    const QJsonObject history = requireObject(request, "history");
    const QJsonArray records = history.value("records").toArray();
    if (inputs.isEmpty() || records.isEmpty()) {
        throw std::runtime_error("FreeCAD naming request requires inputs and native history records");
    }

    App::StringHasherRef hasher(new App::StringHasher());
    hasher->setSaveAll(true);
    restorePriorStringHasher(inputs, hasher);
    auto elementMap = std::make_shared<Data::ElementMap>();
    elementMap->hasher = hasher;
    QJsonArray mappedNames;
    std::set<std::string> resultKeys;
    std::map<std::string, int> sourceOrdinals;

    for (const QJsonValue recordValue : records) {
        const QJsonObject record = recordValue.toObject();
        const QString relation = requireString(record, "relation");
        if (relation == "deleted") {
            continue;
        }
        if (relation != "modified" && relation != "generated") {
            throw std::runtime_error("FreeCAD naming history relation is unsupported");
        }
        const QString sourceKind = requireString(record, "kind");
        const QString resultKind = record.value("resultKind").toString(sourceKind);
        titleKind(sourceKind);
        const QString resultType = titleKind(resultKind);
        const int sourceIndex = requireIndex(record, "sourceIndex");
        const QString source = record.value("sourceId").toString(
            requireString(record, "source"));
        const QJsonObject input = findSourceInput(inputs, source);
        const QString sourceObjectId = requireString(input, "objectId");
        const long sourceTag = requirePositiveTag(input, "objectTag");
        const SourceName sourceName = sourceNameFor(input, sourceKind, sourceIndex, hasher);
        QJsonArray resultIndexes = record.value("resultIndexes").toArray();
        if (resultIndexes.isEmpty() && record.contains("resultIndex")) {
            resultIndexes.append(record.value("resultIndex"));
        }
        if (resultIndexes.isEmpty()) {
            throw std::runtime_error("Non-deleted FreeCAD naming history requires result indexes");
        }
        const std::string ordinalKey = (sourceObjectId + '|' + sourceKind + '|'
                                        + QString::number(sourceIndex) + '|' + relation)
                                           .toStdString();
        for (const QJsonValue resultValue : resultIndexes) {
            const int resultIndex = requireIndex(QJsonObject {{"value", resultValue}}, "value");
            const std::string resultKey =
                (resultKind + ':' + QString::number(resultIndex)).toStdString();
            if (!resultKeys.insert(resultKey).second) {
                throw std::runtime_error("FreeCAD naming history maps the same result subshape twice");
            }
            const int ordinal = ++sourceOrdinals[ordinalKey];
            std::ostringstream postfix;
            postfix << (relation == "modified" ? Data::POSTFIX_MOD : Data::POSTFIX_GEN);
            if (ordinal > 1) {
                postfix << ordinal;
            }
            Data::MappedName encoded(sourceName.name);
            Data::ElementIDRefs stringIds;
            elementMap->encodeElementName(
                resultType.at(0).toLatin1(),
                encoded,
                postfix,
                &stringIds,
                resultTag,
                operationPostfix.toUtf8().constData(),
                sourceTag);
            const Data::IndexedName resultIndexed(
                (resultType + QString::number(resultIndex + 1)).toUtf8());
            const Data::MappedName stored =
                elementMap->setElementName(resultIndexed, encoded, resultTag, &stringIds);
            if (!stored) {
                throw std::runtime_error("FreeCAD ElementMap rejected a native history mapping");
            }
            mappedNames.append(QJsonObject {
                {"kind", resultKind},
                {"resultIndex", resultIndex},
                {"resultPersistentId", resultType + QString::number(resultIndex + 1)},
                {"relation", relation},
                {"reference", mappedReference(stored, stringIds)},
                {"sourceRefs", QJsonArray {QJsonObject {
                    {"objectId", sourceObjectId},
                    {"persistentId", sourceName.persistentId},
                    {"stageId", input.value("stageId")},
                }}},
            });
        }
    }
    if (mappedNames.isEmpty()) {
        throw std::runtime_error("FreeCAD naming history has no result subshape evidence");
    }

    elementMap->beforeSave(hasher);
    std::ostringstream saved;
    elementMap->save(saved);
    const QJsonObject response {
        {"schemaVersion", ABI_VERSION},
        {"stageId", stageId},
        {"resultObjectId", resultObjectId},
        {"status", "native-evidence"},
        {"mappedNames", mappedNames},
        {"stringHasher", stringHasherJson(hasher)},
        {"elementMap2", parseElementMap(saved.str())},
    };
    return QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
}

std::string evidenceJson(const std::string& requestJson)
{
    try {
        return evidenceJsonImpl(requestJson);
    }
    catch (const Base::Exception& exception) {
        return QJsonDocument(QJsonObject {
            {"schemaVersion", ABI_VERSION},
            {"status", "error"},
            {"error", QString::fromStdString("FreeCAD private naming failure: "
                                              + exception.getMessage())},
        }).toJson(QJsonDocument::Compact).toStdString();
    }
    catch (const std::exception& exception) {
        return QJsonDocument(QJsonObject {
            {"schemaVersion", ABI_VERSION},
            {"status", "error"},
            {"error", QString::fromUtf8(exception.what())},
        }).toJson(QJsonDocument::Compact).toStdString();
    }
    catch (...) {
        return R"({"schemaVersion":1,"status":"error","error":"Unknown native exception"})";
    }
}

int freecadNamingAbiVersion()
{
    return ABI_VERSION;
}

std::string freecadNamingCapabilitiesJson()
{
    return QJsonDocument(QJsonObject {
        {"schemaVersion", ABI_VERSION},
        {"freecadVersion", FREECAD_VERSION},
        {"sourceCommit", FREECAD_COMMIT},
        {"mappedNameRef", true},
        {"stringHasher", true},
        {"elementMap2", true},
        {"operations", QJsonArray {
            "fuse", "cut", "common", "rotate", "pad", "pocket", "loft", "pipe",
            "revolution", "groove", "fillet", "chamfer", "hole", "draft", "thickness",
            "linear-pattern", "polar-pattern", "mirrored", "multi-transform",
        }},
        {"maxRequestBytes", 16 * 1024 * 1024},
        {"maxResponseBytes", 32 * 1024 * 1024},
    }).toJson(QJsonDocument::Compact).toStdString();
}
}  // namespace

EMSCRIPTEN_BINDINGS(freecad_private_naming_bridge)
{
    emscripten::function("freecadNamingAbiVersion", &freecadNamingAbiVersion);
    emscripten::function("freecadNamingCapabilitiesJson", &freecadNamingCapabilitiesJson);
    emscripten::function("freecadNamingEvidenceJson", &evidenceJson);
}
