// Dstl (c) Crown Copyright 2017
package uk.gov.dstl.baleen.resources;

import java.util.Arrays;
import java.util.Map;

import org.apache.uima.fit.descriptor.ConfigurationParameter;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceSpecifier;

import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.mongodb.MongoClient;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoDatabase;

import uk.gov.dstl.baleen.core.utils.ConfigUtils;
import uk.gov.dstl.baleen.exceptions.BaleenException;
import uk.gov.dstl.baleen.uima.BaleenResource;

/**
 * <b>Shared resource for accessing Mongo</b>
 *
 * <p>This resource removes the need for individual annotators to establish their own connections to
 * Mongo, instead providing a single MongoClient instance for Baleen that can be used. This provides
 * benefits such as reduced configuration, reduced repeated code, and support for connection
 * pooling.
 *
 * @baleen.javadoc
 */
public class SharedMongoResource extends BaleenResource {

  /** default key for accessing the mongo resource */
  public static final String RESOURCE_KEY = "mongo";

  private MongoClient m;
  private MongoDatabase db;

  /**
   * The Mongo host to connect to
   *
   * @baleen.config localhost
   */
  public static final String PARAM_HOST = "mongo.host";

  @ConfigurationParameter(name = PARAM_HOST, defaultValue = "localhost")
  private String mongoHost;

  /**
   * The port to connect to Mongo on
   *
   * @baleen.config 27017
   */
  public static final String PARAM_PORT = "mongo.port";

  @ConfigurationParameter(name = PARAM_PORT, defaultValue = "27017")
  private String mongoPortString;

  // Parse the port config parameter into this variable to avoid issues with parameter types
  private int mongoPort;

  /**
   * The Mongo database to connect to
   *
   * @baleen.config baleen
   */
  public static final String PARAM_DB = "mongo.db";

  @ConfigurationParameter(name = PARAM_DB, defaultValue = "baleen")
  private String mongoDb;

  /**
   * The username to use for authentication. If left blank, then authentication will not be used.
   *
   * @baleen.config
   */
  public static final String PARAM_USER = "mongo.user";

  @ConfigurationParameter(name = PARAM_USER, defaultValue = "")
  private String mongoUser;

  /**
   * The password to use for authentication. If left blank, then authentication will not be used.
   *
   * @baleen.config
   */
  public static final String PARAM_PASS = "mongo.pass";

  @ConfigurationParameter(name = PARAM_PASS, defaultValue = "")
  private String mongoPass;

  @Override
  protected boolean doInitialize(
      ResourceSpecifier aSpecifier, Map<String, Object> aAdditionalParams)
      throws ResourceInitializationException {
    mongoPort = ConfigUtils.stringToInteger(mongoPortString, 27017);

    try {
      connectToMongo(mongoHost, mongoPort, mongoDb, mongoUser, mongoPass);
    } catch (BaleenException be) {
      throw new ResourceInitializationException(be);
    }

    getMonitor().info("Initialised shared Mongo resource");
    return true;
  }

  private void connectToMongo(
      String host, int port, String database, String username, String password)
      throws BaleenException {
    try {
      // Connect to Mongo
      ServerAddress sa = new ServerAddress(host, port);
      Optional<MongoCredential> cred = createCredentials(username, password, database);

      m = createMongoClient(sa, cred);

      getMonitor().debug("Getting Mongo Database '{}'", db);
      db = m.getDatabase(database);
    } catch (Exception e) {
      throw new BaleenException("Unable to connect to Mongo", e);
    }
  }

  protected MongoClient createMongoClient(ServerAddress sa, Optional<MongoCredential> credentials) {
    if (!credentials.isPresent()) {
      getMonitor().debug("Connecting to Mongo without authentication");
      return new MongoClient(sa);
    } else {
      getMonitor()
          .debug(
              "Connecting to Mongo with authentication as user '{}'",
              credentials.get().getUserName());
      return new MongoClient(sa, Arrays.asList(credentials.get()));
    }
  }

  /**
   * Creates a MongoCredential if a username, password and database are supplied, or returns
   * absent() otherwise
   *
   * @param username The username to connect to Mongo with
   * @param password The password to connect to Mongo with
   * @param database The database to connect to
   */
  public static Optional<MongoCredential> createCredentials(
      String username, String password, String database) {
    if (!Strings.isNullOrEmpty(username)
        && !Strings.isNullOrEmpty(password)
        && !Strings.isNullOrEmpty(database)) {
      return Optional.of(
          MongoCredential.createMongoCRCredential(username, database, password.toCharArray()));
    } else {
      return Optional.absent();
    }
  }

  @Override
  protected void doDestroy() {
    getMonitor().debug("Disconnecting from Mongo");
    m.close();

    db = null;
    m = null;
  }

  /**
   * Get the Mongo DB accessor.
   *
   * @return the monogo db, or null if the source has been destroyed or an initialisation error
   *     occurred.
   */
  public MongoDatabase getDB() {
    return db;
  }

  /**
   * Creates a Mongo URI string, without the username:password
   *
   * @return
   */
  public String getMongoURI() {
    return "mongodb://" + mongoHost + ":" + mongoPort + "/" + mongoDb;
  }
}
