/*
 * Copyright 2021 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef INCLUDED_BUILDBOXCOMMON_PROTOJSONUTILS
#define INCLUDED_BUILDBOXCOMMON_PROTOJSONUTILS

#include <buildboxcommon_protos.h>

#include <buildboxcommon_exception.h>
#include <buildboxcommon_logging.h>

#include <memory>
#include <string>

#include <google/protobuf/util/json_util.h>

namespace buildboxcommon {
struct ProtoJsonUtils {
    /*
     * Helper functions to generate the JSON representation of protobuf
     * messages.
     */
    typedef google::protobuf::util::JsonPrintOptions JsonPrintOptions;

    /*
     * Generate a string containing the representation of the given protobuf
     * message.
     *
     * The `options` parameter controls the format of the output.
     *
     * If the conversion fails, returns `nullptr` and logs an ERROR message.
     *
     */
    static std::unique_ptr<std::string>
    protoToJsonString(const google::protobuf::Message &message,
                      const JsonPrintOptions &options)
    {
        return protobufMessageToJsonString(message, options);
    }

    /* For most protobuf types it is straightforward to obtain their JSON
     * representation by using
     * `google::protobuf::util::MessageToJsonString()`. However, a
     * pre-condition of that method is that all messages stored in fields of
     * type `Any` are of a known type.
     *
     * (See https://gitlab.com/BuildGrid/buildbox/buildbox/-/issues/81).
     *
     * There is potential for problems with `ExecutedActionMetadata`. That
     * message has an `auxiliary_metadata` field of type `Any`, which could be
     * populated by a REAPI runner with arbitrary contents.
     *
     * `ProtoJsonUtils` helpers will consider the contents of that field valid
     * only if they contain at most a single entry of type `Digest`. That is
     * the only type that a `buildboxcommon::Runner` writes to that field, and,
     * being part `remote_execution.proto`, is a well-known type.
     *
     * Any other types stored in `auxiliary_metadata` will cause the whole
     * field to be discarded before generating the JSON representation of the
     * `ExecutedActionMetadata` message.
     *
     * As a consequence, these methods will succeed in converting protobuf
     * messages produced by other REAPI tools, but will potentially lose
     * auxiliary metadata that does not follow the convention followed by
     * buildbox.
     */
    static std::unique_ptr<std::string>
    protoToJsonString(const ExecutedActionMetadata &executedActionMetadata,
                      const JsonPrintOptions &options);
    /*
     * The problematic `ExecutedActionMetadata` type is used in the REAPI
     * to populate `ActionResult.execution_metadata.auxiliary_metadata`. Thus
     * we also overload the method that converts an `ActionResult` to JSON.
     */
    static std::unique_ptr<std::string>
    protoToJsonString(const ActionResult &actionResult,
                      const JsonPrintOptions &options);

  private:
    static std::unique_ptr<std::string>
    protobufMessageToJsonString(const google::protobuf::Message &message,
                                const JsonPrintOptions &options)
    {
        auto jsonPtr = std::make_unique<std::string>();
        const auto conversionStatus =
            google::protobuf::util::MessageToJsonString(message, jsonPtr.get(),
                                                        options);
        if (conversionStatus.ok()) {
            return jsonPtr;
        }

        BUILDBOX_LOG_ERROR("Unable to convert " << message.GetTypeName()
                                                << " to json format");
        return nullptr;
    }

    /*
     * Returns whether the `ExecutedActionMetadata.auxiliary_metadata` field
     * contains at most one entry of type `Digest`, which is how
     * `buildboxcommon::Runner` populates that field.
     *
     * (Detecting the case where the field contains something else will allow
     * discarding potentially unknown protobuf types written by other tools
     * that would be impossible to convert to JSON.)
     */
    static bool executedActionAuxiliaryMetadataIsValid(
        const ExecutedActionMetadata &executedActionMetadata);
};

// protobuf has changed fields in this struct in the past, so
// this function sets the value portably.
void jsonOptionsAlwaysPrintFieldsWithNoPresence(
    ProtoJsonUtils::JsonPrintOptions &options);

} // namespace buildboxcommon

#endif
