/*
 Copyright 2014 Red Hat, Inc. and/or its affiliates.

 This file is part of lightblue.

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.redhat.lightblue.config.ldap;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.redhat.lightblue.config.DataSourceConfiguration;
import com.redhat.lightblue.metadata.ldap.parser.LdapDataStoreParser;
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionPool;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.RoundRobinServerSet;
import com.unboundid.ldap.sdk.ServerSet;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldap.sdk.SingleServerSet;

/**
 * {@link DataSourceConfiguration} for LDAP.
 *
 * @author dcrissman
 */
public class LdapDataSourceConfiguration implements DataSourceConfiguration{

    private static final long serialVersionUID = 3276072662352275664L;
    private static final Logger LOGGER = LoggerFactory.getLogger(LdapDataSourceConfiguration.class);

    private static final String LDAP_CONFIG_DATABASE = "database";
    private static final String LDAP_CONFIG_BINDABLE_DB = "bindabledn";
    private static final String LDAP_CONFIG_PASSWORD = "password";
    private static final String LDAP_CONFIG_NUMBER_OF_INITIAL_CONNECTIONS = "numberOfInitialConnections";
    private static final String LDAP_CONFIG_MAX_NUMBER_OF_CONNECTIONS = "maxNumberOfConnections";
    private static final String LDAP_SERVER_CONFIG_HOST = "host";
    private static final String LDAP_SERVER_CONFIG_PORT = "port";

    private static final int DEFAULT_NUMBER_OF_INITIAL_CONNECTIONS = 5;
    private static final int DEFAULT_MAX_NUMBER_OF_CONNECTIONS = 10;

    private String databaseName;
    private transient LDAPConnectionPool connectionPool;

    public String getDatabaseName(){
        return databaseName;
    }

    @Override
    @SuppressWarnings("rawtypes")
    public Class<LdapDataStoreParser> getMetadataDataStoreParser() {
        return LdapDataStoreParser.class;
    }

    /**
     * Returns a {@link LDAPConnection} instance to use for communicating with the
     * underlying database. The returned connection may not always be to the same
     * database node if a connection pool is being used.
     * @return a {@link LDAPConnection} instance.
     * @throws LDAPException
     */
    public LDAPConnection getLdapConnection() throws LDAPException{
        if(connectionPool == null){
            throw new IllegalStateException("Class has not yet been initialized");
        }
        return connectionPool.getConnection();
    }

    @Override
    public void initializeFromJson(JsonNode node) {
        if(node == null){
            LOGGER.warn("Attempted to initizlize an LDAP datasource from a null JsonNode.");
            return;
        }

        databaseName = parseJsonNode(node, LDAP_CONFIG_DATABASE, true).asText();

        //TODO Add functionality for other BindRequest Types
        BindRequest bindRequest = new SimpleBindRequest(
                parseJsonNode(node, LDAP_CONFIG_BINDABLE_DB, true).asText(),
                parseJsonNode(node, LDAP_CONFIG_PASSWORD, true).asText());

        int initialConnections = parseInitialConnections(node);
        int maxConnections = parseMaxConnections(node);
        Map<String, Integer> hostPortMap = parseServers(node);

        String[] hosts = hostPortMap.keySet().toArray(new String[0]);

        ServerSet serverSet;
        if(hostPortMap.size() == 1){
            serverSet = new SingleServerSet(hosts[0], hostPortMap.get(hosts[0]));
        }
        else{
            int[] ports = new int[hosts.length];
            for(int x = 0; x < ports.length; x++){
                ports[x] = hostPortMap.get(hosts[x]);
            }

            //TODO Add support for other ServerSet types.
            serverSet = new RoundRobinServerSet(hosts, ports);
        }

        try{
            connectionPool = new LDAPConnectionPool(serverSet, bindRequest, initialConnections, maxConnections);
        }
        catch(LDAPException e) {
            throw new LdapConfigException("Unable to connect to ldap server(s).", e);
        }
    }

    private Map<String, Integer> parseServers(JsonNode node) {
        JsonNode serversNode = parseJsonNode(node, "servers", true);
        Map<String, Integer> hostPortMap = new HashMap<String, Integer>();
        if(serversNode.isArray()){
            Iterator<JsonNode> serversIterator = serversNode.elements();
            while(serversIterator.hasNext()){
                JsonNode serverNode = serversIterator.next();
                hostPortMap.put(
                        parseJsonNode(serverNode, LDAP_SERVER_CONFIG_HOST, true).asText(),
                        parseJsonNode(serverNode, LDAP_SERVER_CONFIG_PORT, true).asInt());
            }
        }
        else{
            throw new IllegalArgumentException("Unable to parse 'servers' for ldap database " + databaseName
                    + ". Must be an instance of an array and must contain at least one entry with a host and port.");
        }

        if(hostPortMap.isEmpty()){
            throw new IllegalArgumentException("At least 1 server must be provided for ldap database " + databaseName);
        }

        return hostPortMap;
    }

    private int parseMaxConnections(JsonNode node) {
        int maxConnections = DEFAULT_MAX_NUMBER_OF_CONNECTIONS;
        JsonNode maxConnectionsNode = parseJsonNode(node, LDAP_CONFIG_MAX_NUMBER_OF_CONNECTIONS, false);
        if(maxConnectionsNode != null){
            maxConnections = maxConnectionsNode.asInt(DEFAULT_MAX_NUMBER_OF_CONNECTIONS);
        }
        return maxConnections;
    }

    private int parseInitialConnections(JsonNode node) {
        int initialConnections = DEFAULT_NUMBER_OF_INITIAL_CONNECTIONS;
        JsonNode initialConnectionsNode = parseJsonNode(node, LDAP_CONFIG_NUMBER_OF_INITIAL_CONNECTIONS, false);
        if(initialConnectionsNode != null){
            initialConnections = initialConnectionsNode.asInt(DEFAULT_NUMBER_OF_INITIAL_CONNECTIONS);
        }
        return initialConnections;
    }

    private JsonNode parseJsonNode(JsonNode node, String key, boolean required){
        JsonNode parsedNode = node.get(key);
        if(required && (parsedNode == null)){
            throw new IllegalArgumentException("Unable to find required field '" + key + "' for ldap connection.");
        }
        return parsedNode;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((databaseName == null) ? 0 : databaseName.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        LdapDataSourceConfiguration other = (LdapDataSourceConfiguration) obj;
        if (databaseName == null) {
            if (other.databaseName != null) {
                return false;
            }
        }
        else if (!databaseName.equals(other.databaseName)) {
            return false;
        }
        return true;
    }

}
