package net.detailedbalance.ladle;

import org.apache.commons.io.FileUtils;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.model.schema.registries.SchemaLoader;
import org.apache.directory.api.ldap.model.ldif.LdifReader;
import org.apache.directory.api.ldap.model.ldif.LdifEntry;
import org.apache.directory.api.ldap.schemaextractor.SchemaLdifExtractor;
import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor;
import org.apache.directory.api.ldap.schemaloader.LdifSchemaLoader;
import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager;
import org.apache.directory.api.util.exception.Exceptions;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.DefaultDirectoryService;
import org.apache.directory.server.core.api.CacheService;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.DnFactory;
import org.apache.directory.server.core.api.InstanceLayout;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.api.schema.SchemaPartition;
import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex;
import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition;
import org.apache.directory.server.core.partition.ldif.LdifPartition;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import org.apache.log4j.Logger;

import javax.naming.Context;
import javax.naming.NamingException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import java.util.List;
import java.util.UUID;

/**
 * The class that creates and controls an embedded ApacheDS instance.  This runner is not designed
 * for thread-safety or even to be used in the same JVM as anything else -- it's intended to be run
 * in its own process to provide LDAP access over TCP.
 * <p>
 * The idea of using ApacheDS for this was from Spring Security's LDAP test support.  The details
 * are from the ApacheDS embedding and unit testing documentation.
 * <p>
 * This version uses ApacheDS 2.0 and is based on
 * http://svn.apache.org/repos/asf/directory/sandbox/kayyagari/embedded-sample-trunk/src/main/java/org/apache/directory/seserver/EmbeddedADSVerTrunk.java
 */
public class Server {
    private final Logger log = Logger.getLogger(getClass());

    private final int port;
    private final String domainComponent;
    private final boolean allowAnonymous;
    private final File tempDir;
    private final String ldifFileName;
    private boolean running = false;
    private Collection<String> customSchemaFilenames;

    private DirectoryService service;
    private LdapServer ldapServer;

    public Server(
        int port, String domainComponent, File ldifFile, File tempDirBase, boolean allowAnonymous,
        Collection<String> customSchemaFilenames
    ) {
        this.port = port;
        this.domainComponent = domainComponent;
        this.allowAnonymous = allowAnonymous;
        this.tempDir = createTempDir(tempDirBase);
        this.ldifFileName = ldifFile.getPath();
        this.customSchemaFilenames = customSchemaFilenames;
    }

    ////// SETUP

    private File createTempDir(File tempDirBase) {
        File temp = new File(tempDirBase, "ladle-server-" + UUID.randomUUID());

        if (temp.mkdir()) {
            return temp;
        } else {
            throw new LadleFatalException("Could not create temporary directory " + temp);
        }
    }

    private static Hashtable<String, String> baseEnvironment() {
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put(Context.PROVIDER_URL, "");
        env.put(Context.INITIAL_CONTEXT_FACTORY,
            "org.apache.directory.server.jndi.ServerContextFactory");

        // these values are apparently hardcoded in ApacheDS
        env.put(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system");
        env.put(Context.SECURITY_CREDENTIALS, "secret");
        env.put(Context.SECURITY_AUTHENTICATION, "simple");

        return env;
    }

    ////// RUNNING

    @SuppressWarnings(value={"unchecked"})
    public void start() throws Exception {
        if (running) return;

        try {
            // Initialize the LDAP service
            service = new DefaultDirectoryService();
            service.setInstanceLayout( new InstanceLayout( tempDir ) );

            CacheService cacheService = new CacheService();
            cacheService.initialize( service.getInstanceLayout() );

            service.setCacheService( cacheService );

            // first load the schema
            initSchemaPartition();

            // then the system partition
            initSystemPartition();

            // Disable the ChangeLog system
            service.getChangeLog().setEnabled( false );
            service.setDenormalizeOpAttrsEnabled( true );

            // Now we can create as many partitions as we need
            Partition ladlePartition = addPartition( "ladle", domainComponent, service.getDnFactory() );

            // Setup indexes, access rules, and start it up
            addIndex( ladlePartition, "objectClass", "ou", "dc", "uid" );
            service.setAllowAnonymousAccess( allowAnonymous );
            service.startup();

             // Inject the context entry for the partition if it does not already exist
            try
            {
                service.getAdminSession().lookup( ladlePartition.getSuffixDn() );
            }
            catch ( LdapException lnnfe )
            {
                Dn userDN = new Dn( domainComponent );
                Entry userEntry = service.newEntry( userDN );
                userEntry.add( "objectClass", "top", "domain", "extensibleObject" );
                userEntry.add( "dc", domainComponent.split(",")[0].substring(3) );
                service.getAdminSession().add( userEntry );
            }

            // Load up any extra data
            for (String schemaFileName : customSchemaFilenames) {
                loadLDIF(schemaFileName);
            }
            loadLDIF(ldifFileName);

            // Now create the LDAP server and transport for the Directory Service.
            ldapServer = new LdapServer();
            ldapServer.setDirectoryService( service );
            TcpTransport ldapTransport = new TcpTransport( port );
            ldapServer.setTransports( ldapTransport );
            ldapServer.start();
        } catch (NamingException e) {
            throw new LadleFatalException("Startup failed", e);
        } catch (InstantiationException e) {
            throw new LadleFatalException("Custom schema not initializable", e);
        } catch (IllegalAccessException e) {
            throw new LadleFatalException("Custom schema not initializable", e);
        }

        running = true;
    }

    public void loadLDIF(String filepath) throws Exception {

        log.info("Loading : " + filepath);

        if (!service.isStarted()) {
            throw new Exception("Directory service not started");
        } else {
            InputStream inputStream = null;
            SchemaManager schemaManager = service.getSchemaManager();
            try {
                inputStream = new FileInputStream(filepath);
                if (inputStream != null) {
                    LdifReader entries = new LdifReader(inputStream);
                    for (LdifEntry ldifEntry : entries) {
                        DefaultEntry newEntry = new DefaultEntry(schemaManager, ldifEntry.getEntry());
                        service.getAdminSession().add( newEntry );
                    }
                }
            } finally {
                if (inputStream != null) inputStream.close();
            }
        }
    }

    /**
     * Add a new partition to the server
     *
     * @param partitionId The partition Id
     * @param partitionDn The partition DN
     * @param dnFactory The DN factory
     * @return The newly added partition
     * @throws Exception If the partition can't be added
     */
    private Partition addPartition( String partitionId, String partitionDn, DnFactory dnFactory ) throws Exception
    {
        // Create a new partition with the given partition id
        JdbmPartition partition = new JdbmPartition(service.getSchemaManager(), dnFactory);
        partition.setId( partitionId );
        partition.setPartitionPath( new File( service.getInstanceLayout().getPartitionsDirectory(), partitionId ).toURI() );
        partition.setSuffixDn( new Dn( partitionDn ) );
        service.addPartition( partition );

        return partition;
    }

    /**
     * initialize the schema manager and add the schema partition to directory service
     *
     * @throws Exception if the schema LDIF files are not found on the classpath
     */
    private void initSchemaPartition() throws Exception
    {
        InstanceLayout instanceLayout = service.getInstanceLayout();

        File schemaPartitionDirectory = new File( instanceLayout.getPartitionsDirectory(), "schema" );

        // Extract the schema on disk (a brand new one) and load the registries
        if ( schemaPartitionDirectory.exists() )
        {
            log.warn( "schema partition already exists, skipping schema extraction" );
        }
        else
        {
            SchemaLdifExtractor extractor = new DefaultSchemaLdifExtractor( instanceLayout.getPartitionsDirectory() );
            extractor.extractOrCopy();
        }

        SchemaLoader loader = new LdifSchemaLoader( schemaPartitionDirectory );
        SchemaManager schemaManager = new DefaultSchemaManager( loader );

        // We have to load the schema now, otherwise we won't be able
        // to initialize the Partitions, as we won't be able to parse
        // and normalize their suffix Dn
        schemaManager.loadAllEnabled();

        List<Throwable> errors = schemaManager.getErrors();

        if ( errors.size() != 0 )
        {
            throw new Exception( I18n.err( I18n.ERR_317, Exceptions.printErrors( errors ) ) );
        }

        service.setSchemaManager( schemaManager );

        // Init the LdifPartition with schema
        LdifPartition schemaLdifPartition = new LdifPartition( schemaManager, service.getDnFactory() );
        schemaLdifPartition.setPartitionPath( schemaPartitionDirectory.toURI() );

        // The schema partition
        SchemaPartition schemaPartition = new SchemaPartition( schemaManager );
        schemaPartition.setWrappedPartition( schemaLdifPartition );
        service.setSchemaPartition( schemaPartition );
    }

    private void initSystemPartition() throws Exception {
        // this is a MANDATORY partition
        // DO NOT add this via addPartition() method, trunk code complains about duplicate partition
        // while initializing
        JdbmPartition systemPartition = new JdbmPartition( service.getSchemaManager(), service.getDnFactory() );
        systemPartition.setId( "system" );
        systemPartition.setPartitionPath( new File( service.getInstanceLayout().getPartitionsDirectory(), systemPartition.getId() ).toURI() );
        systemPartition.setSuffixDn( new Dn( ServerDNConstants.SYSTEM_DN ) );
        systemPartition.setSchemaManager( service.getSchemaManager() );

        // mandatory to call this method to set the system partition
        // Note: this system partition might be removed from trunk
        service.setSystemPartition( systemPartition );
    }

    /**
     * Add a new set of index on the given attributes
     *
     * @param partition The partition on which we want to add index
     * @param attrs The list of attributes to index
     */
    private void addIndex( Partition partition, String... attrs )
    {
        // Index some attributes on the apache partition
        Set indexedAttributes = new HashSet();

        for ( String attribute : attrs )
        {
            indexedAttributes.add( new JdbmIndex( attribute, false ) );
        }

        ( ( JdbmPartition ) partition ).setIndexedAttributes( indexedAttributes );
    }

    public void stop() throws LadleFatalException {
        if (!running) return;
        try {
            service.shutdown();
        } catch (Exception e) {
            throw new LadleFatalException("Shutdown failed", e);
        }
        running = false;

        if (tempDir.exists()) {
            try {
                FileUtils.deleteDirectory(tempDir);
            } catch (IOException e) {
                log.error("Deleting the temporary directory " + tempDir + " failed", e);
            }
        }
    }
}