package com.ilscipio.scipio.solr;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.transaction.Transaction;

import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.request.SolrPing;
import org.apache.solr.client.solrj.response.SolrPingResponse;
import org.ofbiz.base.component.ComponentConfig;
import org.ofbiz.base.component.ComponentConfig.WebappInfo;
import org.ofbiz.base.component.ComponentException;
import org.ofbiz.base.start.Start;
import org.ofbiz.base.start.Start.ServerState;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.catalina.container.ScipioConnectorInfo;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.transaction.GenericTransactionException;
import org.ofbiz.entity.transaction.TransactionUtil;
import org.ofbiz.entity.util.EntityQuery;
import org.ofbiz.entity.util.EntityUtilProperties;

import com.ilscipio.scipio.solr.util.ScipioHttpSolrClient;

/**
 * Generic Solr utility class, and helpers to get HttpSolrClient for the Scipio Solr instance.
 */
public abstract class SolrUtil {

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());

    public static final String solrConfigName = "solrconfig";
    public static final boolean DEBUG = UtilProperties.getPropertyAsBoolean("solr", "solr.debug", false)
            || UtilValidate.booleanValueVersatile(System.getProperty("scipio.solr.debug"), false); // SCIPIO

    private static final Boolean solrEnabledSysProp = getSolrSysPropBool("enabled", null);
    private static final boolean solrEnabledCfg = getSolrCfgBool("enabled", true);
    private static final boolean solrEnabled = (solrEnabledSysProp != null) ? solrEnabledSysProp : solrEnabledCfg;

    private static final Boolean solrEcaEnabledSysProp = getSolrSysPropBool("eca.enabled", null);
    private static final boolean solrEcaEnabledCfg = getSolrCfgBool("eca.enabled", false);

    private static final Boolean solrWebappCheckEnabledSysProp = getSolrSysPropBool("eca.useSolrWebappLoadedCheck", null);
    private static final boolean solrWebappCheckEnabledCfg = getSolrCfgBool("eca.useSolrWebappLoadedCheck", true);
    private static final boolean solrWebappCheckEnabled = (solrWebappCheckEnabledSysProp != null) ? solrWebappCheckEnabledSysProp : solrWebappCheckEnabledCfg;

    private static final String solrWebappProtocol = UtilProperties.getPropertyValue(solrConfigName, "solr.webapp.protocol");
    private static final String solrWebappHost = UtilProperties.getPropertyValue(solrConfigName, "solr.webapp.domainName");
    private static final Integer solrWebappPort = readSolrWebappPort();
    private static final String solrWebappPath = UtilProperties.getPropertyValue(solrConfigName, "solr.webapp.path", "/solr");
    private static final String solrWebappServer = UtilProperties.getPropertyValue(solrConfigName, "solr.webapp.server", "default-server");
    private static final String solrDefaultCore = UtilProperties.getPropertyValue(solrConfigName, "solr.core.default");

    private static final String effectiveConfigVersion = determineSolrConfigVersion(
            UtilProperties.getPropertyValue(solrConfigName, "solr.config.version"),
            UtilProperties.getPropertyValue(solrConfigName, "solr.config.version.custom"),
            UtilProperties.getPropertyValue(solrConfigName, "solr.config.version.extra"));

    /**
     * @deprecated use {@link #getSolrWebappUrl} instead.
     */
    @Deprecated
    public static final String solrUrl = makeSolrWebappUrl();
    /**
     * @deprecated use {@link #getSolrDefaultCoreUrl} instead.
     */
    @Deprecated
    public static final String solrFullUrl = makeSolrDefaultCoreUrl();

    public static String getSolrConfigName() {
        return solrConfigName;
    }

    /**
     * Returns the EFFECTIVE Solr config version, which is a combination
     * of the solrconfig.properties values
     * <code>solr.config.version</code>,
     * <code>solr.config.version.custom</code> and
     * <code>solr.config.version.extra</code>.
     */
    public static String getSolrConfigVersionStatic() {
        return effectiveConfigVersion;
    }

    public static String determineSolrConfigVersion(String baseVersion, String customVersion, String extraVersion) {
        String version = baseVersion;
        if (UtilValidate.isNotEmpty(customVersion)) {
            version = customVersion;
        }
        if (UtilValidate.isNotEmpty(extraVersion)) {
            if (extraVersion.startsWith(".")) version += extraVersion;
            else version += "." + extraVersion;
        }
        return version;
    }

    public static String getSolrDefaultCore() {
        return solrDefaultCore;
    }

    public static String getSolrWebappProtocol() {
        return solrWebappProtocol;
    }

    public static String getSolrWebappHost() {
        return solrWebappHost;
    }

    public static int getSolrWebappPort() {
        return solrWebappPort;
    }

    private static int readSolrWebappPort() {
        Integer solrPort = UtilProperties.getPropertyAsInteger(solrConfigName, "solr.webapp.portOverride", null);
        if (solrPort != null) {
            return solrPort;
        }
        solrPort = LocalUrlUtil.getWebappContainerPort(solrWebappProtocol);
        if (solrPort != null) {
            return solrPort;
        }
        solrPort = LocalUrlUtil.getStandardPort(solrWebappProtocol);
        Debug.logWarning("Solr: Could not determine a solr webapp port from " + solrConfigName
                + " or webapp container; using default: " + solrPort, module);
        return solrPort;
    }

    public static boolean isSolrWebappLocal() {
        if (!LocalUrlUtil.isLocalhost(getSolrWebappHost())) {
            return false;
        }
        return LocalUrlUtil.isWebappContainerPort(getSolrWebappPort());
    }

    public static String getSolrWebappPath() {
        return solrWebappPath;
    }

    public static String getSolrWebappUrl() {
        return solrUrl;
    }

    private static String makeSolrWebappUrl() {
        StringBuilder sb = new StringBuilder();
        sb.append(getSolrWebappProtocol());
        sb.append("://");
        sb.append(getSolrWebappHost());
        if (!LocalUrlUtil.isStandardPort(getSolrWebappPort(), getSolrWebappProtocol())) {
            sb.append(":");
            sb.append(getSolrWebappPort());
        }
        sb.append(getSolrWebappPath());
        if (sb.charAt(sb.length() - 1) == '/') {
            sb.setLength(sb.length() - 1);
        }
        Debug.logInfo("Solr: Determined Solr webapp URL: " + sb.toString(), module);
        return sb.toString();
    }

    public static String getSolrCoreUrl(String core) {
        return getSolrWebappUrl() + "/" + core;
    }

    public static String getSolrDefaultCoreUrl() {
        return solrFullUrl;
    }

    private static String makeSolrDefaultCoreUrl() {
        return getSolrCoreUrl(solrDefaultCore);
    }

    /**
     * @deprecated bad name; use {@link #getSolrDefaultCoreUrl()} instead.
     */
    @Deprecated
    public static String makeFullSolrWebappUrl() {
        return makeSolrDefaultCoreUrl();
    }

    public static WebappInfo getSolrWebappInfo() {
        WebappInfo solrApp = null;
        try {
            ComponentConfig cc = ComponentConfig.getComponentConfig("solr");
            for(WebappInfo currApp : cc.getWebappInfos()) {
                if ("solr".equals(currApp.getName())) {
                    solrApp = currApp;
                    break;
                }
            }
        } catch(ComponentException e) {
            throw new IllegalStateException(e);
        }
        return solrApp;
    }

    private static Boolean getSolrSysPropBool(String name, Boolean defaultValue) {
        Boolean value = UtilMisc.booleanValueVersatile(System.getProperty("scipio.solr."+name));
        if (value == null) {
            value = UtilMisc.booleanValueVersatile(System.getProperty("ofbiz.solr."+name));
        }
        return (value != null) ? value : defaultValue;
    }

    private static Boolean getSolrCfgBool(String name, Boolean defaultValue) {
        return UtilProperties.getPropertyAsBoolean(SolrUtil.solrConfigName, "solr."+name, defaultValue);
    }

    public static boolean isSolrEnabled() {
        return solrEnabled;
    }

    public static boolean isSolrEcaEnabled(Delegator delegator) {
        if (solrEcaEnabledSysProp != null) {
            return solrEcaEnabledSysProp;
        }
        Boolean ecaEnabled = getSolrEcaEnabledEntityProperty(delegator);
        return (ecaEnabled != null) ? ecaEnabled : solrEcaEnabledCfg;
    }

    public static Boolean getSolrEcaEnabledEntityProperty(Delegator delegator) {
        // Need without the fallback (special case)
        //return EntityUtilProperties.getPropertyAsBoolean(solrConfigName, "solr.eca.enabled", null, delegator);
        return UtilProperties.asBoolean(EntityUtilProperties.getEntityPropertyValueOrNull(solrConfigName, "solr.eca.enabled", delegator));
    }

    /**
     * @deprecated Use {@link #getSolrEcaEnabledEntityProperty}.
     */
    @Deprecated
    public static String getSolrEcaEnabledSystemProperty(Delegator delegator) {
        return EntityUtilProperties.getEntityPropertyValueOrNull(solrConfigName, "solr.eca.enabled", delegator);
    }

    /**
     * Checks if solr is enabled and scipio is initialized for solr and the system/server
     * is in a running state in which queries may be attempted against the solr webapp.
     * <p>
     * NOTE: true does NOT mean that solr webapp is fully loaded and pingable,
     * because the webapp may be remote and such a check must be done physically using
     * waitSolrReady service, {@link #isSolrWebappPingOk(HttpSolrClient)} or {@link #isSolrWebappReady(HttpSolrClient)}.
     */
    public static boolean isSystemInitialized() {
        return isSolrEnabled() && (Start.getInstance().getCurrentState() == ServerState.RUNNING);
    }

    /**
     * Same as {@link #isSystemInitialized()} but assumes solr is enabled.
     */
    public static boolean isSystemInitializedAssumeEnabled() {
        return (Start.getInstance().getCurrentState() == ServerState.RUNNING);
    }

    /**
     * Returns true if the LOCAL Solr webapp has reached initialization.
     */
    public static boolean isSolrLocalWebappStarted() {
        return ScipioSolrInfoServlet.isServletInitStatusReached();
    }

    /**
     * Returns true if the LOCAL Solr webapp is initialized.
     * @deprecated 2018-05-25: ambiguous; use {@link #isSystemInitialized()} or {@link #isSolrLocalWebappStarted()}.
     */
    @Deprecated
    public static boolean isSolrWebappInitialized() {
        return isSolrLocalWebappStarted();
    }

    /**
     * If the solr webapp/initialization check if enabled in config,
     * returns {@link #isSystemInitialized()}, otherwise returns true.
     */
    public static boolean isSolrEcaWebappInitCheckPassed() {
        return (solrWebappCheckEnabled) ? isSystemInitialized() : true;
    }

    public static boolean isEcaTreatConnectErrorNonFatal() {
        Boolean treatConnectErrorNonFatal = UtilProperties.getPropertyAsBoolean(solrConfigName, "solr.eca.treatConnectErrorNonFatal", true);
        return Boolean.TRUE.equals(treatConnectErrorNonFatal);
    }

    /**
     * Returns true if the LOCAL Solr webapp is present in the system (by context root),
     * as defined in applications/solr/scipio-component.xml.
     * <p>
     * For stock Scipio configuration, this always returns true.
     * It only returns false if the webapp is disabled/commented in scipio-component.xml.
     * <p>
     * WARN: Do not call this before or while components are being read.
     */
    public static boolean isSolrLocalWebappPresent() {
        return (ComponentConfig.getWebAppInfo(solrWebappServer, solrWebappPath) != null);
    }

    /**
     * Returns true if the LOCAL Solr webapp is present in the system (by context root).
     * @deprecated 2018-05-25: ambiguous; use {@link #isSystemInitialized()} or {@link #isSolrLocalWebappPresent()}.
     */
    @Deprecated
    public static boolean isSolrWebappPresent() {
        return isSolrLocalWebappPresent();
    }

    /**
     * Returns true if the LOCAL Solr webapp is present in the system (by context root) and running state.
     * @deprecated 2018-05-25: ambiguous; use {@link #isSystemInitialized()} or {@link #isSolrLocalWebappPresent()}.
     */
    @Deprecated
    public static boolean isSolrWebappEnabled() {
        return isSolrEnabled() && isSolrLocalWebappPresent();
    }

    public static boolean isSolrWebappPingOk(HttpSolrClient client) throws Exception {
        try {
            return isSolrWebappPingOkRaw(client);
        } catch(Exception e) {
            // FIXME: we are not supposed to catch this, but in current setup with Tomcat
            //  solr can't handle the incomplete loading 503 and throws exception, so have no choice
            //  but because this is only a Ping, we can usually assume this means not yet loaded...
            // NOTE: This case also occurs currently if connect/socket timeout
            Debug.logInfo("Solr: isSolrWebappPingOk: Solr webapp not pingable. Cause: " + e.toString(), module);
            return false;
        }
    }

    public static boolean isSolrWebappPingOkRaw(HttpSolrClient client) throws Exception {
        SolrPing solrPing = new SolrPing();
        SolrPingResponse rsp = solrPing.process(client);
        int status = rsp.getStatus();
        if (status == 0) {
            if (Debug.verboseOn()) Debug.logVerbose("isSolrWebappPingOk: ping response status: " + status, module);
            return true;
        } else {
            Debug.logInfo("Solr: isSolrWebappPingOk: Solr webapp not pingable; status: " + status, module);
            return false;
        }
    }

    /**
     * Returns true if Solr is loaded and available for queries.
     */
    public static boolean isSolrWebappReady(HttpSolrClient client) throws Exception {
        return isSystemInitialized() && isSolrWebappPingOk(client);
    }

    /**
     * Returns true if Solr is loaded and available for queries, using default core and client.
     */
    public static boolean isSolrWebappReady() throws Exception {
        return isSolrWebappReady(getQueryHttpSolrClient(null));
    }

    public static boolean isSolrWebappReadyRaw(HttpSolrClient client) throws Exception {
        return isSystemInitialized() && isSolrWebappPingOkRaw(client);
    }

    public static boolean isSolrWebappReadyRaw() throws Exception {
        return isSolrWebappReadyRaw(getQueryHttpSolrClient(null));
    }

    /**
     * Returns the SolrStatus data status from DB. NOTE: useCache=true should usually not be used.
     */
    public static GenericValue getSolrStatus(Delegator delegator, boolean useCache) {
        GenericValue solrStatus;
        try {
            solrStatus = EntityQuery.use(delegator).from("SolrStatus")
                    .where("solrId", "SOLR-MAIN").cache(useCache).queryOne();
            if (solrStatus == null) {
                Debug.logWarning("Could not get SolrStatus for SOLR-MAIN - seed data missing?", module);
            } else {
                return solrStatus;
            }
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        return null;
    }

    public static GenericValue getSolrStatus(Delegator delegator) {
        return getSolrStatus(delegator, false);
    }

    /**
     * Returns the SolrStatus data status ID from DB. NOTE: useCache=true should usually not be used.
     */
    public static String getSolrDataStatusId(Delegator delegator, boolean useCache) {
        GenericValue solrStatus = getSolrStatus(delegator, useCache);
        return solrStatus != null ? solrStatus.getString("dataStatusId") : null;
    }

    public static String getSolrDataStatusId(Delegator delegator) {
        return getSolrDataStatusId(delegator, false);
    }

    public static void setSolrDataStatusId(Delegator delegator, String dataStatusId, boolean updateVersion) throws GenericEntityException {
        GenericValue solrStatus = EntityQuery.use(delegator).from("SolrStatus")
                .where("solrId", "SOLR-MAIN").cache(false).queryOne();
        //solrStatus = delegator.findOne("SolrStatus", UtilMisc.toMap("solrId", "SOLR-MAIN"), false);
        if (solrStatus == null) {
            Debug.logWarning("Could not get SolrStatus for SOLR-MAIN - creating new", module);
            solrStatus = delegator.create("SolrStatus",
                    "solrId", "SOLR-MAIN",
                    "dataStatusId", dataStatusId,
                    "dataCfgVersion", getSolrConfigVersionStatic());
        } else {
            solrStatus.setString("dataStatusId", dataStatusId);
            if (updateVersion) {
                solrStatus.setString("dataCfgVersion", getSolrConfigVersionStatic());
            }
            solrStatus.store();
        }
    }

    public static void setSolrDataStatusIdSafe(Delegator delegator, String dataStatusId, boolean updateVersion) {
        try {
            setSolrDataStatusId(delegator, dataStatusId, updateVersion);
        } catch (Exception t) {
            final String errMsg = "Error while trying to mark Solr data status (" + dataStatusId + "): " + t.getMessage();
            Debug.logError(t, "Solr: " + errMsg, module);
        }
    }

    public static void setSolrDataStatusId(Delegator delegator, String dataStatusId) throws GenericEntityException {
        setSolrDataStatusId(delegator, dataStatusId, false);
    }

    // DEV NOTE: could have done this with service call, but just in case need fine-grained control...
    public static boolean setSolrDataStatusIdSepTxSafe(Delegator delegator, String dataStatusId, boolean updateVersion) {
        Transaction parentTransaction = null;
        boolean beganTrans = false;
        try {
            try {
                if (TransactionUtil.isTransactionInPlace()) {
                    parentTransaction = TransactionUtil.suspend();
                    // now start a new transaction
                    beganTrans = TransactionUtil.begin();
                } else {
                    beganTrans = TransactionUtil.begin();
                }
            } catch (GenericTransactionException t) {
                Debug.logError(t, "Solr: Cannot create transaction to mark Solr data status (" + dataStatusId + ")", module);
            }
            try {
                SolrUtil.setSolrDataStatusId(delegator, dataStatusId, updateVersion);
                return true;
            } catch (Throwable t) {
                final String errMsg = "Error while trying to mark Solr data status (" + dataStatusId + "): " + t.getMessage();
                Debug.logError(t, "Solr: " + errMsg, module);
                try {
                    TransactionUtil.rollback(beganTrans, errMsg, t);
                } catch (GenericTransactionException te) {
                    Debug.logError(te, "Solr: Cannot rollback transaction to mark Solr data status (" + dataStatusId + ")", module);
                }
            } finally {
                try {
                    TransactionUtil.commit(beganTrans);
                } catch (GenericTransactionException e) {
                    Debug.logError(e, "Solr: Could not commit transaction to mark Solr data status (" + dataStatusId + ")", module);
                }
            }
        } finally {
            if (parentTransaction != null) {
                try {
                    TransactionUtil.resume(parentTransaction);
                } catch (GenericTransactionException t) {
                    Debug.logError(t, "Solr: Error resuming parent transaction after marking Solr data status (" + dataStatusId + ")", module);
                }
            }
        }
        return false;
    }

    /**
     * Returns a Solr client for making read-only queries, for given core or default core (if null).
     * <p>
     * Supports explicit solr basic auth username/password, if not empty; otherwise
     * includes the basic auth defined in solrconfig.properties/solr.query.login.*,
     * which can be further overridden on individual requests (using QueryRequest).
     */
    public static HttpSolrClient getQueryHttpSolrClient(String core, String solrUsername, String solrPassword) {
        return SolrClientFactory.queryClientFactory.getClientForCore(core, solrUsername, solrPassword);
    }

    /**
     * Returns a Solr client for making read-only queries, for given core or default core (if null).
     * <p>
     * This client includes the basic auth defined in solrconfig.properties/solr.query.login.*,
     * which can be overridden on individual requests (using QueryRequest).
     */
    public static HttpSolrClient getQueryHttpSolrClient(String core) {
        return SolrClientFactory.queryClientFactory.getClientForCore(core, null, null);
    }

    public static HttpSolrClient getQueryHttpSolrClientFromUrl(String url, String solrUsername, String solrPassword) {
        return SolrClientFactory.queryClientFactory.getClientFromUrl(url, solrUsername, solrPassword);
    }

    public static HttpSolrClient getQueryHttpSolrClientFromUrl(String url) {
        return SolrClientFactory.queryClientFactory.getClientFromUrl(url, null, null);
    }

    /**
     * Returns a Solr client for making update/indexing queries, for given core or default core (if null).
     * <p>
     * Supports explicit solr basic auth username/password, if not empty; otherwise
     * includes the basic auth defined in solrconfig.properties/solr.update.login.*,
     * which can be further overridden on individual requests (using QueryRequest).
     */
    public static HttpSolrClient getUpdateHttpSolrClient(String core, String solrUsername, String solrPassword) {
        return SolrClientFactory.updateClientFactory.getClientForCore(core, solrUsername, solrPassword);
    }

    /**
     * Returns a Solr client for making update/indexing queries, for given core or default core (if null).
     * <p>
     * This client includes the basic auth defined in solrconfig.properties/solr.update.login.*,
     * which can be overridden on individual requests (using UpdateRequest).
     */
    public static HttpSolrClient getUpdateHttpSolrClient(String core) {
        return SolrClientFactory.updateClientFactory.getClientForCore(core, null, null);
    }

    public static HttpSolrClient getUpdateHttpSolrClientFromUrl(String url, String solrUsername, String solrPassword) {
        return SolrClientFactory.updateClientFactory.getClientFromUrl(url, solrUsername, solrPassword);
    }

    public static HttpSolrClient getUpdateHttpSolrClientFromUrl(String url) {
        return SolrClientFactory.updateClientFactory.getClientFromUrl(url, null, null);
    }

    /**
     * Returns a Solr client for making admin queries, for given core or default core (if null).
     * <p>
     * Supports explicit solr basic auth username/password, if not empty; otherwise
     * includes the basic auth defined in solrconfig.properties/solr.admin.login.*,
     * which can be further overridden on individual requests.
     */
    public static HttpSolrClient getAdminHttpSolrClient(String core, String solrUsername, String solrPassword) {
        return SolrClientFactory.adminClientFactory.getClientForCore(core, solrUsername, solrPassword);
    }

    /**
     * Returns a Solr client for making admin queries, for given core or default core (if null).
     * <p>
     * This client includes the basic auth defined in solrconfig.properties/solr.admin.login.*,
     * which can be overridden on individual requests.
     */
    public static HttpSolrClient getAdminHttpSolrClient(String core) {
        return SolrClientFactory.adminClientFactory.getClientForCore(core, null, null);
    }

    public static HttpSolrClient getAdminHttpSolrClientFromUrl(String url, String solrUsername, String solrPassword) {
        return SolrClientFactory.adminClientFactory.getClientFromUrl(url, solrUsername, solrPassword);
    }

    public static HttpSolrClient getAdminHttpSolrClientFromUrl(String url) {
        return SolrClientFactory.adminClientFactory.getClientFromUrl(url, null, null);
    }

    /**
     * Returns a Solr client for making read-only queries.
     * @deprecated 2018-04-17: now ambiguous; use {@link #getQueryHttpSolrClient(String) instead
     */
    @Deprecated
    public static HttpSolrClient getHttpSolrClient(String core) {
        return getQueryHttpSolrClient(core);
    }

    /**
     * Returns a Solr client for making read-only queries.
     * @deprecated 2018-04-17: now ambiguous; use {@link #getQueryHttpSolrClient(String) instead
     */
    @Deprecated
    public static HttpSolrClient getHttpSolrClient() {
        return getQueryHttpSolrClient(null);
    }

    /**
     * Returns a new Solr client for making read-only queries.
     * @deprecated 2018-04-27: now ambiguous; use {@link #makeQueryHttpSolrClientFromUrl} or other instead
     */
    @Deprecated
    public static HttpSolrClient makeHttpSolrClientFromUrl(String url) {
        // use query username/password for backward compat, but this is not guaranteed to work
        return makeQueryHttpSolrClientFromUrl(url, getSolrQueryConnectConfig().getSolrUsername(), getSolrQueryConnectConfig().getSolrPassword());
    }

    public static HttpSolrClient makeQueryHttpSolrClientFromUrl(String url, String solrUsername, String solrPassword) {
        return SolrClientFactory.NewSolrClientFactory.newQueryClientFactory.getClientFromUrl(url, solrUsername, solrPassword);
    }

    public static HttpSolrClient makeUpdateHttpSolrClientFromUrl(String url, String solrUsername, String solrPassword) {
        return SolrClientFactory.NewSolrClientFactory.newUpdateClientFactory.getClientFromUrl(url, solrUsername, solrPassword);
    }

    public static HttpSolrClient makeAdminHttpSolrClientFromUrl(String url, String solrUsername, String solrPassword) {
        return SolrClientFactory.NewSolrClientFactory.newAdminClientFactory.getClientFromUrl(url, solrUsername, solrPassword);
    }

    static SolrConnectConfig getSolrQueryConnectConfig() {
        return SolrConnectConfig.queryConnectConfig;
    }

    static SolrConnectConfig getSolrUpdateConnectConfig() {
        return SolrConnectConfig.updateConnectConfig;
    }

    static SolrConnectConfig getSolrAdminConnectConfig() {
        return SolrConnectConfig.adminConnectConfig;
    }

    public enum SolrClientMode {
        QUERY("query"),
        UPDATE("update"),
        ADMIN("admin");

        private final String name;

        private SolrClientMode(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

    public static class SolrConnectConfig {
        private static final SolrConnectConfig queryConnectConfig = SolrConnectConfig.fromProperties(SolrClientMode.QUERY, solrConfigName, "solr.query.connect.");
        private static final SolrConnectConfig updateConnectConfig = SolrConnectConfig.fromProperties(SolrClientMode.UPDATE, solrConfigName, "solr.update.connect.");
        private static final SolrConnectConfig adminConnectConfig = SolrConnectConfig.fromProperties(SolrClientMode.ADMIN, solrConfigName, "solr.admin.connect.");

        private final SolrClientMode clientMode;
        private final String solrUsername;
        private final String solrPassword;
        private final boolean reuseClient;
        private final Integer connectTimeout;
        private final Integer socketTimeout;
        //private final Boolean noDelay; // TODO: FLAG NOT IMPLEMENTED (client building issue)
        private final Integer maxConnections;
        private final Integer maxConnectionsPerHost;

        public SolrConnectConfig(SolrClientMode clientMode, Map<String, ?> configMap) {
            this.clientMode = clientMode;
            this.solrUsername = UtilProperties.valueOrNull((String) configMap.get("login.username"));
            this.solrPassword = UtilProperties.valueOrNull((String) configMap.get("login.password"));
            this.reuseClient = UtilProperties.asBoolean(configMap.get("reuseClient"), false);
            this.connectTimeout = UtilProperties.asInteger(configMap.get("connectTimeout"));
            this.socketTimeout = UtilProperties.asInteger(configMap.get("socketTimeout"));
            //this.noDelay = UtilProperties.asBoolean(configMap.get("noDelay"));
            // NOTE: Defaults are based on Solr 7 (not 6) - see org.apache.solr.client.solrj.impl.HttpClientUtil source
            // For Solr 6, the defaults were 128 and 32, respectively
            this.maxConnections = UtilProperties.asInteger(configMap.get("maxConnections"), 10000);
            this.maxConnectionsPerHost = UtilProperties.asInteger(configMap.get("maxConnectionsPerHost"), 10000);
        }

        public static SolrConnectConfig fromProperties(SolrClientMode clientMode, String resource, String prefix) {
            return new SolrConnectConfig(clientMode,
                    UtilProperties.getPropertiesWithPrefix(UtilProperties.getProperties(resource), prefix));
        }

        public SolrClientMode getClientMode() { return clientMode; }
        public String getSolrUsername() { return solrUsername; }
        public String getSolrPassword() { return solrPassword; }
        public boolean isReuseClient() { return reuseClient; }
        public String getSolrCore() { return SolrUtil.getSolrDefaultCore(); }
        public String getSolrCoreUrl() { return SolrUtil.getSolrDefaultCoreUrl(); }
        public Integer getConnectTimeout() { return connectTimeout; }
        public Integer getSocketTimeout() { return socketTimeout; }
        //public Boolean getNoDelay() { return noDelay; }
        public Integer getMaxConnections() { return maxConnections; }
        public Integer getMaxConnectionsPerHost() { return maxConnectionsPerHost; }

        /**
         * Sets basic Solr auth on the given request.
         * <p>
         * NOTE: 2018-04-17: This should largely not be needed now; it should be
         * already handled in ScipioHttpSolrClient returned by {@link #getQueryHttpSolrClient},
         * although doing it in double here on the SolrRequest will not cause any problems.
         */
        public void setSolrRequestAuth(SolrRequest<?> req, String solrUsername, String solrPassword) {
            if (solrUsername == null) {
                solrUsername = this.getSolrUsername();
                solrPassword = this.getSolrPassword();
            }
            if (solrUsername != null) {
                req.setBasicAuthCredentials(solrUsername, solrPassword);
            }
        }

        public void setSolrRequestAuth(SolrRequest<?> req) {
            String solrUsername = this.getSolrUsername();
            if (solrUsername != null) {
                String solrPassword = this.getSolrPassword();
                req.setBasicAuthCredentials(solrUsername, solrPassword);
            }
        }

        private String makeClientLogDesc(String url, String solrUsername) {
            return getClientMode().getName()
                    + " client: " + (solrUsername != null ? solrUsername + "@" : "") + url
                    + (connectTimeout != null ? " (timeout: " + connectTimeout + ")" : "")
                    + (socketTimeout != null ? " (sotimeout: " + socketTimeout + ")" : "")
                    + (maxConnections != null ? " (maxconn: " + maxConnections + ")" : "")
                    + (maxConnectionsPerHost != null ? " (hostmaxconn: " + maxConnectionsPerHost + ")" : "");
        }
    }

    private static abstract class SolrClientFactory {

        /**
         * Abstracted factory that gets a cached or new client.
         */
        static final SolrClientFactory queryClientFactory = create(SolrConnectConfig.queryConnectConfig);
        static final SolrClientFactory updateClientFactory = create(SolrConnectConfig.updateConnectConfig);
        static final SolrClientFactory adminClientFactory = create(SolrConnectConfig.adminConnectConfig);

        public static SolrClientFactory create(SolrConnectConfig connectConfig) {
            if (connectConfig.isReuseClient()) return CachedSolrClientFactory.create(connectConfig);
            else return NewSolrClientFactory.create(connectConfig);
        }

        public abstract HttpSolrClient getClientFromUrlRaw(String url, String solrUsername, String solrPassword);

        public HttpSolrClient getClientFromUrl(String url, String solrUsername, String solrPassword) {
            if (UtilValidate.isEmpty(solrUsername)) {
                solrUsername = getConnectConfig().getSolrUsername();
                solrPassword = getConnectConfig().getSolrPassword();
            } else if ("_none_".equals(solrUsername)) {
                solrUsername = null;
                solrPassword = null;
            }
            return getClientFromUrlRaw(url, solrUsername, solrPassword);
        }

        public HttpSolrClient getClientForCore(String core, String solrUsername, String solrPassword) {
            if (UtilValidate.isEmpty(solrUsername)) {
                solrUsername = getConnectConfig().getSolrUsername();
                solrPassword = getConnectConfig().getSolrPassword();
            } else if ("_none_".equals(solrUsername)) {
                solrUsername = null;
                solrPassword = null;
            }
            if (UtilValidate.isNotEmpty(core)) {
                return getClientFromUrlRaw(getSolrCoreUrl(core), solrUsername, solrPassword);
            } else {
                return getClientFromUrlRaw(getSolrDefaultCoreUrl(), solrUsername, solrPassword);
            }
        }

        public abstract SolrConnectConfig getConnectConfig();

        static class NewSolrClientFactory extends SolrClientFactory {
            private final SolrConnectConfig connectConfig;

            /**
             * Factory that always creates a new client.
             */
            static final SolrClientFactory newQueryClientFactory = NewSolrClientFactory.create(SolrConnectConfig.queryConnectConfig);
            static final SolrClientFactory newUpdateClientFactory = NewSolrClientFactory.create(SolrConnectConfig.updateConnectConfig);
            static final SolrClientFactory newAdminClientFactory = NewSolrClientFactory.create(SolrConnectConfig.adminConnectConfig);

            NewSolrClientFactory(SolrConnectConfig connectConfig) {
                this.connectConfig = connectConfig;
            }

            public static NewSolrClientFactory create(SolrConnectConfig connectConfig) {
                return new NewSolrClientFactory(connectConfig);
            }

            @Override
            public HttpSolrClient getClientFromUrlRaw(String url, String solrUsername, String solrPassword) {
                return makeClient(connectConfig, url, solrUsername, solrPassword);
            }

            public static HttpSolrClient makeClient(SolrConnectConfig connectConfig, String url, String solrUsername, String solrPassword) {
                if (Debug.verboseOn()) Debug.logVerbose("Solr: Creating new solr " + connectConfig.makeClientLogDesc(url,  solrUsername), module);
                return ScipioHttpSolrClient.create(url, null, solrUsername, solrPassword,
                            connectConfig.getMaxConnections(), connectConfig.getMaxConnectionsPerHost(),
                            connectConfig.getConnectTimeout(), connectConfig.getSocketTimeout());
            }

            @Override
            public SolrConnectConfig getConnectConfig() {
                return connectConfig;
            }
        }

        /**
         * Special client cache, optimized using read-only map for thread safety and default clients.
         */
        static class CachedSolrClientFactory extends SolrClientFactory {
            private final SolrConnectConfig connectConfig;
            private final String defaultClientCacheKey;
            private final HttpSolrClient defaultClient;
            private Map<String, HttpSolrClient> clientCache;

            CachedSolrClientFactory(SolrConnectConfig connectConfig, String defaultClientCacheKey, HttpSolrClient defaultClient) {
                this.connectConfig = connectConfig;
                this.defaultClientCacheKey = defaultClientCacheKey;
                this.defaultClient = defaultClient;
                Map<String, HttpSolrClient> clientCache = new HashMap<>();
                clientCache.put(defaultClientCacheKey, defaultClient);
                this.clientCache = Collections.unmodifiableMap(clientCache);
            }

            public static CachedSolrClientFactory create(SolrConnectConfig connectConfig) {
                String defaultClientCacheKey = (connectConfig.getSolrUsername() != null) ?
                        (connectConfig.getSolrCoreUrl() + ":" + connectConfig.getSolrUsername() + ":" + connectConfig.getSolrPassword())
                        : connectConfig.getSolrCoreUrl();
                HttpSolrClient defaultClient = NewSolrClientFactory.makeClient(connectConfig, connectConfig.getSolrCoreUrl(),
                        connectConfig.getSolrUsername(), connectConfig.getSolrPassword());
                return new CachedSolrClientFactory(connectConfig, defaultClientCacheKey, defaultClient);
            }

            @Override
            public HttpSolrClient getClientFromUrlRaw(String url, String solrUsername, String solrPassword) {
                final String cacheKey = (solrUsername != null) ? (url + ":" + solrUsername + ":" + solrPassword) : url;
                if (defaultClientCacheKey.equals(cacheKey)) {
                    if (Debug.verboseOn()) Debug.logVerbose("Solr: Using default solr " + connectConfig.makeClientLogDesc(url,  solrUsername), module);
                    return defaultClient;
                }

                HttpSolrClient client = clientCache.get(cacheKey);
                if (client == null) {
                    synchronized(this) {
                        client = clientCache.get(cacheKey);
                        if (client == null) {
                            client = NewSolrClientFactory.makeClient(connectConfig, url, solrUsername, solrPassword);
                            // use immutable map pattern for performance (the map only ever contains a few entries)
                            Map<String, HttpSolrClient> newCache = new HashMap<>(clientCache);
                            newCache.put(cacheKey, client);
                            clientCache = Collections.unmodifiableMap(newCache);
                        } else {
                            if (Debug.verboseOn()) Debug.logVerbose("Solr: Using cached solr " + connectConfig.makeClientLogDesc(url,  solrUsername), module);
                        }
                    }
                } else {
                    if (Debug.verboseOn()) Debug.logVerbose("Solr: Using cached solr " + connectConfig.makeClientLogDesc(url,  solrUsername), module);
                }
                return client;
            }

            @Override
            public SolrConnectConfig getConnectConfig() {
                return connectConfig;
            }
        }
    }

    static class LocalUrlUtil {
        public static boolean isLocalhost(String host) {
            return "localhost".equals(host) || "127.0.0.1".equals(host);
        }

        public static int getStandardPort(String protocol) {
            return ("https".equals(protocol)) ? 443 : 80;
        }

        public static boolean isStandardPort(int port, String protocol) {
            return ("https".equals(protocol) && port == 443) || ("http".equals(protocol) && port == 80);
        }

        public static Integer getWebappContainerPort(String protocol) {
            ScipioConnectorInfo connectorInfo = ScipioConnectorInfo.getWebContainer(protocol);
            return (connectorInfo != null) ? connectorInfo.getPort() : null;
        }

        public static boolean isWebappContainerPort(int port) {
            ScipioConnectorInfo httpConnectorInfo = ScipioConnectorInfo.getWebContainer(false);
            if (httpConnectorInfo != null && port == httpConnectorInfo.getPort()) {
                return true;
            }
            ScipioConnectorInfo httpsConnectorInfo = ScipioConnectorInfo.getWebContainer(true);
            if (httpConnectorInfo != null && port == httpsConnectorInfo.getPort()) {
                return true;
            }
            return false;
        }

        public static Integer getFrontendServerPort(String protocol) {
            Integer port = UtilProperties.getPropertyAsInteger("url", "https".equals(protocol) ? "port.https" : "port.http", null);
            return port;
        }

        public static boolean isFrontendServerPort(int port) {
            if (port == UtilProperties.getPropertyAsInteger("url", "port.https", -1)) {
                return true;
            }
            if (port == UtilProperties.getPropertyAsInteger("url", "port.http", -1)) {
                return true;
            }
            return false;
        }
    }

    /**
     * Returns true if either solr.debug is true or verbose logging is on.
     */
    public static boolean verboseOn() {
        return (SolrUtil.DEBUG || Debug.verboseOn());
    }
}
