/*
 * Copyright 2021, TeamDev. All rights reserved.
 *
 * 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
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.spine.server.event;

import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.Any;
import io.spine.base.CommandMessage;
import io.spine.base.Identifier;
import io.spine.base.RejectionMessage;
import io.spine.base.RejectionThrowable;
import io.spine.core.Command;
import io.spine.core.Event;
import io.spine.core.EventContext;
import io.spine.core.EventId;
import io.spine.core.RejectionEventContext;
import io.spine.core.TenantId;
import io.spine.server.type.AbstractMessageEnvelope;
import io.spine.server.type.CommandEnvelope;
import io.spine.server.type.EventClass;
import io.spine.server.type.EventEnvelope;
import io.spine.server.type.SignalEnvelope;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.getRootCause;
import static com.google.common.base.Throwables.getStackTraceAsString;

/**
 * The holder of a rejection {@code Event} which provides convenient access to its properties.
 */
public final class RejectionEnvelope
        extends AbstractMessageEnvelope<EventId, Event, EventContext>
        implements SignalEnvelope<EventId, Event, EventContext> {

    /**
     * The default producer ID for rejection events.
     *
     * <p>Represented by a packed {@link com.google.protobuf.StringValue StringValue} of
     * {@code "Unknown"}.
     */
    @SuppressWarnings("DuplicateStringLiteralInspection") // Coincidence
    private static final Any DEFAULT_EVENT_PRODUCER = Identifier.pack("Unknown");

    private final EventEnvelope event;

    private RejectionEnvelope(EventEnvelope event) {
        super(event.outerObject());
        this.event = event;
    }

    /**
     * Creates a new {@code RejectionEnvelope} from the given event.
     *
     * <p>Throws an {@link IllegalArgumentException} if the given event is not a rejection.
     *
     * @param event the rejection event
     * @return new
     */
    public static RejectionEnvelope from(EventEnvelope event) {
        checkNotNull(event);
        checkArgument(event.isRejection(), "`%s` is not a rejection", event.messageClass());
        return new RejectionEnvelope(event);
    }

    /**
     * Creates an instance of {@code Rejection} from the rejected command and a {@link Throwable}
     * caused by the {@link RejectionThrowable}.
     *
     * <p>If the producer is not {@linkplain RejectionThrowable#initProducer(Any) set}, uses
     * the {@link #DEFAULT_EVENT_PRODUCER} as the producer.
     *
     * @param origin    the rejected command
     * @param throwable the caught error
     * @return new instance of {@code Rejection}
     * @throws IllegalArgumentException if the given {@link Throwable} is not caused by
     *                                  a {@link RejectionThrowable}
     */
    public static RejectionEnvelope from(CommandEnvelope origin, Throwable throwable) {
        checkNotNull(origin);
        checkNotNull(throwable);

        RejectionThrowable rt = unwrap(throwable);
        Event rejectionEvent = produceEvent(origin, rt);
        EventEnvelope event = EventEnvelope.of(rejectionEvent);

        return from(event);
    }

    private static RejectionThrowable unwrap(Throwable causedByRejection) {
        Throwable cause = getRootCause(causedByRejection);
        boolean correctType = cause instanceof RejectionThrowable;
        checkArgument(correctType);
        RejectionThrowable rt = (RejectionThrowable) cause;
        return rt;
    }

    private static Event produceEvent(CommandEnvelope origin, RejectionThrowable throwable) {
        Any producerId = throwable.producerId()
                                         .orElse(DEFAULT_EVENT_PRODUCER);
        EventFactory factory = EventFactory.on(origin, producerId);
        RejectionMessage thrownMessage = throwable.messageThrown();
        RejectionEventContext context = rejectionContext(origin.outerObject(), throwable);
        Event rejectionEvent = factory.createRejectionEvent(thrownMessage, null, context);
        return rejectionEvent;
    }

    /**
     * Constructs a new {@link RejectionEventContext} from the given command message and
     * {@link RejectionThrowable}.
     *
     * @param command
     *         the rejected command
     * @param th
     *         the thrown rejection
     * @return the new instance of {@code RejectionEventContext}
     */
    private static RejectionEventContext rejectionContext(Command command, RejectionThrowable th) {
        checkNotNull(command);
        checkNotNull(th);

        String stacktrace = getStackTraceAsString(th);
        return RejectionEventContext
                .newBuilder()
                .setCommand(command)
                .setStacktrace(stacktrace)
                .build();
    }

    @Override
    public TenantId tenantId() {
        return event.tenantId();
    }

    @Override
    public EventId id() {
        return event.id();
    }

    @Override
    public RejectionMessage message() {
        return (RejectionMessage) event.message();
    }

    @Override
    public EventClass messageClass() {
        EventClass eventClass = event.messageClass();
        @SuppressWarnings("unchecked") // Checked at runtime.
        Class<? extends RejectionMessage> value =
                (Class<? extends RejectionMessage>) eventClass.value();
        EventClass rejectionClass = EventClass.from(value);
        return rejectionClass;
    }

    @Override
    public EventContext context() {
        return event.context();
    }

    @VisibleForTesting
    public EventEnvelope getEvent() {
        return event;
    }

    /**
     * Obtains the origin command.
     *
     * @return the rejected command
     */
    public Command getOrigin() {
        return context().getRejection()
                        .getCommand();
    }

    /**
     * Obtains the origin command message.
     *
     * @return the rejected command message
     */
    public CommandMessage getOriginMessage() {
        CommandMessage commandMessage = context().getRejection()
                                                 .getCommand()
                                                 .enclosedMessage();
        return commandMessage;
    }
}

@@ -2491,52 +2491,70 @@
   * 
-The default producer ID for rejection events
+A placeholder for telling that a rejection origin is not known
 .%0A  
@@ -2783,38 +2783,24 @@
 nal Any 
-DEFAULT_EVENT_
 PRODUCER
  = Ident
@@ -2791,16 +2791,24 @@
 PRODUCER
+_UNKNOWN
  = Ident
@@ -3405,16 +3405,17 @@
 ejection
+.
 %22, event
@@ -3758,38 +3758,24 @@
 %7B@link #
-DEFAULT_EVENT_
 PRODUCER
 %7D as the
@@ -3766,16 +3766,24 @@
 PRODUCER
+_UNKNOWN
 %7D as the
@@ -4902,16 +4902,32 @@
 ucerId =
+%0A               
  throwab
@@ -4971,54 +4971,32 @@
     
-                .orElse(DEFAULT_EVENT_PRODUCER
+.orElse(PRODUCER_UNKNOWN
 );%0A 

Improve names and layout