/*

 *   Licensed to the Apache Software Foundation (ASF) under one
 *   or more contributor license agreements.  See the NOTICE file
 *   distributed with this work for additional information
 *   regarding copyright ownership.  The ASF licenses this file
 *   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
 *
 *     https://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.apache.directory.fortress.core.impl;


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.SearchCursor;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.DefaultModification;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapNoSuchObjectException;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.fortress.core.CreateException;
import org.apache.directory.fortress.core.FinderException;
import org.apache.directory.fortress.core.GlobalErrIds;
import org.apache.directory.fortress.core.GlobalIds;
import org.apache.directory.fortress.core.RemoveException;
import org.apache.directory.fortress.core.UpdateException;
import org.apache.directory.fortress.core.ldap.LdapDataProvider;
import org.apache.directory.fortress.core.model.*;
import org.apache.directory.fortress.core.util.Config;
import org.apache.directory.fortress.core.util.PropUtil;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Contains the Group node for LDAP Directory Information Tree.
 * This class is thread safe.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
final class GroupDAO extends LdapDataProvider implements PropertyProvider<Group>
{
    private static final String CLS_NM = GroupDAO.class.getName();
    private static final Logger LOG = LoggerFactory.getLogger( CLS_NM );
    private static final String GROUP_OBJECT_CLASS = "group.objectclass";
    private String GROUP_OBJECT_CLASS_IMPL;
    private static final String GROUP_PROTOCOL_ATTR = "group.protocol";
    private String GROUP_PROTOCOL_ATTR_IMPL;
    private static final String GROUP_PROPERTY_ATTR = "group.properties";
    private String GROUP_PROPERTY_ATTR_IMPL;
    private String[] GROUP_OBJ_CLASS;
    private String[] GROUP_ATRS;

    /**
     * Package private default constructor.
     */
    GroupDAO()
    {
        super();
        GROUP_OBJECT_CLASS_IMPL = Config.getInstance().getProperty( GROUP_OBJECT_CLASS );
        GROUP_PROTOCOL_ATTR_IMPL = Config.getInstance().getProperty( GROUP_PROTOCOL_ATTR );
        GROUP_PROPERTY_ATTR_IMPL = Config.getInstance().getProperty( GROUP_PROPERTY_ATTR );
        
        GROUP_OBJ_CLASS = new String[]{SchemaConstants.TOP_OC, GROUP_OBJECT_CLASS_IMPL };
        
        GROUP_ATRS = new String[]
            {
                SchemaConstants.CN_AT,
                SchemaConstants.DESCRIPTION_AT,
                GlobalIds.TYPE,
                GROUP_PROTOCOL_ATTR_IMPL,
                GROUP_PROPERTY_ATTR_IMPL,
                SchemaConstants.MEMBER_AT };
    }


    /**
     * @param group
     * @throws org.apache.directory.fortress.core.CreateException
     *
     */
    Group create( Group group ) throws CreateException
    {
        LdapConnection ld = null;
        String nodeDn = getDn( group.getName(), group.getContextId() );

        try
        {
            LOG.debug( "create group dn [{}]", nodeDn );
            Entry myEntry = new DefaultEntry( nodeDn );
            myEntry.add( SchemaConstants.OBJECT_CLASS_AT, GROUP_OBJ_CLASS );
            myEntry.add( SchemaConstants.CN_AT, group.getName() );
            // protocol is required:
            myEntry.add( GROUP_PROTOCOL_ATTR_IMPL, group.getProtocol() );
            // type is required:
            myEntry.add( GlobalIds.TYPE, group.getType().toString() );

            loadAttrs( group.getMembers(), myEntry, SchemaConstants.MEMBER_AT );
            loadProperties( group.getProperties(), myEntry, GROUP_PROPERTY_ATTR_IMPL, '=' );

            if ( StringUtils.isNotEmpty( group.getDescription() ) )
            {
                myEntry.add( SchemaConstants.DESCRIPTION_AT, group.getDescription() );
            }

            ld = getAdminConnection();
            add( ld, myEntry );
        }
        catch ( LdapException e )
        {
            String error = "create group node dn [" + nodeDn + "] caught LDAPException=" + e;
            throw new CreateException( GlobalErrIds.GROUP_ADD_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return group;
    }


    /**
     * @param group
     * @return
     * @throws org.apache.directory.fortress.core.CreateException
     *
     */
    Group update( Group group ) throws FinderException, UpdateException
    {
        LdapConnection ld = null;
        String nodeDn = getDn( group.getName(), group.getContextId() );

        try
        {
            LOG.debug( "update group dn [{}]", nodeDn );
            List<Modification> mods = new ArrayList<Modification>();

            if ( StringUtils.isNotEmpty( group.getDescription() ) )
            {
                mods.add( new DefaultModification(
                    ModificationOperation.REPLACE_ATTRIBUTE, SchemaConstants.DESCRIPTION_AT, group.getDescription() ) );
            }

            if ( StringUtils.isNotEmpty( group.getProtocol() ) )
            {
                mods.add( new DefaultModification(
                    ModificationOperation.REPLACE_ATTRIBUTE, GROUP_PROTOCOL_ATTR_IMPL, group.getProtocol() ) );
            }

            loadAttrs( group.getMembers(), mods, SchemaConstants.MEMBER_AT );
            loadProperties( group.getProperties(), mods, GROUP_PROPERTY_ATTR_IMPL, true, '=' );

            if ( mods.size() > 0 )
            {
                ld = getAdminConnection();
                modify( ld, nodeDn, mods, group );
            }
        }
        catch ( LdapException e )
        {
            String error = "update group node dn [" + nodeDn + "] caught LDAPException=" + e;
            throw new UpdateException( GlobalErrIds.GROUP_UPDATE_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return get( group );
    }


    Group add( Group group, String key, String value ) throws FinderException, CreateException
    {
        LdapConnection ld = null;
        String nodeDn = getDn( group.getName(), group.getContextId() );

        try
        {
            LOG.debug( "add group property dn [{}], key [{}], value [{}]", nodeDn, key, value );
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification(
                ModificationOperation.ADD_ATTRIBUTE, GROUP_PROPERTY_ATTR_IMPL, key + "=" + value ) );
            ld = getAdminConnection();
            modify( ld, nodeDn, mods, group );
        }
        catch ( LdapException e )
        {
            String error = "update group property node dn [" + nodeDn + "] caught LDAPException=" + e;
            throw new CreateException( GlobalErrIds.GROUP_ADD_PROPERTY_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return get( group );
    }


    Group delete( Group group, String key, String value ) throws FinderException, RemoveException
    {
        LdapConnection ld = null;
        String nodeDn = getDn( group.getName(), group.getContextId() );

        try
        {
            LOG.debug( "delete group property dn [{}], key [{}], value [{}]", nodeDn, key, value );
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification(
                ModificationOperation.REMOVE_ATTRIBUTE, GROUP_PROPERTY_ATTR_IMPL, key + "=" + value ) );
            ld = getAdminConnection();
            modify( ld, nodeDn, mods, group );
        }
        catch ( LdapException e )
        {
            String error = "delete group property node dn [" + nodeDn + "] caught LDAPException=" + e;
            throw new RemoveException( GlobalErrIds.GROUP_DELETE_PROPERTY_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return get( group );
    }


    /**
     * This method will remove group node from diretory.
     *
     * @param group
     * @throws org.apache.directory.fortress.core.RemoveException
     *
     */
    Group remove( Group group ) throws RemoveException
    {
        LdapConnection ld = null;
        String nodeDn = getDn( group.getName(), group.getContextId() );
        LOG.debug( "remove group dn [{}]", nodeDn );
        try
        {
            ld = getAdminConnection();
            delete( ld, nodeDn, group );
        }
        catch ( LdapException e )
        {
            String error = "remove group node dn [" + nodeDn + "] caught LDAPException=" + e;
            throw new RemoveException( GlobalErrIds.GROUP_DELETE_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return group;
    }


    /**
     * @param entity
     * @param userDn
     * @return
     * @throws org.apache.directory.fortress.core.UpdateException
     *
     */
    Group assign( Group entity, String userDn ) throws FinderException, UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( entity.getName(), entity.getContextId() );
        LOG.debug( "assign group property dn [{}], member dn [{}]", dn, userDn );
        try
        {
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification(
                ModificationOperation.ADD_ATTRIBUTE, SchemaConstants.MEMBER_AT, userDn ) );
            ld = getAdminConnection();
            modify( ld, dn, mods, entity );
        }
        catch ( LdapException e )
        {
            String error = "assign group name [" + entity.getName() + "] user dn [" + userDn + "] caught " +
                "LDAPException=" + e;
            throw new UpdateException( GlobalErrIds.GROUP_USER_ASSIGN_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return get( entity );
    }


    /**
     * @param entity
     * @param userDn
     * @return
     * @throws org.apache.directory.fortress.core.UpdateException
     *
     */
    Group deassign( Group entity, String userDn ) throws FinderException, UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( entity.getName(), entity.getContextId() );
        LOG.debug( "deassign group property dn [{}], member dn [{}]", dn, userDn );

        try
        {
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification(
                ModificationOperation.REMOVE_ATTRIBUTE, SchemaConstants.MEMBER_AT, userDn ) );

            ld = getAdminConnection();
            modify( ld, dn, mods, entity );
        }
        catch ( LdapException e )
        {
            String error = "deassign group name [" + entity.getName() + "] user dn [" + userDn + "] caught " +
                "LDAPException=" + e;
            throw new UpdateException( GlobalErrIds.GROUP_USER_DEASSIGN_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return get( entity );
    }


    /**
     * @param group
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    Group get( Group group ) throws FinderException
    {
        Group entity = null;
        LdapConnection ld = null;
        String dn = getDn( group.getName(), group.getContextId() );

        try
        {
            ld = getAdminConnection();
            Entry findEntry = read( ld, dn, GROUP_ATRS );
            if ( findEntry == null )
            {
                String warning = "No Group entry found dn [" + dn + "]";
                throw new FinderException( GlobalErrIds.GROUP_NOT_FOUND, warning );
            }
            entity = unloadLdapEntry( findEntry, 0 );
        }
        catch ( LdapNoSuchObjectException e )
        {
            String warning = "read Obj COULD NOT FIND ENTRY for dn [" + dn + "]";
            throw new FinderException( GlobalErrIds.GROUP_NOT_FOUND, warning, e );
        }
        catch ( LdapException e )
        {
            String error = "read dn [" + dn + "] LdapException=" + e;
            throw new FinderException( GlobalErrIds.GROUP_READ_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return entity;
    }


    /**
     * @param group
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    List<Group> find( Group group ) throws FinderException
    {
        List<Group> groupList = new ArrayList<>();
        LdapConnection ld = null;
        String groupRoot = getRootDn( group.getContextId(), GlobalIds.GROUP_ROOT );
        String filter = null;
        try
        {
            String searchVal = encodeSafeText( group.getName(), GlobalIds.ROLE_LEN );
            filter = GlobalIds.FILTER_PREFIX + GROUP_OBJECT_CLASS_IMPL + ")(" + SchemaConstants.CN_AT + "=" + searchVal
                + "*))";
            ld = getAdminConnection();
            try( SearchCursor searchResults = search( ld, groupRoot, SearchScope.ONELEVEL, filter, GROUP_ATRS, false,
                Config.getInstance().getInt(GlobalIds.CONFIG_LDAP_MAX_BATCH_SIZE, GlobalIds.BATCH_SIZE ) ) )
            {
                long sequence = 0;
                while ( searchResults.next() )
                {
                    groupList.add( unloadLdapEntry( searchResults.getEntry(), sequence++ ) );
                }
            }
            catch ( IOException i )
            {
                String error = "find filter [" + filter + "] caught IOException=" + i.getMessage();
                throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, i );
            }
            catch ( CursorException e )
            {
                String error = "find filter [" + filter + "] caught CursorException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, e );
            }
        }
        catch ( LdapException e )
        {
            String error = "find filter [" + filter + "] caught LDAPException=" + e;
            throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return groupList;
    }


    /**
     * @param user
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    List<Group> find( User user ) throws FinderException
    {
        List<Group> groupList = new ArrayList<>();
        LdapConnection ld = null;
        String groupRoot = getRootDn( user.getContextId(), GlobalIds.GROUP_ROOT );
        String filter = null;

        try
        {
            encodeSafeText( user.getUserId(), GlobalIds.USERID_LEN );
            filter = GlobalIds.FILTER_PREFIX + GROUP_OBJECT_CLASS_IMPL + ")(" + SchemaConstants.MEMBER_AT + "="
                + user.getDn() + "))";
            ld = getAdminConnection();
            try ( SearchCursor searchResults = search( ld, groupRoot, SearchScope.ONELEVEL, filter, GROUP_ATRS, false,
                Config.getInstance().getInt(GlobalIds.CONFIG_LDAP_MAX_BATCH_SIZE, GlobalIds.BATCH_SIZE ) ) )
            {
                long sequence = 0;
                while ( searchResults.next() )
                {
                    groupList.add( unloadLdapEntry( searchResults.getEntry(), sequence++ ) );
                }
            }
            catch ( IOException i )
            {
                String error = "find filter [" + filter + "] caught IOException=" + i.getMessage();
                throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, i );
            }
            catch ( CursorException e )
            {
                String error = "find filter [" + filter + "] caught CursorException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, e );
            }
        }
        catch ( LdapException e )
        {
            String error = "find filter [" + filter + "] caught LDAPException=" + e;
            throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return groupList;
    }


    /**
     * @param role
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    List<Group> roleGroups( Role role ) throws FinderException
    {
        List<Group> groupList = new ArrayList<>();
        LdapConnection ld = null;
        String groupRoot = getRootDn( role.getContextId(), GlobalIds.GROUP_ROOT );
        String filter = null;
        try
        {
            encodeSafeText( role.getName(), GlobalIds.ROLE_LEN );
            filter = GlobalIds.FILTER_PREFIX + GROUP_OBJECT_CLASS_IMPL + ")(" + SchemaConstants.MEMBER_AT + "="
                    + role.getDn() + "))";
            ld = getAdminConnection();
            try ( SearchCursor searchResults = search( ld, groupRoot, SearchScope.ONELEVEL, filter, GROUP_ATRS, false,
                    Config.getInstance().getInt(GlobalIds.CONFIG_LDAP_MAX_BATCH_SIZE, GlobalIds.BATCH_SIZE ) ) )
            {
                long sequence = 0;
                while ( searchResults.next() )
                {
                    groupList.add( unloadLdapEntry( searchResults.getEntry(), sequence++ ) );
                }
            }
            catch ( IOException i )
            {
                String error = "find filter [" + filter + "] caught IOException=" + i.getMessage();
                throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, i );
            }
            catch ( CursorException e )
            {
                String error = "find filter [" + filter + "] caught CursorException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, e );
            }
        }
        catch ( LdapException e )
        {
            String error = "find filter [" + filter + "] caught LDAPException=" + e;
            throw new FinderException( GlobalErrIds.GROUP_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return groupList;
    }


    /**
     * @param le
     * @param sequence
     * @return
     * @throws LdapException
     */
    private Group unloadLdapEntry( Entry le, long sequence )
        throws LdapInvalidAttributeValueException
    {
        Group entity = new ObjectFactory().createGroup();
        entity.setName( getAttribute( le, SchemaConstants.CN_AT ) );
        entity.setDescription( getAttribute( le, SchemaConstants.DESCRIPTION_AT ) );
        String typeAsString = getAttribute( le, GlobalIds.TYPE );
        if ( StringUtils.isNotEmpty(typeAsString) )
        {
            entity.setType( Group.Type.valueOf( typeAsString.toUpperCase() ) );
        }
        entity.setProtocol( getAttribute( le, GROUP_PROTOCOL_ATTR_IMPL ) );
        entity.setMembers( getAttributes( le, SchemaConstants.MEMBER_AT ) );
        entity.setMemberDn( true );
        entity.setProperties( PropUtil.getProperties( getAttributes( le, GROUP_PROPERTY_ATTR_IMPL ), '=' ) );
        entity.setSequenceId( sequence );

        return entity;
    }


    private String getDn( String name, String contextId )
    {
        return SchemaConstants.CN_AT + "=" + name + "," + getRootDn( contextId, GlobalIds.GROUP_ROOT );
    }


    @Override
    public String getDn( Group entity )
    {
        return getDn( entity.getName(), entity.getContextId() );
    }


    @Override
    public FortEntity getEntity( Group entity ) throws FinderException
    {
        return this.get( entity );
    }
}
