package org.apereo.cas.util.ldap.uboundid;

import com.google.common.base.Throwables;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.schema.Schema;
import com.unboundid.util.ssl.KeyStoreKeyManager;
import com.unboundid.util.ssl.SSLUtil;
import com.unboundid.util.ssl.TrustStoreTrustManager;
import org.apache.commons.io.IOUtils;
import org.apereo.cas.util.LdapTestUtils;
import org.ldaptive.LdapEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;

import javax.annotation.PreDestroy;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Properties;

/**
 * @author Misagh Moayyed
 * @since 4.1.0
 */
public class InMemoryTestLdapDirectoryServer implements Closeable {
    private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryTestLdapDirectoryServer.class);

    private InMemoryDirectoryServer directoryServer;

    private Collection<LdapEntry> ldapEntries;

    /**
     * Instantiates a new Ldap directory server.
     * Parameters need to be streams so they can be read from JARs.
     */
    public InMemoryTestLdapDirectoryServer(final InputStream properties,
                                           final InputStream ldifFile,
                                           final InputStream schemaFile) {
        try {

            LOGGER.debug("Loading properties...");
            final Properties p = new Properties();
            p.load(properties);

            final InMemoryDirectoryServerConfig config =
                    new InMemoryDirectoryServerConfig(p.getProperty("ldap.rootDn"));
            config.addAdditionalBindCredentials(p.getProperty("ldap.managerDn"), p.getProperty("ldap.managerPassword"));

            LOGGER.debug("Loading keystore file...");
            final File keystoreFile = File.createTempFile("key", "store");
            try (OutputStream outputStream = new FileOutputStream(keystoreFile)) {
                IOUtils.copy(new ClassPathResource("/ldapServerTrustStore").getInputStream(), outputStream);
            }

            final String serverKeyStorePath = keystoreFile.getCanonicalPath();
            final SSLUtil serverSSLUtil = new SSLUtil(
                    new KeyStoreKeyManager(serverKeyStorePath, "changeit".toCharArray()),
                    new TrustStoreTrustManager(serverKeyStorePath));
            final SSLUtil clientSSLUtil = new SSLUtil(new TrustStoreTrustManager(serverKeyStorePath));

            LOGGER.debug("Loading LDAP listeners and ports...");
            config.setListenerConfigs(
                    InMemoryListenerConfig.createLDAPConfig("LDAP", // Listener name
                            null, // Listen address. (null = listen on all interfaces)
                            1389, // Listen port (0 = automatically choose an available port)
                            serverSSLUtil.createSSLSocketFactory()), // StartTLS factory
                    InMemoryListenerConfig.createLDAPSConfig("LDAPS", // Listener name
                            null, // Listen address. (null = listen on all interfaces)
                            1636, // Listen port (0 = automatically choose an available port)
                            serverSSLUtil.createSSLServerSocketFactory(), // Server factory
                            clientSSLUtil.createSSLSocketFactory())); // Client factory

            config.setEnforceSingleStructuralObjectClass(false);
            config.setEnforceAttributeSyntaxCompliance(true);
            config.setMaxConnections(-1);

            LOGGER.debug("Loading LDAP schema...");
            final File file = File.createTempFile("ldap", "schema");
            try (OutputStream outputStream = new FileOutputStream(file)) {
                IOUtils.copy(schemaFile, outputStream);
            }

            LOGGER.debug("Setting LDAP schema...");
            final Schema s = Schema.mergeSchemas(Schema.getSchema(file));
            config.setSchema(s);

            this.directoryServer = new InMemoryDirectoryServer(config);
            LOGGER.debug("Populating directory...");

            LOGGER.debug("Loading LDIF file...");
            final File ldif = File.createTempFile("ldiff", "file");
            try (OutputStream outputStream = new FileOutputStream(ldif)) {
                IOUtils.copy(ldifFile, outputStream);
            }

            LOGGER.debug("Importing LDIF file...");
            this.directoryServer.importFromLDIF(true, ldif.getCanonicalPath());

            int retryCount = 5;
            while (retryCount > 0) {
                try {
                    LOGGER.debug("Trying to restart LDAP server: attempt [{}]", retryCount);
                    this.directoryServer.restartServer();
                    try (LDAPConnection c = getConnection()) {
                        LOGGER.debug("Connected to [{}]:[{}]", c.getConnectedAddress(), c.getConnectedPort());
                        populateDefaultEntries(c);
                    }
                    retryCount = 0;
                } catch (final Throwable e) {
                    Thread.sleep(2000);
                    retryCount--;
                }
            }
        } catch (final Exception e) {
            throw Throwables.propagate(e);
        }
    }

    private void populateDefaultEntries(final LDAPConnection c) throws Exception {
        populateEntries(c, new ClassPathResource("ldif/users-groups.ldif").getInputStream());
    }

    public void populateEntries(final InputStream rs) throws Exception {
        populateEntries(getConnection(), rs);
    }

    protected void populateEntries(final LDAPConnection c, final InputStream rs) throws Exception {
        this.ldapEntries = LdapTestUtils.readLdif(rs, getBaseDn());
        LdapTestUtils.createLdapEntries(c, ldapEntries);
        populateEntriesInternal(c);
    }

    protected void populateEntriesInternal(final LDAPConnection c) {
    }

    public String getBaseDn() {
        return this.directoryServer.getBaseDNs().get(0).toNormalizedString();
    }

    public Collection<LdapEntry> getLdapEntries() {
        return this.ldapEntries;
    }

    public LDAPConnection getConnection() throws LDAPException {
        return this.directoryServer.getConnection();
    }

    @Override
    @PreDestroy
    public void close() {
        LOGGER.debug("Shutting down LDAP server...");
        this.directoryServer.closeAllConnections(true);
        this.directoryServer.shutDown(true);
        LOGGER.debug("Shut down LDAP server.");
    }

    public boolean isAlive() {
        try {
            return getConnection() != null;
        } catch (final Throwable e) {
            return false;
        }
    }
}
