/*
 * Copyright 2016 Google LLC
 *
 * 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 com.google.cloud.http;

import static com.google.common.base.MoreObjects.firstNonNull;

import com.google.api.client.extensions.appengine.http.UrlFetchTransport;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.gax.core.GaxProperties;
import com.google.api.gax.httpjson.HttpHeadersUtils;
import com.google.api.gax.httpjson.HttpJsonStatusCode;
import com.google.api.gax.rpc.ApiClientHeaderProvider;
import com.google.api.gax.rpc.EndpointContext;
import com.google.api.gax.rpc.HeaderProvider;
import com.google.api.gax.rpc.StatusCode;
import com.google.api.gax.rpc.UnauthenticatedException;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.http.HttpTransportFactory;
import com.google.cloud.NoCredentials;
import com.google.cloud.PlatformInformation;
import com.google.cloud.ServiceOptions;
import com.google.cloud.TransportOptions;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Objects;

/** Class representing service options for those services that use HTTP as the transport layer. */
public class HttpTransportOptions implements TransportOptions {

  private static final long serialVersionUID = 7890117765045419810L;
  private final int connectTimeout;
  private final int readTimeout;
  private final String httpTransportFactoryClassName;

  private transient HttpTransportFactory httpTransportFactory;

  public static class DefaultHttpTransportFactory implements HttpTransportFactory {

    private static final HttpTransportFactory INSTANCE = new DefaultHttpTransportFactory();

    @Override
    public HttpTransport create() {
      // Consider App Engine Standard
      if (PlatformInformation.isOnGAEStandard7()) {
        try {
          return new UrlFetchTransport();
        } catch (Exception ignore) {
          // Maybe not on App Engine
        }
      }
      return new NetHttpTransport();
    }
  }

  /** Builder for {@code HttpTransportOptions}. */
  public static class Builder {

    private HttpTransportFactory httpTransportFactory;
    private int connectTimeout = -1;
    private int readTimeout = -1;

    private Builder() {}

    private Builder(HttpTransportOptions options) {
      httpTransportFactory = options.httpTransportFactory;
      connectTimeout = options.connectTimeout;
      readTimeout = options.readTimeout;
    }

    public HttpTransportOptions build() {
      return new HttpTransportOptions(this);
    }

    /**
     * Sets the HTTP transport factory.
     *
     * @return the builder
     */
    public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory) {
      this.httpTransportFactory = httpTransportFactory;
      return this;
    }

    /**
     * Sets the timeout in milliseconds to establish a connection.
     *
     * @param connectTimeout connection timeout in milliseconds. 0 for an infinite timeout, a
     *     negative number for the default value (20000).
     * @return the builder
     */
    public Builder setConnectTimeout(int connectTimeout) {
      this.connectTimeout = connectTimeout;
      return this;
    }

    /**
     * Sets the timeout in milliseconds to read data from an established connection.
     *
     * @param readTimeout read timeout in milliseconds. 0 for an infinite timeout, a negative number
     *     for the default value (20000).
     * @return the builder
     */
    public Builder setReadTimeout(int readTimeout) {
      this.readTimeout = readTimeout;
      return this;
    }
  }

  public HttpTransportOptions(Builder builder) {
    httpTransportFactory =
        firstNonNull(
            builder.httpTransportFactory,
            ServiceOptions.getFromServiceLoader(
                HttpTransportFactory.class, DefaultHttpTransportFactory.INSTANCE));
    httpTransportFactoryClassName = httpTransportFactory.getClass().getName();
    connectTimeout = builder.connectTimeout;
    readTimeout = builder.readTimeout;
  }

  /** Returns the HTTP transport factory. */
  public HttpTransportFactory getHttpTransportFactory() {
    return httpTransportFactory;
  }

  /**
   * Returns a request initializer responsible for initializing requests according to service
   * options.
   */
  public HttpRequestInitializer getHttpRequestInitializer(
      final ServiceOptions<?, ?> serviceOptions) {
    Credentials scopedCredentials = serviceOptions.getScopedCredentials();
    final HttpRequestInitializer delegate =
        scopedCredentials != null && scopedCredentials != NoCredentials.getInstance()
            ? new HttpCredentialsAdapter(scopedCredentials)
            : null;
    HeaderProvider internalHeaderProvider =
        getInternalHeaderProviderBuilder(serviceOptions).build();
    final HeaderProvider headerProvider =
        serviceOptions.getMergedHeaderProvider(internalHeaderProvider);

    return new HttpRequestInitializer() {

      /**
       * Helper method to resolve the Universe Domain. First checks the user configuration from
       * ServiceOptions, then the Environment Variable. If both haven't been set, resolve the value
       * to be the Google Default Universe.
       */
      private String determineUniverseDomain() {
        String universeDomain = serviceOptions.getUniverseDomain();
        if (universeDomain == null) {
          universeDomain = System.getenv(EndpointContext.GOOGLE_CLOUD_UNIVERSE_DOMAIN);
        }
        return universeDomain == null ? Credentials.GOOGLE_DEFAULT_UNIVERSE : universeDomain;
      }

      @Override
      public void initialize(HttpRequest httpRequest) throws IOException {
        String configuredUniverseDomain = determineUniverseDomain();
        // Default to the GDU. Override with value in the Credentials if needed
        String credentialsUniverseDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE;

        // delegate is always HttpCredentialsAdapter or null (NoCredentials)
        if (delegate != null) {
          HttpCredentialsAdapter httpCredentialsAdapter = (HttpCredentialsAdapter) delegate;
          credentialsUniverseDomain = httpCredentialsAdapter.getCredentials().getUniverseDomain();
        }

        // Validate the universe domain before initializing the request
        if (!configuredUniverseDomain.equals(credentialsUniverseDomain)) {
          throw new UnauthenticatedException(
              new Throwable(
                  String.format(
                      EndpointContext.INVALID_UNIVERSE_DOMAIN_ERROR_TEMPLATE,
                      configuredUniverseDomain,
                      credentialsUniverseDomain)),
              HttpJsonStatusCode.of(StatusCode.Code.UNAUTHENTICATED),
              false);
        }

        if (delegate != null) {
          delegate.initialize(httpRequest);
        }

        if (connectTimeout >= 0) {
          httpRequest.setConnectTimeout(connectTimeout);
        }
        if (readTimeout >= 0) {
          httpRequest.setReadTimeout(readTimeout);
        }

        HttpHeadersUtils.setHeaders(httpRequest.getHeaders(), headerProvider.getHeaders());
      }
    };
  }

  ApiClientHeaderProvider.Builder getInternalHeaderProviderBuilder(
      ServiceOptions<?, ?> serviceOptions) {
    ApiClientHeaderProvider.Builder builder = ApiClientHeaderProvider.newBuilder();
    builder.setClientLibToken(
        ServiceOptions.getGoogApiClientLibName(),
        GaxProperties.getLibraryVersion(serviceOptions.getClass()));
    builder.setQuotaProjectIdToken(serviceOptions.getQuotaProjectId());
    return builder;
  }

  /**
   * Returns the timeout in milliseconds to establish a connection. 0 is an infinite timeout, a
   * negative number is the default value (20000).
   */
  public int getConnectTimeout() {
    return connectTimeout;
  }

  /**
   * Returns the timeout in milliseconds to read from an established connection. 0 is an infinite
   * timeout, a negative number is the default value (20000).
   */
  public int getReadTimeout() {
    return readTimeout;
  }

  public Builder toBuilder() {
    return new Builder(this);
  }

  @Override
  public int hashCode() {
    return Objects.hash(httpTransportFactoryClassName, connectTimeout, readTimeout);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    HttpTransportOptions other = (HttpTransportOptions) obj;
    return Objects.equals(httpTransportFactoryClassName, other.httpTransportFactoryClassName)
        && Objects.equals(connectTimeout, other.connectTimeout)
        && Objects.equals(readTimeout, other.readTimeout);
  }

  private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    input.defaultReadObject();
    httpTransportFactory = ServiceOptions.newInstance(httpTransportFactoryClassName);
  }

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