/*
 * Copyright 2020 The gRPC Authors
 *
 * 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.
 */

package io.grpc.xds;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.util.Durations;
import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol;
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext;
import io.grpc.Internal;
import io.grpc.xds.client.EnvoyProtoData;
import io.grpc.xds.internal.security.SslContextProviderSupplier;
import java.net.InetAddress;
import java.util.Objects;
import javax.annotation.Nullable;

/**
 * Defines gRPC data types for Envoy protobuf messages used in xDS protocol on the server side,
 * similar to how {@link EnvoyProtoData} defines it for the client side.
 */
@Internal
public final class EnvoyServerProtoData {

  // Prevent instantiation.
  private EnvoyServerProtoData() {
  }

  public abstract static class BaseTlsContext {
    protected final CommonTlsContext commonTlsContext;

    protected BaseTlsContext(CommonTlsContext commonTlsContext) {
      this.commonTlsContext = checkNotNull(commonTlsContext, "commonTlsContext cannot be null.");
    }

    public CommonTlsContext getCommonTlsContext() {
      return commonTlsContext;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof BaseTlsContext)) {
        return false;
      }
      BaseTlsContext that = (BaseTlsContext) o;
      return Objects.equals(commonTlsContext, that.commonTlsContext);
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(commonTlsContext);
    }
  }

  public static final class UpstreamTlsContext extends BaseTlsContext {

    @VisibleForTesting
    public UpstreamTlsContext(CommonTlsContext commonTlsContext) {
      super(commonTlsContext);
    }

    public static UpstreamTlsContext fromEnvoyProtoUpstreamTlsContext(
        io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
            upstreamTlsContext) {
      return new UpstreamTlsContext(upstreamTlsContext.getCommonTlsContext());
    }

    @Override
    public String toString() {
      return "UpstreamTlsContext{" + "commonTlsContext=" + commonTlsContext + '}';
    }
  }

  public static final class DownstreamTlsContext extends BaseTlsContext {

    private final boolean requireClientCertificate;

    @VisibleForTesting
    public DownstreamTlsContext(
        CommonTlsContext commonTlsContext, boolean requireClientCertificate) {
      super(commonTlsContext);
      this.requireClientCertificate = requireClientCertificate;
    }

    public static DownstreamTlsContext fromEnvoyProtoDownstreamTlsContext(
        io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
            downstreamTlsContext) {
      return new DownstreamTlsContext(downstreamTlsContext.getCommonTlsContext(),
        downstreamTlsContext.hasRequireClientCertificate());
    }

    public boolean isRequireClientCertificate() {
      return requireClientCertificate;
    }

    @Override
    public String toString() {
      return "DownstreamTlsContext{"
          + "commonTlsContext="
          + commonTlsContext
          + ", requireClientCertificate="
          + requireClientCertificate
          + '}';
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      if (!super.equals(o)) {
        return false;
      }
      DownstreamTlsContext that = (DownstreamTlsContext) o;
      return requireClientCertificate == that.requireClientCertificate;
    }

    @Override
    public int hashCode() {
      return Objects.hash(super.hashCode(), requireClientCertificate);
    }
  }

  @AutoValue
  abstract static class CidrRange {

    abstract InetAddress addressPrefix();

    abstract int prefixLen();

    static CidrRange create(InetAddress addressPrefix, int prefixLen) {
      return new AutoValue_EnvoyServerProtoData_CidrRange(
          addressPrefix, prefixLen);
    }
  }

  enum ConnectionSourceType {
    // Any connection source matches.
    ANY,

    // Match a connection originating from the same host.
    SAME_IP_OR_LOOPBACK,

    // Match a connection originating from a different host.
    EXTERNAL
  }

  /**
   * Corresponds to Envoy proto message
   * {@link io.envoyproxy.envoy.config.listener.v3.FilterChainMatch}.
   */
  @AutoValue
  abstract static class FilterChainMatch {

    abstract int destinationPort();

    abstract ImmutableList<CidrRange> prefixRanges();

    abstract ImmutableList<String> applicationProtocols();

    abstract ImmutableList<CidrRange> sourcePrefixRanges();

    abstract ConnectionSourceType connectionSourceType();

    abstract ImmutableList<Integer> sourcePorts();

    abstract ImmutableList<String> serverNames();

    abstract String transportProtocol();

    public static FilterChainMatch create(int destinationPort,
        ImmutableList<CidrRange> prefixRanges,
        ImmutableList<String> applicationProtocols, ImmutableList<CidrRange> sourcePrefixRanges,
        ConnectionSourceType connectionSourceType, ImmutableList<Integer> sourcePorts,
        ImmutableList<String> serverNames, String transportProtocol) {
      return new AutoValue_EnvoyServerProtoData_FilterChainMatch(
          destinationPort, prefixRanges, applicationProtocols, sourcePrefixRanges,
          connectionSourceType, sourcePorts, serverNames, transportProtocol);
    }
  }

  /**
   * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.config.listener.v3.FilterChain}.
   */
  @AutoValue
  abstract static class FilterChain {

    // Must be unique per server instance (except the default chain).
    abstract String name();

    // TODO(sanjaypujare): flatten structure by moving FilterChainMatch class members here.
    abstract FilterChainMatch filterChainMatch();

    abstract HttpConnectionManager httpConnectionManager();

    @Nullable
    abstract SslContextProviderSupplier sslContextProviderSupplier();

    static FilterChain create(
        String name,
        FilterChainMatch filterChainMatch,
        HttpConnectionManager httpConnectionManager,
        @Nullable DownstreamTlsContext downstreamTlsContext,
        TlsContextManager tlsContextManager) {
      SslContextProviderSupplier sslContextProviderSupplier =
          downstreamTlsContext == null
              ? null : new SslContextProviderSupplier(downstreamTlsContext, tlsContextManager);
      return new AutoValue_EnvoyServerProtoData_FilterChain(
          name, filterChainMatch, httpConnectionManager, sslContextProviderSupplier);
    }
  }

  /**
   * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.config.listener.v3.Listener} and
   * related classes.
   */
  @AutoValue
  abstract static class Listener {

    abstract String name();

    @Nullable
    abstract String address();

    abstract ImmutableList<FilterChain> filterChains();

    @Nullable
    abstract FilterChain defaultFilterChain();

    @Nullable
    abstract Protocol protocol();

    static Listener create(
        String name,
        @Nullable String address,
        ImmutableList<FilterChain> filterChains,
        @Nullable FilterChain defaultFilterChain,
        @Nullable Protocol protocol) {
      return new AutoValue_EnvoyServerProtoData_Listener(name, address, filterChains,
          defaultFilterChain, protocol);
    }
  }

  /**
   * Corresponds to Envoy proto message {@link
   * io.envoyproxy.envoy.config.cluster.v3.OutlierDetection}. Only the fields supported by gRPC are
   * included.
   *
   * <p>Protobuf Duration fields are represented in their string format (e.g. "10s").
   */
  @AutoValue
  abstract static class OutlierDetection {

    @Nullable
    abstract Long intervalNanos();

    @Nullable
    abstract Long baseEjectionTimeNanos();

    @Nullable
    abstract Long maxEjectionTimeNanos();

    @Nullable
    abstract Integer maxEjectionPercent();

    @Nullable
    abstract SuccessRateEjection successRateEjection();

    @Nullable
    abstract FailurePercentageEjection failurePercentageEjection();

    static OutlierDetection create(
        @Nullable Long intervalNanos,
        @Nullable Long baseEjectionTimeNanos,
        @Nullable Long maxEjectionTimeNanos,
        @Nullable Integer maxEjectionPercentage,
        @Nullable SuccessRateEjection successRateEjection,
        @Nullable FailurePercentageEjection failurePercentageEjection) {
      return new AutoValue_EnvoyServerProtoData_OutlierDetection(intervalNanos,
          baseEjectionTimeNanos, maxEjectionTimeNanos, maxEjectionPercentage, successRateEjection,
          failurePercentageEjection);
    }

    static OutlierDetection fromEnvoyOutlierDetection(
        io.envoyproxy.envoy.config.cluster.v3.OutlierDetection envoyOutlierDetection) {

      Long intervalNanos = envoyOutlierDetection.hasInterval()
          ? Durations.toNanos(envoyOutlierDetection.getInterval()) : null;
      Long baseEjectionTimeNanos = envoyOutlierDetection.hasBaseEjectionTime()
          ? Durations.toNanos(envoyOutlierDetection.getBaseEjectionTime()) : null;
      Long maxEjectionTimeNanos = envoyOutlierDetection.hasMaxEjectionTime()
          ? Durations.toNanos(envoyOutlierDetection.getMaxEjectionTime()) : null;
      Integer maxEjectionPercentage = envoyOutlierDetection.hasMaxEjectionPercent()
          ? envoyOutlierDetection.getMaxEjectionPercent().getValue() : null;

      SuccessRateEjection successRateEjection;
      // If success rate enforcement has been turned completely off, don't configure this ejection.
      if (envoyOutlierDetection.hasEnforcingSuccessRate()
          && envoyOutlierDetection.getEnforcingSuccessRate().getValue() == 0) {
        successRateEjection = null;
      } else {
        Integer stdevFactor = envoyOutlierDetection.hasSuccessRateStdevFactor()
            ? envoyOutlierDetection.getSuccessRateStdevFactor().getValue() : null;
        Integer enforcementPercentage = envoyOutlierDetection.hasEnforcingSuccessRate()
            ? envoyOutlierDetection.getEnforcingSuccessRate().getValue() : null;
        Integer minimumHosts = envoyOutlierDetection.hasSuccessRateMinimumHosts()
            ? envoyOutlierDetection.getSuccessRateMinimumHosts().getValue() : null;
        Integer requestVolume = envoyOutlierDetection.hasSuccessRateRequestVolume()
            ? envoyOutlierDetection.getSuccessRateRequestVolume().getValue() : null;

        successRateEjection = SuccessRateEjection.create(stdevFactor, enforcementPercentage,
            minimumHosts, requestVolume);
      }

      FailurePercentageEjection failurePercentageEjection;
      if (envoyOutlierDetection.hasEnforcingFailurePercentage()
          && envoyOutlierDetection.getEnforcingFailurePercentage().getValue() == 0) {
        failurePercentageEjection = null;
      } else {
        Integer threshold = envoyOutlierDetection.hasFailurePercentageThreshold()
            ? envoyOutlierDetection.getFailurePercentageThreshold().getValue() : null;
        Integer enforcementPercentage = envoyOutlierDetection.hasEnforcingFailurePercentage()
            ? envoyOutlierDetection.getEnforcingFailurePercentage().getValue() : null;
        Integer minimumHosts = envoyOutlierDetection.hasFailurePercentageMinimumHosts()
            ? envoyOutlierDetection.getFailurePercentageMinimumHosts().getValue() : null;
        Integer requestVolume = envoyOutlierDetection.hasFailurePercentageRequestVolume()
            ? envoyOutlierDetection.getFailurePercentageRequestVolume().getValue() : null;

        failurePercentageEjection = FailurePercentageEjection.create(threshold,
            enforcementPercentage, minimumHosts, requestVolume);
      }

      return create(intervalNanos, baseEjectionTimeNanos, maxEjectionTimeNanos,
          maxEjectionPercentage, successRateEjection, failurePercentageEjection);
    }
  }

  @AutoValue
  abstract static class SuccessRateEjection {

    @Nullable
    abstract Integer stdevFactor();

    @Nullable
    abstract Integer enforcementPercentage();

    @Nullable
    abstract Integer minimumHosts();

    @Nullable
    abstract Integer requestVolume();

    static SuccessRateEjection create(
        @Nullable Integer stdevFactor,
        @Nullable Integer enforcementPercentage,
        @Nullable Integer minimumHosts,
        @Nullable Integer requestVolume) {
      return new AutoValue_EnvoyServerProtoData_SuccessRateEjection(stdevFactor,
          enforcementPercentage, minimumHosts, requestVolume);
    }
  }

  @AutoValue
  abstract static class FailurePercentageEjection {

    @Nullable
    abstract Integer threshold();

    @Nullable
    abstract Integer enforcementPercentage();

    @Nullable
    abstract Integer minimumHosts();

    @Nullable
    abstract Integer requestVolume();

    static FailurePercentageEjection create(
        @Nullable Integer threshold,
        @Nullable Integer enforcementPercentage,
        @Nullable Integer minimumHosts,
        @Nullable Integer requestVolume) {
      return new AutoValue_EnvoyServerProtoData_FailurePercentageEjection(threshold,
          enforcementPercentage, minimumHosts, requestVolume);
    }
  }
}
