/*
 * Copyright 2018 herd-mdl contributors
 *
 * Licensed 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 org.tsi.mdlt.util;

import java.lang.invoke.MethodHandles;
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tsi.mdlt.aws.SsmUtil;
import org.tsi.mdlt.enums.SsmParameterKeyEnum;
import org.tsi.mdlt.pojos.User;

/**
 * Ldap Utils for ldap actions, like ldap user creation/modification/deletion, list ldap entries etc
 */
public class LdapUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    private static final String BASE_DN = SsmUtil.getPlainLdapParameter(SsmParameterKeyEnum.LDAP_DN).getValue();
    private static final String HOSTNAME = SsmUtil.getPlainLdapParameter(SsmParameterKeyEnum.LDAP_HOSTNAME).getValue() + ".ec2.internal";

    private static final String OU_PEOPLE = "People";
    private static final String OU_GROUPS = "Groups";

    private static final String DOMAIN_NAME = "cloudfjord.com";

    static {
        System.setProperty("javax.net.ssl.keyStore", "/usr/lib/jvm/jre/lib/security/cacerts");
        System.setProperty("javax.net.ssl.keyStorePassword", "changeit");
    }

    /**
     * Create ldap AD group and add user to newly created AD group
     *
     * @param adGroupName ldap AD group name to create
     * @param userId      uid of existing ldap user to be added to newly created AD group
     * @throws NamingException
     */
    public static void createAdGroup(String adGroupName, String userId) throws NamingException {
        DirContext ldapContext = getLdapContext(User.getLdapAdminUser());
        String groupDn = constructGroupDn(adGroupName, OU_GROUPS);
        String memberDn = constructEntryCn(userId, OU_PEOPLE);

        //Create attributes to be associated with the new group
        Attributes attrs = new BasicAttributes(true);
        Attribute objclass = new BasicAttribute("objectClass");
        objclass.add("top");
        objclass.add("groupOfNames");
        attrs.put("cn", adGroupName);
        attrs.put(objclass);
        BasicAttribute member = new BasicAttribute("member", memberDn);
        attrs.put(member);

        ldapContext.createSubcontext(groupDn, attrs);
        LOGGER.info("Created group: " + adGroupName);
    }

    /**
     * delete ldap AD group with provided group name
     *
     * @param groupName ldap AD group name to delete
     * @throws NamingException
     */
    public static void deleteAdGroup(String groupName) throws NamingException {
        LOGGER.info(String.format("Remove AD group: %s", groupName));
        DirContext ldapContext = getLdapContext(User.getLdapAdminUser());
        String groupDn = constructGroupDn(groupName, OU_GROUPS);
        ldapContext.unbind(groupDn);
    }

    private static void createOu(String ou) throws NamingException {
        DirContext ldapContext = getLdapContext(User.getLdapAdminUser());
        Attributes attrs = new BasicAttributes(true);
        Attribute objclass = new BasicAttribute("objectClass");
        objclass.add("top");
        objclass.add("organizationalUnit");
        attrs.put(objclass);
        attrs.put("ou", ou);
        ldapContext.bind(constructOuDn(ou), null, attrs);
    }

    /**
     * Delete ldap user from ldap entry
     *
     * @param userId ldap user id
     * @throws NamingException
     */
    public static void deleteEntry(String userId) throws NamingException {
        deleteEntry(userId, OU_PEOPLE);
    }

    /**
     * Delete ldap user from ldap entry
     *
     * @param userId ldap user id
     * @throws NamingException
     */
    private static void deleteEntry(String userId, String ou) throws NamingException {
        LOGGER.info(String.format("Delete user: %s", userId));
        String entryDN = constructEntryCn(userId, ou);
        DirContext ldapContext = getLdapContext(User.getLdapAdminUser());
        ldapContext.unbind(entryDN);
    }

    /**
     * add ldap user to ldap AD group
     *
     * @param userId    user id
     * @param groupName ad group name
     * @throws NamingException
     */
    public static void addUserToGroup(String userId, String groupName) throws NamingException {
        modifyAttributes(userId, OU_PEOPLE, groupName, LdapContext.ADD_ATTRIBUTE);
    }

    /**
     * add ldap user to ldap AD group
     *
     * @param userId    ldap user id
     * @param ou        ou of the user
     * @param groupName ldap AD group name
     * @throws NamingException
     */
    private static void addUserToGroup(String userId, String ou, String groupName) throws NamingException {
        LOGGER.info(String.format("Add user: %s to AD group: %s", userId, groupName));
        modifyAttributes(userId, ou, groupName, LdapContext.ADD_ATTRIBUTE);
    }

    /**
     * remove ldap user from ldap AD group
     *
     * @param userId    ldap user id
     * @param groupName ldap AD group
     * @throws NamingException
     */
    public static void removeUserFromGroup(String userId, String groupName) throws NamingException {
        LOGGER.info(String.format("Remove user: %s from AD group: %s", userId, groupName));
        removeUserFromGroup(userId, OU_PEOPLE, groupName);
    }

    /**
     * remove ldap user from ldap AD group
     *
     * @param userId    ldap user id
     * @param ouName    ou of the user
     * @param groupName ldap AD group
     * @throws NamingException
     */
    private static void removeUserFromGroup(String userId, String ouName, String groupName) throws NamingException {
        modifyAttributes(userId, ouName, groupName, LdapContext.REMOVE_ATTRIBUTE);
    }

    private static void modifyAttributes(String userId, String ou, String groupName, int modOp) throws NamingException {
        DirContext ldapContext = getLdapContext(User.getLdapAdminUser());
        String memberEntryDN = constructEntryCn(userId, ou);
        String groupDn = String.format("cn=%s,ou=%s,%s", groupName, OU_GROUPS, BASE_DN);

        BasicAttribute member = new BasicAttribute("member", memberEntryDN);
        Attributes atts = new BasicAttributes();
        atts.put(member);
        ldapContext.modifyAttributes(groupDn, modOp, atts);
    }

    /**
     * create ldap user with provided user id and user password
     *
     * @param user new ldap user to create
     * @throws NamingException
     */
    public static void addEntry(User user) throws NamingException {
        String username = user.getUsername();

        Attribute userCn = new BasicAttribute("cn", user.getUsername());
        Attribute userSn = new BasicAttribute("sn", "null");
        Attribute uid = new BasicAttribute("uid", user.getUsername());

        Attribute uidNumber = new BasicAttribute("uidNumber", String.valueOf(listEntries() + 1));
        Attribute gidNumber = new BasicAttribute("gidNumber", String.valueOf(1001));
        Attribute homeDirectory = new BasicAttribute("homeDirectory", "/home/" + username);
        Attribute mail = new BasicAttribute("mail", username + "@" + DOMAIN_NAME);
        Attribute loginShell = new BasicAttribute("loginShell", "/bin/bash");

        Attribute userUserPassword = new BasicAttribute("userPassword", user.getPassword());
        //ObjectClass attributes
        Attribute objectClass = new BasicAttribute("objectClass");
        objectClass.add("inetOrgPerson");
        objectClass.add("posixAccount");

        Attributes entry = new BasicAttributes();
        entry.put(userCn);
        entry.put(userSn);
        entry.put(userUserPassword);
        entry.put(objectClass);
        entry.put(uid);

        entry.put(uidNumber);
        entry.put(gidNumber);
        entry.put(homeDirectory);
        entry.put(mail);
        entry.put(loginShell);

        String ou = user.getOu() == null ? "People" : user.getOu();
        String entryDN = constructEntryCn(user.getUsername(), ou);
        DirContext ldapContext = getLdapContext(User.getLdapAdminUser());
        ldapContext.createSubcontext(entryDN, entry);
        LOGGER.info("Added Entry :" + entryDN);
    }

        /**
         * list ldap entries
         *
         * @throws NamingException
         */
    //TODO split list Entries with get Max uidNumber
    public static int listEntries() throws NamingException {
        DirContext context = getLdapContext(User.getLdapAdminUser());
        int maxUidNumber = 10009;

        String searchFilter = "(objectClass=inetOrgPerson)";
        String[] requiredAttributes = {"uid", "cn", "sn", "uidNumber"};

        SearchControls controls = new SearchControls();
        controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        controls.setReturningAttributes(requiredAttributes);

        NamingEnumeration users;
        try {
            users = context.search(BASE_DN, searchFilter, controls);
            while (users.hasMore()) {
                SearchResult searchResult = (SearchResult) users.next();
                Attributes attr = searchResult.getAttributes();
                String commonName = attr.get("cn").get(0).toString();
                String uniqueName = attr.get("uid").get(0).toString();
                String sn = attr.get("sn").get(0).toString();
                int uidNumber = Integer.parseInt(attr.get("uidNumber").get(0).toString());
                maxUidNumber = maxUidNumber > uidNumber ? maxUidNumber : uidNumber;
                LOGGER.info("Name = " + commonName);
                LOGGER.info("Uid = " + uniqueName);
                LOGGER.info("sn = " + sn);
                LOGGER.info("uidNumber = " + uidNumber);
            }
        }
        catch (NamingException e) {
            LOGGER.error(e.getMessage());
        }
        return maxUidNumber;
    }

    private static DirContext getLdapContext(User user) throws NamingException {
        String username = user.getUsername();
        String password = user.getPassword();

        String url = String.format("ldaps://%s:636", HOSTNAME);
        String conntype = "simple";
        String adminDN = String.format("cn=%s,%s", username, BASE_DN);

        Hashtable<String, String> environment = new Hashtable<>();

        environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        environment.put(Context.PROVIDER_URL, url);
        environment.put(Context.SECURITY_AUTHENTICATION, conntype);
        environment.put(Context.SECURITY_PRINCIPAL, adminDN);
        environment.put(Context.SECURITY_CREDENTIALS, password);

        DirContext ldapContext = new InitialDirContext(environment);
        LOGGER.info("Ldap Bind successful");
        return ldapContext;
    }

    private static String constructEntryCn(String cn, String ou) {
        return String.format("cn=%s,ou=%s,%s", cn, ou, BASE_DN);
    }

    private static String constructGroupDn(String groupName, String ou) {
        return String.format("cn=%s,ou=%s,%s", groupName, ou, BASE_DN);
    }

    private static String constructOuDn(String ou) {
        return String.format("ou=%s,%s", ou, BASE_DN);
    }
}