package com.atlassian.bitbucket.mesh.rpc.v1;

import com.google.protobuf.InvalidProtocolBufferException;
import io.grpc.Metadata;
import io.grpc.Status;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static java.util.Objects.requireNonNull;

public class RpcErrorUtils {

    private static final Metadata.Key<RpcError> KEY_ERROR = Metadata.Key.of(
            "mesh-error-bin", new Metadata.BinaryMarshaller<RpcError>() {

                @Override
                public RpcError parseBytes(byte[] serialized) {
                    try {
                        return RpcError.parseFrom(serialized);
                    } catch (InvalidProtocolBufferException e) {
                        LoggerFactory.getLogger(RpcErrorUtils.class)
                                .warn("Failed to parse RpcError from trailer", e);
                        return null;
                    }
                }

                @Override
                public byte[] toBytes(RpcError value) {
                    return value.toByteArray();
                }
            });

    private RpcErrorUtils() {
        throw new UnsupportedOperationException(getClass().getSimpleName() +
                " is a utility class and should not be instantiated");
    }

    /**
     * Parses an {@link RpcError} from the provided {@link Metadata}, if one is present <i>and can be parsed
     * successfully</i>.
     *
     * @param metadata the metadata to parse an {@link RpcError} from
     * @return the parsed {@link RpcError}, or {@code null} if no error was included in the provided metadata
     *         or the error could not be parsed
     */
    @Nullable
    public static RpcError fromMetadata(@Nonnull Metadata metadata) {
        return requireNonNull(metadata, "metadata").get(KEY_ERROR);
    }

    /**
     * Parses an {@link RpcError} from the provided {@code Throwable}, if it has gRPC {@link Metadata} attached.
     *
     * @param t the exception to parse an {@link RpcError} from
     * @return the parsed {@link RpcError}, or {@code null} if the exception has no metadata, the metadata has
     *         no included error, or the included error could not be parsed
     */
    @Nullable
    public static RpcError fromThrowable(@Nonnull Throwable t) {
        Metadata metadata = Status.trailersFromThrowable(t);

        return metadata == null ? null : fromMetadata(metadata);
    }

    /**
     * Serializes the provided {@link RpcError} and returns gRPC {@link Metadata} with a key containing the
     * serialized bytes.
     *
     * @param error the error to serialize
     * @return metadata containing the provided error
     */
    @Nonnull
    public static Metadata toMetadata(@Nonnull RpcError error) {
        requireNonNull(error, "error");

        Metadata metadata = new Metadata();
        metadata.put(KEY_ERROR, error);

        return metadata;
    }
}
