package org.apache.manifoldcf.agents.output.solr;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
import org.apache.solr.client.solrj.impl.ClusterStateProvider;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.common.SolrException;

public class ModifiedCloudHttp2SolrClient extends ModifiedCloudSolrClient {

  private static final long serialVersionUID = -7543846119917075693L;
  private final ClusterStateProvider stateProvider;
  private final ModifiedLBHttp2SolrClient lbClient;
  private final ModifiedHttp2SolrClient myClient;
  private final boolean clientIsInternal;

  /**
   * Create a new client object that connects to Zookeeper and is always aware of the SolrCloud state. If there is a fully redundant Zookeeper quorum and SolrCloud has enough replicas for every shard
   * in a collection, there is no single point of failure. Updates will be sent to shard leaders by default.
   *
   * @param builder a {@link ModifiedHttp2SolrClient.Builder} with the options used to create the client.
   */
  protected ModifiedCloudHttp2SolrClient(final Builder builder) {
    super(builder.shardLeadersOnly, builder.parallelUpdates, builder.directUpdatesToLeadersOnly);
    if (builder.httpClient == null) {
      this.clientIsInternal = true;
      if (builder.internalClientBuilder == null) {
        this.myClient = new ModifiedHttp2SolrClient.Builder().build();
      } else {
        this.myClient = builder.internalClientBuilder.build();
      }
    } else {
      this.clientIsInternal = false;
      this.myClient = builder.httpClient;
    }
    if (builder.stateProvider == null) {
      if (builder.zkHosts != null && builder.solrUrls != null) {
        throw new IllegalArgumentException("Both zkHost(s) & solrUrl(s) have been specified. Only specify one.");
      }
      if (builder.zkHosts != null) {
        this.stateProvider = ClusterStateProvider.newZkClusterStateProvider(builder.zkHosts, builder.zkChroot);
      } else if (builder.solrUrls != null && !builder.solrUrls.isEmpty()) {
        try {
          this.stateProvider = new ModifiedHttp2ClusterStateProvider(builder.solrUrls, builder.httpClient);
        } catch (final Exception e) {
          throw new RuntimeException("Couldn't initialize a HttpClusterStateProvider (is/are the " + "Solr server(s), " + builder.solrUrls + ", down?)", e);
        }
      } else {
        throw new IllegalArgumentException("Both zkHosts and solrUrl cannot be null.");
      }
    } else {
      this.stateProvider = builder.stateProvider;
    }
    this.lbClient = new ModifiedLBHttp2SolrClient(myClient);
  }

  @Override
  public void close() throws IOException {
    stateProvider.close();
    lbClient.close();

    if (clientIsInternal && myClient != null) {
      myClient.close();
    }

    super.close();
  }

  @Override
  public ModifiedLBHttp2SolrClient getLbClient() {
    return lbClient;
  }

  @Override
  public ClusterStateProvider getClusterStateProvider() {
    return stateProvider;
  }

  public ModifiedHttp2SolrClient getHttpClient() {
    return myClient;
  }

  @Override
  protected boolean wasCommError(final Throwable rootCause) {
    return false;
  }

  /** Constructs {@link CloudHttp2SolrClient} instances from provided configuration. */
  public static class Builder {
    protected Collection<String> zkHosts = new ArrayList<>();
    protected List<String> solrUrls = new ArrayList<>();
    protected String zkChroot;
    protected ModifiedHttp2SolrClient httpClient;
    protected boolean shardLeadersOnly = true;
    protected boolean directUpdatesToLeadersOnly = false;
    protected boolean parallelUpdates = true;
    protected ClusterStateProvider stateProvider;
    protected ModifiedHttp2SolrClient.Builder internalClientBuilder;

    /**
     * Provide a series of Solr URLs to be used when configuring {@link CloudHttp2SolrClient} instances. The solr client will use these urls to understand the cluster topology, which solr nodes are
     * active etc.
     *
     * <p>
     * Provided Solr URLs are expected to point to the root Solr path ("http://hostname:8983/solr"); they should not include any collections, cores, or other path components.
     *
     * <p>
     * Usage example:
     *
     * <pre>
     * final List&lt;String&gt; solrBaseUrls = new ArrayList&lt;String&gt;();
     * solrBaseUrls.add("http://solr1:8983/solr");
     * solrBaseUrls.add("http://solr2:8983/solr");
     * solrBaseUrls.add("http://solr3:8983/solr");
     * final SolrClient client = new CloudHttp2SolrClient.Builder(solrBaseUrls).build();
     * </pre>
     */
    public Builder(final List<String> solrUrls) {
      this.solrUrls = solrUrls;
    }

    /**
     * Provide a series of ZK hosts which will be used when configuring {@link CloudHttp2SolrClient} instances.
     *
     * <p>
     * Usage example when Solr stores data at the ZooKeeper root ('/'):
     *
     * <pre>
     * final List&lt;String&gt; zkServers = new ArrayList&lt;String&gt;();
     * zkServers.add("zookeeper1:2181");
     * zkServers.add("zookeeper2:2181");
     * zkServers.add("zookeeper3:2181");
     * final SolrClient client = new CloudHttp2SolrClient.Builder(zkServers, Optional.empty()).build();
     * </pre>
     *
     * Usage example when Solr data is stored in a ZooKeeper chroot:
     *
     * <pre>
     * final List&lt;String&gt; zkServers = new ArrayList&lt;String&gt;();
     * zkServers.add("zookeeper1:2181");
     * zkServers.add("zookeeper2:2181");
     * zkServers.add("zookeeper3:2181");
     * final SolrClient client = new CloudHttp2SolrClient.Builder(zkServers, Optional.of("/solr")).build();
     * </pre>
     *
     * @param zkHosts  a List of at least one ZooKeeper host and port (e.g. "zookeeper1:2181")
     * @param zkChroot the path to the root ZooKeeper node containing Solr data. Provide {@code
     *     java.util.Optional.empty()} if no ZK chroot is used.
     */
    public Builder(final List<String> zkHosts, final Optional<String> zkChroot) {
      this.zkHosts = zkHosts;
      if (zkChroot.isPresent())
        this.zkChroot = zkChroot.get();
    }

    /**
     * Tells {@link CloudHttp2SolrClient.Builder} that created clients should send direct updates to shard leaders only.
     *
     * <p>
     * UpdateRequests whose leaders cannot be found will "fail fast" on the client side with a {@link SolrException}
     */
    public Builder sendDirectUpdatesToShardLeadersOnly() {
      directUpdatesToLeadersOnly = true;
      return this;
    }

    /**
     * Tells {@link CloudHttp2SolrClient.Builder} that created clients can send updates to any shard replica (shard leaders and non-leaders).
     *
     * <p>
     * Shard leaders are still preferred, but the created clients will fallback to using other replicas if a leader cannot be found.
     */
    public Builder sendDirectUpdatesToAnyShardReplica() {
      directUpdatesToLeadersOnly = false;
      return this;
    }

    /**
     * Tells {@link CloudHttp2SolrClient.Builder} whether created clients should send shard updates serially or in parallel
     *
     * <p>
     * When an {@link UpdateRequest} affects multiple shards, {@link CloudHttp2SolrClient} splits it up and sends a request to each affected shard. This setting chooses whether those sub-requests are
     * sent serially or in parallel.
     *
     * <p>
     * If not set, this defaults to 'true' and sends sub-requests in parallel.
     */
    public Builder withParallelUpdates(final boolean parallelUpdates) {
      this.parallelUpdates = parallelUpdates;
      return this;
    }

    public Builder withHttpClient(final ModifiedHttp2SolrClient httpClient) {
      if (this.internalClientBuilder != null) {
        throw new IllegalStateException("The builder can't accept an httpClient AND an internalClientBuilder, only one of those can be provided");
      }
      this.httpClient = httpClient;
      return this;
    }

    /**
     * If provided, the CloudHttp2SolrClient will build it's internal ModifiedHttp2SolrClient using this builder (instead of the empty default one). Providing this builder allows users to configure
     * the internal clients (authentication, timeouts, etc).
     *
     * @param internalClientBuilder the builder to use for creating the internal http client.
     * @return this
     */
    public Builder withInternalClientBuilder(final ModifiedHttp2SolrClient.Builder internalClientBuilder) {
      if (this.httpClient != null) {
        throw new IllegalStateException("The builder can't accept an httpClient AND an internalClientBuilder, only one of those can be provided");
      }
      this.internalClientBuilder = internalClientBuilder;
      return this;
    }

    /** Create a {@link CloudHttp2SolrClient} based on the provided configuration. */
    public ModifiedCloudHttp2SolrClient build() {
      if (stateProvider == null) {
        if (!zkHosts.isEmpty()) {
          stateProvider = ClusterStateProvider.newZkClusterStateProvider(zkHosts, Builder.this.zkChroot);
        } else if (!this.solrUrls.isEmpty()) {
          try {
            stateProvider = new ModifiedHttp2ClusterStateProvider(solrUrls, httpClient);
          } catch (final Exception e) {
            throw new RuntimeException("Couldn't initialize a HttpClusterStateProvider (is/are the " + "Solr server(s), " + solrUrls + ", down?)", e);
          }
        } else {
          throw new IllegalArgumentException("Both zkHosts and solrUrl cannot be null.");
        }
      }
      return new ModifiedCloudHttp2SolrClient(this);
    }
  }
}
