/*
 * Copyright (c) 2014 Denis Mikhalkin.
 *
 * This software is provided to you 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 com.denismo.apacheds;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;

import com.denismo.apacheds.auth.AWSIAMAuthenticator;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.Cursor;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.Entry;
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.name.Rdn;
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.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.InstanceLayout;
import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
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.AbstractBTreePartition;
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.core.partition.ldif.SingleFileLdifPartition;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.protocol.shared.store.LdifFileLoader;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import org.apache.directory.server.xdbm.Index;
import org.apache.directory.server.xdbm.IndexEntry;
import org.apache.directory.server.xdbm.ParentIdAndRdn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * User: Denis Mikhalkin
 * Date: 10/03/14
 * Time: 6:02 PM
 */
public class Runner {

    private static final Logger IAM_LOG = LoggerFactory.getLogger(Runner.class);
    private static int serverPort = 10389;
    private ApacheDSUtils utils;

    /** The directory service */
    private DirectoryService service;
//    private ApacheDsService service;
    /** The LDAP server */
    private LdapServer server;


    public Runner(DirectoryService service) {
        this.service = service;
        utils = new ApacheDSUtils(service);
    }
    public Runner() {}


    /**
     * initialize the schema manager and add the schema partition to diectory 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() )
        {
            System.out.println( "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 );
    }
    /**
     * Initialize the server. It creates the partition, adds the index, and
     * injects the context entries for the created partitions.
     *
     * @param workDir the directory to be used for storing the data
     * @throws Exception if there were some problems while initializing the system
     */
    private void initDirectoryService( File workDir ) throws Exception
    {
        // Initialize the LDAP service
        service = new DefaultDirectoryService();
        utils = new ApacheDSUtils(service);
//        service = new ApacheDsService();
//        service.start(new InstanceLayout( workDir ));
        service.setInstanceLayout( new InstanceLayout( workDir ) );

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

        service.setCacheService( cacheService );

        // first load the schema
        initSchemaPartition();

        // then the system partition
        // 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 );

        service.getChangeLog().setEnabled(false);
        service.setDenormalizeOpAttrsEnabled(true);

        SingleFileLdifPartition configPartition = new SingleFileLdifPartition(service.getSchemaManager(), service.getDnFactory());
        configPartition.setId("config");
        configPartition.setPartitionPath(new File(service.getInstanceLayout().getConfDirectory(), "config.ldif").toURI());
        configPartition.setSuffixDn(new Dn(service.getSchemaManager(), "ou=config"));
        configPartition.setSchemaManager(service.getSchemaManager());
        configPartition.setCacheService(cacheService);

        configPartition.initialize();
        service.addPartition(configPartition);

        readIAMProperties();

        String rootDN = AWSIAMAuthenticator.getConfig().rootDN;
        Partition iamPartition = utils.addPartition("iam", rootDN, service.getDnFactory());

        // Index some attributes on the apache partition
        utils.addIndex(iamPartition, "objectClass", "ou", "uid", "gidNumber", "uidNumber", "cn");

        // And start the service
        service.startup();

        utils.loadLdif("iam.ldif");
        utils.loadLdif("enable_nis.ldif");
        utils.loadLdif("auth.ldif");
        if (!utils.exists("cn=config,ads-authenticatorid=awsiamauthenticator,ou=authenticators,ads-interceptorId=authenticationInterceptor,ou=interceptors,ads-directoryServiceId=default,ou=config")) {
            Entry entryIAM = service.newEntry( service.getDnFactory().create("cn=config,ads-authenticatorid=awsiamauthenticator,ou=authenticators,ads-interceptorId=authenticationInterceptor,ou=interceptors,ads-directoryServiceId=default,ou=config") );
            entryIAM.put("objectClass", "iamauthenticatorconfig", "top");
            entryIAM.put(SchemaConstants.ENTRY_CSN_AT, service.getCSN().toString());
            entryIAM.put(SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString());
            entryIAM.put("cn", "config");
            entryIAM.put("idGenerator", "1000");
            service.getAdminSession().add(entryIAM);
        }
        Dn dnIAM = service.getDnFactory().create(rootDN);
        if (!service.getAdminSession().exists(dnIAM)) {
            Entry entryIAM = new DefaultEntry(service.getSchemaManager(), dnIAM, "objectClass: top", "objectClass: domain", "dc: iam",
                    "entryCsn: " + service.getCSN(), SchemaConstants.ENTRY_UUID_AT + ": " + UUID.randomUUID().toString());
            iamPartition.add(new AddOperationContext(null, entryIAM));
        }
    }

    public void createStructure() throws Exception {
        String rootDN = AWSIAMAuthenticator.getConfig().rootDN;
        Dn dnIAM = service.getDnFactory().create(rootDN);
        if (!utils.exists(dnIAM)) {
            IAM_LOG.info("Creating partition " + rootDN);
            Partition iamPartition = utils.addPartition("iam", rootDN, service.getDnFactory());

            // Index some attributes on the apache partition
            utils.addIndex(iamPartition, "objectClass", "ou", "uid", "gidNumber", "uidNumber", "cn");

            if (!utils.exists(dnIAM)) {
                IAM_LOG.info("Creating root node " + rootDN);
                Rdn rdn = dnIAM.getRdn(0);
                Entry entryIAM = new DefaultEntry(service.getSchemaManager(), dnIAM, "objectClass: top", "objectClass: domain",
                        "entryCsn: " + service.getCSN(), SchemaConstants.ENTRY_UUID_AT + ": " + UUID.randomUUID().toString(),
                        rdn.getType() + ": " + rdn.getValue());
                service.getAdminSession().add(entryIAM);
                checkErrors();
            }
        }
        service.sync();
    }

    public void checkErrors() {
        if (!service.getSchemaManager().getErrors().isEmpty()) {
            throw new RuntimeException("Errors: " + service.getSchemaManager().getErrors());
        }
    }

    private void readIAMProperties() {
        String propsPath = System.getProperty("iamLdapPropertiesPath", "/etc/iam_ldap.conf");
        File propsFile = new File(propsPath);
        // Read the config file if exists
        if (propsFile.exists()) {
            try {
                Properties props = new Properties();
                props.load(new FileInputStream(propsFile));
                AWSIAMAuthenticator.Config config = new AWSIAMAuthenticator.Config();
                if (props.containsKey("pollPeriod")) config.pollPeriod = Integer.parseInt(props.getProperty("pollPeriod"));
                if (props.containsKey("rootDN")) config.rootDN = props.getProperty("rootDN");
                AWSIAMAuthenticator.setConfig(config);
            } catch (IOException e) {
                IAM_LOG.error("Unable to read IAM LDAP config file");
                AWSIAMAuthenticator.setConfig(new AWSIAMAuthenticator.Config());
            }
        } else {
            // Populate from defaults
            AWSIAMAuthenticator.setConfig(new AWSIAMAuthenticator.Config());
        }
    }

    /**
     * starts the LdapServer
     *
     * @throws Exception
     */
    public void startServer() throws Exception
    {
        server = new LdapServer();

        server.setTransports( new TcpTransport( serverPort ) );
        server.setDirectoryService( service );

        server.start();
    }
    public static void main(String[] args) throws Exception {
        System.setProperty("default.controls", "org.apache.directory.api.ldap.codec.controls.cascade.CascadeFactory,org.apache.directory.api.ldap.codec.controls.manageDsaIT.ManageDsaITFactory,org.apache.directory.api.ldap.codec.controls.search.entryChange.EntryChangeFactory,org.apache.directory.api.ldap.codec.controls.search.pagedSearch.PagedResultsFactory,org.apache.directory.api.ldap.codec.controls.search.persistentSearch.PersistentSearchFactory,org.apache.directory.api.ldap.codec.controls.search.subentries.SubentriesFactory");
        System.setProperty("extra.controls", "org.apache.directory.api.ldap.extras.controls.ppolicy_impl.PasswordPolicyFactory,org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncDoneValueFactory,org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncInfoValueFactory,org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncRequestValueFactory,org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncStateValueFactory");
        System.setProperty("default.extendedOperation.requests", "org.apache.directory.api.ldap.extras.extended.ads_impl.cancel.CancelFactory,org.apache.directory.api.ldap.extras.extended.ads_impl.certGeneration.CertGenerationFactory,org.apache.directory.api.ldap.extras.extended.ads_impl.gracefulShutdown.GracefulShutdownFactory,org.apache.directory.api.ldap.extras.extended.ads_impl.storedProcedure.StoredProcedureFactory");
        System.setProperty("default.extendedOperation.responses", "org.apache.directory.api.ldap.extras.extended.ads_impl.gracefulDisconnect.GracefulDisconnectFactory");

        Runner runner = new Runner();
        runner.initDirectoryService(getDirectoryPath(args));
        runner.startServer();
        System.out.println("Server started on " + serverPort);
    }

    private static File getDirectoryPath(String[] args) {
        if (args.length > 0) return new File(args[0]);
        if (!new File("/var").exists()) {
            return new File(System.getProperty("java.io.tmpdir"), "iam_ldap");
        } else {
            return new File("/var", "iam_ldap");
        }
    }

    public Partition getPartition(DirectoryService directory, String id) throws LdapException {
        Set<? extends Partition> partitions = directory.getPartitions();
        for (Partition part : partitions) {
            if (part.getId().equalsIgnoreCase(id)) return part;
        }
        throw new LdapException("No partition with the ID " + id);
    }
}