/*
 * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * 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.helidon.service.configuration.hikaricp.accs;

import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;

import javax.sql.DataSource; // for javadoc only

import io.helidon.service.configuration.api.System;
import io.helidon.service.configuration.hikaricp.HikariCPServiceConfiguration;
import io.helidon.service.configuration.hikaricp.HikariCPServiceConfigurationProvider;

/**
 * A {@link HikariCPServiceConfigurationProvider} that provides {@link
 * HikariCPServiceConfiguration} instances when running on the <a
 * href="https://docs.oracle.com/en/cloud/paas/app-container-cloud/index.html">Oracle
 * Application Container Cloud Service</a> {@linkplain System system}.
 *
 * @author <a href="mailto:laird.nelson@oracle.com">Laird Nelson</a>
 *
 * @see HikariCPServiceConfiguration
 *
 * @see HikariCPServiceConfigurationProvider
 */
public class HikariCPServiceConfigurationACCSProvider extends HikariCPServiceConfigurationProvider {


  /*
   * Constructors.
   */


  /**
   * Creates a new {@link HikariCPServiceConfigurationACCSProvider}.
   */
  public HikariCPServiceConfigurationACCSProvider() {
    super();
  }


  /*
   * Instance methods.
   */


  /**
   * Overrides the {@link
   * HikariCPServiceConfigurationProvider#appliesTo(Properties,
   * System, Properties)} method to return {@code true} if the
   * supplied {@link System} {@linkplain System#isEnabled() is
   * enabled} and has a {@linkplain System#getName() name} equal to
   * {@code accs}, and if its {@linkplain System#getenv() environment}
   * contains at least one key starting with either {@code MYSQLCS_}
   * or {@code DBAAS_}, and if the {@link
   * HikariCPServiceConfigurationProvider#appliesTo(Properties,
   * System, Properties)} method also returns {@code true}.
   *
   * @param properties a {@link Properties} instance that will be used
   * as the basis of the {@link HikariCPServiceConfiguration}
   * implementation that will be returned by the {@link
   * #create(Properties, System, Properties)} method; must not be
   * {@code null}
   *
   * @param system a {@link System} determined to be in effect; may,
   * strictly speaking, be {@code null} but ordinarily is non-{@code
   * null} and {@linkplain System#isEnabled() enabled}
   *
   * @param coordinates a {@link Properties} instance representing the
   * meta-properties in effect; may be {@code null}
   *
   * @return {@code true} if this {@link
   * HikariCPServiceConfigurationACCSProvider} applies to the
   * configuration space implied by the supplied parameters; {@code
   * false} otherwise
   *
   * @exception NullPointerException if {@code properties} is {@code null}
   */
  @Override
  protected boolean appliesTo(final Properties properties, final System system, final Properties coordinates) {
    Objects.requireNonNull(properties);
    boolean returnValue = false;
    if (system != null && system.isEnabled() && "accs".equalsIgnoreCase(system.getName())) {
      final Map<? extends String, ? extends String> env = system.getenv();
      if (env != null && !env.isEmpty()) {
        final Set<? extends String> keys = env.keySet();
        if (keys != null && !keys.isEmpty()) {
          boolean dbaas = false;
          boolean mysqlcs = false;
          for (final String key : keys) {
            if (key != null) {
              if (dbaas) {
                if (mysqlcs || key.startsWith("MYSQLCS_")) {
                  mysqlcs = true;
                  break;
                }
              } else if (mysqlcs) {
                if (key.startsWith("DBAAS_")) {
                  dbaas = true;
                  break;
                }
              } else if (key.startsWith("DBAAS_")) {
                dbaas = true;
              } else if (key.startsWith("MYSQLCS_")) {
                mysqlcs = true;
              }
            }
          }
          if (dbaas) {
            if (mysqlcs) {
              // We deliberately get out of the business of trying to
              // pick among competing service bindings.
              returnValue = false;
            } else {
              returnValue = super.appliesTo(properties, system, coordinates);
            }
          } else if (mysqlcs) {
            returnValue = super.appliesTo(properties, system, coordinates);
          } else {
            returnValue = false;
          }
        }
      }
    }
    return returnValue;
  }

  /**
   * Overrides the {@link
   * HikariCPServiceConfigurationProvider#installDataSourceProperties(Properties,
   * System, Properties, String)} to install data source-related
   * properties discovered in the <a
   * href="https://docs.oracle.com/en/cloud/paas/app-container-cloud/csjse/exploring-application-deployments-page.html#GUID-843F7013-B6FA-45E0-A9D3-29A0EFD53E11">Oracle
   * Application Container Cloud Service environment</a>.
   *
   * <p>While reading the documentation for this method, note that the
   * Oracle Application Container Cloud Service sets up automatic
   * service bindings for only Oracle or MySQL databases.</p>
   *
   * <p>This method:</p>
   *
   * <ol>
   *
   * <li>Calls the {@link
   * HikariCPServiceConfigurationProvider#installDataSourceProperties(Properties,
   * System, Properties, String)} method.</li>
   *
   * <li>Checks to see if a property named {@code
   * javax.sql.DataSource.}<em>{@code dataSourceName}</em>{@code
   * .explicitlyConfigured} has a {@link String} value equal to
   * anything other than {@code true}, including {@code null}.</li>
   *
   * <li>If so, <strong>and only if {@code dataSourceName} is {@code
   * null}</strong>, then it {@linkplain
   * Properties#setProperty(String, String) sets} certain properties
   * on {@code target}.</li>
   *
   * <li>Specifically, if there is a MySQL service binding and not an
   * Oracle database service binding, the following properties will be
   * set:
   *
   * <ol>
   *
   * <li>{@code javax.sql.DataSource.dataSourceClassName =
   * com.mysql.cj.jdbc.MysqlDataSource}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.url =
   * jdbc:mysql://${MYSQLCS_CONNECT_STRING}}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.description =
   * Autodiscovered}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.user =
   * ${MYSQLCS_USER_NAME}}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.password =
   * ${MYSQLCS_PASSWORD}}</li>
   *
   * </ol></li>
   *
   * <li>If instead there is an Oracle database service binding, but
   * not also a MySQL service binding, the following properties will
   * be set:
   *
   * <ol>
   *
   * <li>{@code javax.sql.DataSource.dataSourceClassName =
   * oracle.jdbc.pool.OracleDataSource}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.url =
   * jdbc:oracle:thin:@//${DBAAS_DEFAULT_CONNECT_DESCRIPTOR}}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.description =
   * Autodiscovered}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.user =
   * ${DBAAS_USER}}</li>
   *
   * <li>{@code javax.sql.DataSource.dataSource.password =
   * ${DBAAS_PASSWORD}}</li>
   *
   * </ol></li>
   *
   * </ol>
   *
   * @param target a {@link Properties} instance that will be used as
   * the basis of the {@link HikariCPServiceConfiguration}
   * implementation that will be returned by the {@link
   * #create(Properties, System, Properties)} method and into which
   * properties may be installed; must not be {@code null}
   *
   * @param system a {@link System} determined to be in effect; may,
   * strictly speaking, be {@code null} but ordinarily is non-{@code
   * null} and {@linkplain System#isEnabled() enabled}
   *
   * @param coordinates a {@link Properties} instance representing the
   * meta-properties in effect; may be {@code null}
   *
   * @param dataSourceName the data source name in question; may be
   * {@code null}
   *
   * @exception NullPointerException if {@code target} is {@code null}
   *
   * @see
   * HikariCPServiceConfigurationProvider#installDataSourceProperties(Properties,
   * System, Properties, String)
   */
  @Override
  protected void installDataSourceProperties(final Properties target,
                                             final System system,
                                             final Properties coordinates,
                                             String dataSourceName) {
    Objects.requireNonNull(target);
    super.installDataSourceProperties(target, system, coordinates, dataSourceName);

    if (!"true".equalsIgnoreCase(this.getDataSourceProperty(target,
                                                            system,
                                                            coordinates,
                                                            dataSourceName,
                                                            "explicitlyConfigured"))) {
      final String prefix = this.getPrefix();
      assert prefix != null;
      assert !prefix.isEmpty();

      // For future reference, see also:
      // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html
      // and
      // https://docs.oracle.com/database/121/JAJDB/oracle/jdbc/pool/OracleDataSource.html.

      final String dataSourceClassName;
      final String description;
      final String url;
      final String urlValue = getJdbcUrl(system, dataSourceName);
      final String user;
      final String password;
      if (dataSourceName == null) {
        dataSourceClassName = prefix + ".dataSourceClassName";
        description = prefix + ".dataSource.description";
        url = prefix + ".dataSource.url";
        user = prefix + ".dataSource.user";
        password = prefix + ".dataSource.password";
      } else {
        dataSourceName = dataSourceName.trim();
        if (dataSourceName.isEmpty()) {
          dataSourceClassName = prefix + ".dataSourceClassName";
          description = prefix + ".dataSource.description";
          url = prefix + ".dataSource.url";
          user = prefix + ".dataSource.user";
          password = prefix + ".dataSource.password";
        } else {
          dataSourceClassName = prefix + "." + dataSourceName + ".dataSourceClassName";
          description = prefix + "." + dataSourceName + ".dataSource.description";
          url = prefix + "." + dataSourceName + ".dataSource.url";
          user = prefix + "." + dataSourceName + ".dataSource.user";
          password = prefix + "." + dataSourceName + ".dataSource.password";
        }
      }

      target.setProperty(dataSourceClassName, getDataSourceClassName(urlValue));
      target.setProperty(url, urlValue);
      target.setProperty(description, "Autodiscovered");
      target.setProperty(user, getUser(system, dataSourceName));
      target.setProperty(password, getPassword(system, dataSourceName));
    }
  }

  /**
   * Returns the name of a Java class that implements the {@link
   * DataSource} interface that is appropriate for the supplied JDBC
   * URL.
   *
   * <p>This method may return {@code null}.</p>
   *
   * <p>Overrides of this method may return {@code null}.</p>
   *
   * <p>Overrides of this method must return the same value for the
   * same input.</p>
   *
   * <p>This implementation returns {@code
   * com.mysql.cj.jdbc.MysqlDataSource} if the supplied {@code
   * jdbcUrl} starts with {@code jdbc:mysql:}, and returns {@code
   * oracle.jdbc.pool.OracleDataSource} if the supplied {@code
   * jdbcUrl} starts with {@code jdbc:oracle:}, and {@code null} in
   * all other cases.</p>
   *
   * @param jdbcUrl a JDBC URL in {@link String} form; must not be
   * {@code null}
   *
   * @return a {@link DataSource} implementation class name, or {@code
   * null}
   *
   * @exception NullPointerException if {@code jdbcUrl} is {@code
   * null}
   */
  protected String getDataSourceClassName(final String jdbcUrl) {
    Objects.requireNonNull(jdbcUrl);
    String returnValue = null;
    if (jdbcUrl.startsWith("jdbc:") && jdbcUrl.length() > "jdbc:".length()) {
      final int colonIndex = jdbcUrl.indexOf(':', "jdbc:".length());
      if (colonIndex > 0) {
        final String type = jdbcUrl.substring("jdbc:".length(), colonIndex);
        assert type != null;
        assert !type.isEmpty();
        switch (type) {
        case "mysql":
          returnValue = "com.mysql.cj.jdbc.MysqlDataSource";
          break;
        case "oracle":
          returnValue = "oracle.jdbc.pool.OracleDataSource";
          break;
        default:
          break;
        }
      }
    }
    return returnValue;
  }

  private static String getUser(final System system, final String suppliedDataSourceName) {
    String returnValue = null;
    if (system != null) {

      final Map<?, ? extends String> env = system.getenv();
      assert env != null;

      String dataSourceName = null;
      if (suppliedDataSourceName != null) {
        dataSourceName = suppliedDataSourceName.trim().toUpperCase();
        if (dataSourceName.isEmpty()) {
          dataSourceName = null;
        }
      }

      if (dataSourceName == null) {
        if (env.containsKey("DBAAS_USER")) {
          if (!env.containsKey("MYSQLCS_USER_NAME")) {
            returnValue = env.get("DBAAS_USER");
          }
        } else if (env.containsKey("MYSQLCS_USER_NAME")) {
          returnValue = env.get("MYSQLCS_USER_NAME");
        }
      } else if (env.containsKey("DBAAS_" + dataSourceName + "_USER")) {
        if (!env.containsKey("MYSQLCS_" + dataSourceName + "_USER_NAME")) {
          returnValue = env.get("DBAAS_" + dataSourceName + "_USER");
        }
      } else if (env.containsKey("MYSQLCS_ " + dataSourceName + "_USER_NAME")) {
        returnValue = env.get("MYSQLCS_" + dataSourceName + "_USER_NAME");
      }
    }
    return returnValue;
  }

  private static String getPassword(final System system, final String suppliedDataSourceName) {
    String returnValue = null;
    if (system != null) {

      final Map<?, ? extends String> env = system.getenv();
      assert env != null;

      String dataSourceName = null;
      if (suppliedDataSourceName != null) {
        dataSourceName = suppliedDataSourceName.trim().toUpperCase();
        if (dataSourceName.isEmpty()) {
          dataSourceName = null;
        }
      }

      if (dataSourceName == null) {
        if (env.containsKey("DBAAS_PASSWORD")) {
          if (!env.containsKey("MYSQLCS_USER_PASSWORD")) {
            returnValue = env.get("DBAAS_PASSWORD");
          }
        } else if (env.containsKey("MYSQLCS_USER_PASSWORD")) {
          returnValue = env.get("MYSQLCS_USER_PASSWORD");
        }
      } else if (env.containsKey("DBAAS_" + dataSourceName + "_PASSWORD")) {
        if (!env.containsKey("MYSQLCS_" + dataSourceName + "_USER_PASSWORD")) {
          returnValue = env.get("DBAAS_" + dataSourceName + "_PASSWORD");
        }
      } else if (env.containsKey("MYSQLCS_ " + dataSourceName + "_USER_PASSWORD")) {
        returnValue = env.get("MYSQLCS_" + dataSourceName + "_USER_PASSWORD");
      }
    }
    return returnValue;
  }

  private static String getJdbcUrl(final System system, final String suppliedDataSourceName) {
    String returnValue = null;
    if (system != null) {

      final Map<?, ? extends String> env = system.getenv();
      assert env != null;

      String dataSourceName = null;
      if (suppliedDataSourceName != null) {
        dataSourceName = suppliedDataSourceName.trim().toUpperCase();
        if (dataSourceName.isEmpty()) {
          dataSourceName = null;
        }
      }

      if (dataSourceName == null) {
        if (env.containsKey("DBAAS_DEFAULT_CONNECT_DESCRIPTOR")) {
          if (!env.containsKey("MYSQLCS_CONNECT_STRING")) {
            returnValue = "jdbc:oracle:thin:@//" + env.get("DBAAS_DEFAULT_CONNECT_DESCRIPTOR");
          }
        } else if (env.containsKey("MYSQLCS_CONNECT_STRING")) {
          returnValue = "jdbc:mysql://" + env.get("MYSQLCS_CONNECT_STRING");
        }
      } else if (env.containsKey("DBAAS_" + dataSourceName + "_CONNECT_DESCRIPTOR")) {
        if (!env.containsKey("MYSQLCS_" + dataSourceName + "_URL")) {
          returnValue = "jdbc:oracle:thin:@//" + env.get("DBAAS_" + dataSourceName + "_CONNECT_DESCRIPTOR");
        }
      } else if (env.containsKey("MYSQLCS_ " + dataSourceName + "_CONNECT_STRING")) {
        returnValue = "jdbc:mysql://" + env.get("MYSQLCS_" + dataSourceName + "_CONNECT_STRING");
      }

    }
    return returnValue;
  }

}
