package com.atlassian.stash.internal.jdbc;

import com.atlassian.bitbucket.Product;
import com.atlassian.stash.internal.db.DbType;
import com.atlassian.stash.internal.home.HomeDirectoryDetails;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parses the JDBC configuration, potentially revising it when H2 or jTDS drivers are detected, and returns a
 * {@link MutableDataSourceConfiguration} with correct runtime values.
 * <p>
 * This class provides a static {@code create} method instead of implementing {@code FactoryBean} to avoid
 * {@code org.springframework.beans.factory.BeanCurrentlyInCreationException} exceptions during initialization.
 *
 * @since 4.0
 */
public class DefaultDataSourceConfigurationFactory implements DataSourceConfigurationFactory {

    public static final String H2_DRIVER = "org.h2.Driver";
    //These settings match the old values used in application-default.properties when HSQL was the default
    //internal database. When an HSQL database file is detected in the home directory, they're still used.
    public static final String HSQL_DRIVER = "org.hsqldb.jdbcDriver";
    public static final String HSQL_PASSWORD = "";
    public static final String HSQL_URL = "jdbc:hsqldb:%1$s/data/db;shutdown=true;hsqldb.tx=mvlocks";
    public static final String HSQL_USER = "SA";
    public static final String JTDS_DRIVER = "net.sourceforge.jtds.jdbc.Driver";
    public static final String JTDS_PROTOCOL = "jdbc:jtds:sqlserver";

    /**
     * Pattern matching jTDS JDBC URLs.
     * <p>
     * This pattern defines the following groups:
     * <ol>
     *     <li>Host name</li>
     *     <li>Port (Optional)</li>
     *     <li>Database name (Optional)</li>
     *     <li>Additional parameters (Optional)</li>
     * </ol>
     * URLs can specify a database name two ways:
     * <ol>
     *     <li>jdbc:jtds:sqlserver://hostname:port/database: Separated from the host/port with a forward slash</li>
     *     <li>jdbc:jtds:sqlserver://hostname:port;databaseName=database: In the additional parameter section with
     *     the parameter name "databaseName"</li>
     * </ol>
     * The {@link DbType#MSSQL SQL Server} database type always generates them the second way, but jTDS supports both.
     * As a nicety, this URL does as well.
     */
    public static final Pattern JTDS_URL = Pattern.compile(JTDS_PROTOCOL + "://([^:;/]++)(?::([\\d]{1,5}))?(?:/([^;]++))?(?:$|([^:/].*))");

    private static final Logger log = LoggerFactory.getLogger(DefaultDataSourceConfigurationFactory.class);

    private final HomeDirectoryDetails homeDirectory;

    public DefaultDataSourceConfigurationFactory(HomeDirectoryDetails homeDirectory) {
        this.homeDirectory = homeDirectory;
    }

    @Nonnull
    @Override
    public DataSourceConfiguration create(@Nonnull String driverClassName, @Nonnull String url,
                                          @Nonnull String user, @Nonnull String password) {
        if (H2_DRIVER.equals(driverClassName)) {
            /* Detects legacy HSQL databases and switches from H2 back to HSQL.
             * <p>
             * The default internal database was changed from HSQL to H2 in 4.0, but upgraded instances may still
             * be using HSQL. Rather than try to migrate those instances during startup, which would be difficult
             * to message to administrators, detect a legacy HSQL database and revert to the legacy defaults.
             * <p>
             * If an H2 database is also detected, it is preferred over the HSQL database if both exist. This is
             * intended to allow the system to be migrated from HSQL to H2 without requiring that migration to
             * either delete or rename the old HSQL database.
             */
            File dataDir = new File(homeDirectory.getSharedHome(), "data");
            if (!new File(dataDir, "db.h2.db").isFile() && //If an H2 database also exists, prefer it
                    new File(dataDir, "db.script").isFile()) { //Otherwise, if an HSQL database exists, revert
                driverClassName = HSQL_DRIVER;
                password = HSQL_PASSWORD;
                url = String.format(HSQL_URL, homeDirectory.getSharedHomeDir());
                user = HSQL_USER;
                log.warn("Detected legacy HSQL database; using HSQL instead of H2");
            }
        } else if (JTDS_DRIVER.equals(driverClassName)) {
            /* Updates the driver class name, JDBC URL, user and password values.
             * <p>
             * As of 2.1, the jTDS driver for SQL Server is no longer supported. It only supports JDBC 3, and all
             * new versions of Hibernate require a JDBC 4-compliant driver. When jTDS details are detected, they
             * are massaged over to Microsoft's own driver format.
             * <p>
             * This method may be supplied with jTDS values in one of two ways:
             * <ul>
             *     <li>Through the constructor, where the values are injected via Spring during startup from
             *     {@code bitbucket.properties} on a system configured to use jTDS values</li>
             *     <li>Via the {@link #update(DataSourceConfiguration) update} called by the restore client
             *     restoring a system configured to use the jTDS driver</li>
             * </ul>
             * There are no other code paths that can trigger this special processing.
             */
            log.debug("Replacing jTDS JDBC details with Microsoft equivalents: Driver = [{}], URL = [{}]",
                    driverClassName, url);

            driverClassName = DbType.MSSQL.getDriverClassName();
            url = rewriteJtdsUrl(url);
            log.warn("Replaced jTDS JDBC details with Microsoft equivalents: Driver = [{}], URL = [{}]",
                    driverClassName, url);
        }

        return new SimpleDataSourceConfiguration(driverClassName, url, user, password);
    }

    private static Map<String, String> parseJtdsParameters(String value) {
        if (StringUtils.isBlank(value)) {
            return Maps.newHashMap();
        }
        String[] pieces = value.split(";");

        Map<String, String> parameters = new HashMap<>(pieces.length);
        for (String piece : pieces) {
            if (StringUtils.isBlank(piece)) {
                continue;
            }
            String[] keyValue = piece.split("=");

            parameters.put(keyValue[0], keyValue[1]);
        }
        return parameters;
    }

    private static int parseJtdsPort(String value) {
        int port;
        if (value == null) {
            port = DbType.MSSQL.getDefaultPort().get();
        } else {
            port = Integer.parseInt(value);
        }
        return port;
    }

    private static String rewriteJtdsUrl(String url) {
        //Microsoft's JDBC URL is very similar to jTDS's, in how the system constructs it, but some customer
        //installations use custom URLs in order to leverage domain authentication and other features. Those
        //properties vary, sometimes widely, between Microsoft's JDBC driver and jTDS.
        Matcher matcher = JTDS_URL.matcher(url);
        if (matcher.matches()) {
            Map<String, String> parameters = parseJtdsParameters(matcher.group(4));

            String databaseName = matcher.group(3);
            if (databaseName == null) {
                databaseName = parameters.remove("databaseName");
            }
            if (databaseName != null && parameters.isEmpty()) {
                int port = parseJtdsPort(matcher.group(2));

                //If the URL only uses the databaseName (which is how the system constructs it), the URL for
                //Microsoft's JDBC driver is identical to the one for jTDS except using a different protocol.
                return DbType.MSSQL.generateUrl(matcher.group(1), databaseName, port);
            }
        }

        //If we could not extract the database name (SQL Server supports setting a default database for
        //a user, so it's not technically required--but the system can't generate a URL without one) or
        //if there are additional parameters supplied, or if the pattern doesn't match, the URL cannot
        //be automatically rewritten.
        log.error("JDBC URL {} was not generated by {} and cannot be updated automatically",
                url, Product.NAME);
        throw new UnsupportedJdbcUrlException("The configured JDBC URL is no longer supported");
    }
}
