/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.apache.ratis.protocol;

import org.apache.ratis.proto.RaftProtos.CommitInfoProto;
import org.apache.ratis.protocol.exceptions.AlreadyClosedException;
import org.apache.ratis.protocol.exceptions.DataStreamException;
import org.apache.ratis.protocol.exceptions.LeaderNotReadyException;
import org.apache.ratis.protocol.exceptions.LeaderSteppingDownException;
import org.apache.ratis.protocol.exceptions.NotLeaderException;
import org.apache.ratis.protocol.exceptions.NotReplicatedException;
import org.apache.ratis.protocol.exceptions.RaftException;
import org.apache.ratis.protocol.exceptions.ReadException;
import org.apache.ratis.protocol.exceptions.ReadIndexException;
import org.apache.ratis.protocol.exceptions.StateMachineException;
import org.apache.ratis.protocol.exceptions.TransferLeadershipException;
import org.apache.ratis.util.JavaUtils;
import org.apache.ratis.util.Preconditions;
import org.apache.ratis.util.ProtoUtils;
import org.apache.ratis.util.ReflectionUtils;

import java.util.Collection;
import java.util.Collections;

/**
 * Reply from server to client
 */
public class RaftClientReply extends RaftClientMessage {
  /**
   * To build {@link RaftClientReply}
   */
  public static class Builder {
    private ClientId clientId;
    private RaftPeerId serverId;
    private RaftGroupId groupId;
    private long callId;

    private boolean success;
    private Message message;
    private RaftException exception;

    private long logIndex;
    private Collection<CommitInfoProto> commitInfos;

    public RaftClientReply build() {
      return new RaftClientReply(clientId, serverId, groupId, callId,
          success, message, exception, logIndex, commitInfos);
    }

    public Builder setClientId(ClientId clientId) {
      this.clientId = clientId;
      return this;
    }

    public Builder setServerId(RaftPeerId serverId) {
      this.serverId = serverId;
      return this;
    }

    public Builder setGroupId(RaftGroupId groupId) {
      this.groupId = groupId;
      return this;
    }

    public Builder setCallId(long callId) {
      this.callId = callId;
      return this;
    }

    public Builder setSuccess(boolean success) {
      this.success = success;
      return this;
    }

    public Builder setSuccess() {
      return setSuccess(true);
    }

    public Builder setException(RaftException exception) {
      this.exception = exception;
      return this;
    }

    public Builder setMessage(Message message) {
      this.message = message;
      return this;
    }

    public Builder setLogIndex(long logIndex) {
      this.logIndex = logIndex;
      return this;
    }

    public Builder setCommitInfos(Collection<CommitInfoProto> commitInfos) {
      this.commitInfos = commitInfos;
      return this;
    }

    public Builder setServerId(RaftGroupMemberId serverId) {
      return setServerId(serverId.getPeerId())
          .setGroupId(serverId.getGroupId());
    }

    public Builder setClientInvocationId(ClientInvocationId invocationId) {
      return setClientId(invocationId.getClientId())
          .setCallId(invocationId.getLongId());
    }

    public Builder setRequest(RaftClientRequest request) {
      return setClientId(request.getClientId())
          .setServerId(request.getServerId())
          .setGroupId(request.getRaftGroupId())
          .setCallId(request.getCallId());
    }
  }

  public static Builder newBuilder() {
    return new Builder();
  }

  private final boolean success;

  /**
   * We mainly track two types of exceptions here:
   * 1. NotLeaderException if the server is not leader
   * 2. StateMachineException if the server's state machine returns an exception
   */
  private final RaftException exception;
  private final Message message;

  /**
   * This field is the log index of the transaction
   * if (1) the request is {@link org.apache.ratis.proto.RaftProtos.RaftClientRequestProto.TypeCase#WRITE} and (2) the
   * reply is success.
   * Otherwise, this field is not used.
   */
  private final long logIndex;
  /** The commit information when the reply is created. */
  private final Collection<CommitInfoProto> commitInfos;

  @SuppressWarnings("parameternumber")
  RaftClientReply(ClientId clientId, RaftPeerId serverId, RaftGroupId groupId,
      long callId, boolean success, Message message, RaftException exception,
      long logIndex, Collection<CommitInfoProto> commitInfos) {
    super(clientId, serverId, groupId, callId);
    this.success = success;
    this.message = message;
    this.exception = exception;
    this.logIndex = logIndex;
    this.commitInfos = commitInfos != null? commitInfos: Collections.emptyList();

    if (exception != null) {
      Preconditions.assertTrue(!success,
          () -> "Inconsistent parameters: success && exception != null: " + this);
      Preconditions.assertTrue(ReflectionUtils.isInstance(exception,
          AlreadyClosedException.class,
          NotLeaderException.class, NotReplicatedException.class,
          LeaderNotReadyException.class, StateMachineException.class, DataStreamException.class,
          LeaderSteppingDownException.class, TransferLeadershipException.class, ReadException.class,
          ReadIndexException.class),
          () -> "Unexpected exception class: " + this);
    }
  }

  /**
   * Get the commit information for the entire group.
   * The commit information may be unavailable for exception reply.
   *
   * @return the commit information if it is available; otherwise, return null.
   */
  public Collection<CommitInfoProto> getCommitInfos() {
    return commitInfos;
  }

  @Override
  public final boolean isRequest() {
    return false;
  }

  public long getLogIndex() {
    return logIndex;
  }

  @Override
  public String toString() {
    return super.toString() + ", "
        + (isSuccess()? "SUCCESS":  "FAILED " + exception)
        + ", logIndex=" + getLogIndex() + ", commits" + ProtoUtils.toString(commitInfos);
  }

  public boolean isSuccess() {
    return success;
  }

  public Message getMessage() {
    return message;
  }

  /** If this reply has {@link AlreadyClosedException}, return it; otherwise return null. */
  public AlreadyClosedException getAlreadyClosedException() {
    return JavaUtils.cast(exception, AlreadyClosedException.class);
  }

  /** If this reply has {@link NotLeaderException}, return it; otherwise return null. */
  public NotLeaderException getNotLeaderException() {
    return JavaUtils.cast(exception, NotLeaderException.class);
  }

  /** If this reply has {@link NotReplicatedException}, return it; otherwise return null. */
  public NotReplicatedException getNotReplicatedException() {
    return JavaUtils.cast(exception, NotReplicatedException.class);
  }

  /** If this reply has {@link StateMachineException}, return it; otherwise return null. */
  public StateMachineException getStateMachineException() {
    return JavaUtils.cast(exception, StateMachineException.class);
  }

  /** If this reply has {@link DataStreamException}, return it; otherwise return null. */
  public DataStreamException getDataStreamException() {
    return JavaUtils.cast(exception, DataStreamException.class);
  }

  public LeaderNotReadyException getLeaderNotReadyException() {
    return JavaUtils.cast(exception, LeaderNotReadyException.class);
  }

  public LeaderSteppingDownException getLeaderSteppingDownException() {
    return JavaUtils.cast(exception, LeaderSteppingDownException.class);
  }

  public TransferLeadershipException getTransferLeadershipException() {
    return JavaUtils.cast(exception, TransferLeadershipException.class);
  }

  public ReadException getReadException() {
    return JavaUtils.cast(exception, ReadException.class);
  }

  public ReadIndexException getReadIndexException() {
    return JavaUtils.cast(exception, ReadIndexException.class);
  }

  /** @return the exception, if there is any; otherwise, return null. */
  public RaftException getException() {
    return exception;
  }
}
