/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.exception.runtime;

import alluxio.exception.status.AlluxioStatusException;
import alluxio.grpc.ErrorInfo;
import alluxio.grpc.ErrorType;
import alluxio.grpc.RetryInfo;

import com.google.common.base.Preconditions;
import com.google.protobuf.Any;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.ProtoUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.channels.ClosedChannelException;
import java.nio.file.attribute.UserPrincipalNotFoundException;
import java.util.concurrent.CompletionException;
import java.util.concurrent.RejectedExecutionException;
import javax.annotation.Nullable;
import javax.security.sasl.SaslException;

/**
 * Alluxio RuntimeException. This Every developer should throw this kind of exception
 * when you really need to throw exception and surface this exception to user or when you want to
 * catch low level exception from external dependency and transform it into AlluxioRuntimeException.
 *
 * There are 3 ways of using this class here:
 * 1. Carefully pick a convenient class like {@link InternalRuntimeException}.
 * 2. Throw new AlluxioRuntimeException directly.
 * 3. Try not to use AlluxioRuntimeException.from(cause). This method used when you catch a very
 *    general cause(ex.{@link IOException}) and is mainly for compatibility
 */
public class AlluxioRuntimeException extends RuntimeException {
  private final Status mStatus;
  private final Any[] mDetails;
  private final boolean mRetryable;
  private final ErrorType mErrorType;

  /**
   * @param status  the grpc status code for this exception
   * @param message the error message
   * @param cause   the exception
   * @param errorType error type
   * @param retryable client can retry or not
   * @param details the additional information needed
   */
  public AlluxioRuntimeException(Status status, String message, @Nullable Throwable cause,
      ErrorType errorType, boolean retryable, @Nullable Any... details) {
    super(message, cause);
    Preconditions.checkNotNull(status, "status");
    Preconditions.checkArgument(status != Status.OK, "OK is not an error status");
    mStatus = status.withCause(cause).withDescription(message);
    mDetails = details;
    mRetryable = retryable;
    mErrorType =  errorType;
  }

  /**
   * @return grpc status
   */
  public Status getStatus() {
    return mStatus;
  }

    /**
   * @return can be retried or not
   */
  public boolean isRetryable() {
    return mRetryable;
  }

  /**
   * @return a gRPC status exception representation of this exception
   */
  public StatusException toGrpcStatusException() {
    return mStatus.withCause(getCause()).withDescription(getMessage()).asException(getMetadata());
  }

  private Metadata getMetadata() {
    Metadata trailers = new Metadata();
    trailers.put(ProtoUtils.keyForProto(RetryInfo.getDefaultInstance()),
        RetryInfo.newBuilder().setIsRetryable(mRetryable).build());
    trailers.put(ProtoUtils.keyForProto(ErrorInfo.getDefaultInstance()),
        ErrorInfo.newBuilder().setErrorType(mErrorType).build());
    if (mDetails != null) {
      for (Any detail : mDetails) {
        trailers.put(ProtoUtils.keyForProto(Any.getDefaultInstance()), detail);
      }
    }
    return trailers;
  }

  /**
   * @return a gRPC status runtime exception representation of this exception
   */
  public StatusRuntimeException toGrpcStatusRuntimeException() {
    return mStatus.withCause(getCause()).withDescription(getMessage())
        .asRuntimeException(getMetadata());
  }

  /**
   * Wrap a RejectedExecutionException with io.grpc RESOURCE_EXHAUSTED
   * exception.
   * @param ex
   * @return AlluxioRuntimeException
   */
  public static AlluxioRuntimeException from(RejectedExecutionException ex) {
    return new AlluxioRuntimeException(Status.RESOURCE_EXHAUSTED, "StageOverload",
        ex, ErrorType.User, true);
  }

  /**
   * Converts an arbitrary throwable to an Alluxio runtime exception.
   * @param t exception
   * @return alluxio runtime exception
   */
  public static AlluxioRuntimeException from(Throwable t) {
    if (t instanceof AlluxioRuntimeException) {
      return (AlluxioRuntimeException) t;
    }
    if (t instanceof AlluxioStatusException) {
      return from((AlluxioStatusException) t);
    }
    if (t instanceof IOException) {
      return from((IOException) t);
    }
    if (t instanceof RuntimeException) {
      return from((RuntimeException) t);
    }
    return new UnknownRuntimeException(t);
  }

  /**
   * Converts an arbitrary RuntimeException to an Alluxio runtime exception.
   * @param t exception
   * @return alluxio runtime exception
   */
  public static AlluxioRuntimeException from(RuntimeException t) {
    if (t instanceof RejectedExecutionException) {
      return from((RejectedExecutionException) t);
    }
    if (t instanceof IllegalArgumentException) {
      return new InvalidArgumentRuntimeException(t);
    }
    if (t instanceof IllegalStateException || t instanceof NullPointerException) {
      return new InternalRuntimeException(t);
    }
    if (t instanceof CompletionException) {
      return from(t.getCause());
    }
    if (t instanceof UnsupportedOperationException) {
      return new UnimplementedRuntimeException(t, ErrorType.External);
    }
    if (t instanceof SecurityException) {
      return new PermissionDeniedRuntimeException(t);
    }
    return new UnknownRuntimeException(t);
  }

  /**
   * Converts an arbitrary AlluxioStatusException to an Alluxio runtime exception.
   * @param t exception
   * @return alluxio runtime exception
   */
  public static AlluxioRuntimeException from(AlluxioStatusException t) {
    return new AlluxioRuntimeException(t.getStatus(), t.getMessage(), t.getCause(), ErrorType.User,
        false);
  }

  /**
   * Converts an IOException to a corresponding runtime exception.
   *
   * @param ioe the IO exception to convert
   * @return the corresponding status exception
   */
  public static AlluxioRuntimeException from(IOException ioe) {
    if (ioe instanceof AlluxioStatusException) {
      return from((AlluxioStatusException) ioe);
    }
    if (ioe instanceof FileNotFoundException) {
      return new NotFoundRuntimeException(ioe);
    }
    if (ioe instanceof MalformedURLException) {
      return new InvalidArgumentRuntimeException(ioe);
    }
    if (ioe instanceof UserPrincipalNotFoundException || ioe instanceof SaslException) {
      return new UnauthenticatedRuntimeException(ioe);
    }
    if (ioe instanceof ClosedChannelException) {
      return new FailedPreconditionRuntimeException(ioe);
    }
    return new UnknownRuntimeException(ioe);
  }

  @Override
  @Nullable
  public String getMessage() {
    String message = super.getMessage();
    if (message == null && getCause() != null) {
      message = getCause().getMessage();
    }
    if (message == null) {
      message = mStatus.getDescription();
    }
    return message;
  }
}
