/*
 *   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.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.commons.lang3.StringUtils;
import org.apache.directory.fortress.core.GlobalErrIds;
import org.apache.directory.fortress.core.GlobalIds;
import org.apache.directory.fortress.core.SecurityException;
import org.apache.directory.fortress.core.ValidationException;
import org.apache.directory.fortress.core.model.PwPolicy;
import org.apache.directory.fortress.core.util.cache.Cache;
import org.apache.directory.fortress.core.util.cache.CacheMgr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Process module for the OpenLDAP Password Policy entity.  This class performs data validations and error mapping.
 * It is typically called by internal Fortress manager class {@link PwPolicyMgrImpl} but also
 * needed by {@link org.apache.directory.fortress.core.impl.UserP#validate(org.apache.directory.fortress.core.model.User, boolean)}
 * This class is not intended to be used by external programs.  This class will accept Fortress entity, {@link org.apache.directory.fortress.core.model.PwPolicy}, on its
 * methods, validate contents and forward on to it's corresponding DAO class {@link PolicyDAO}.
 * <p>
 * Class will throw {@link SecurityException} to caller in the event of security policy, data constraint violation or system
 * error internal to DAO object. This class will forward DAO exceptions ({@link org.apache.directory.fortress.core.FinderException},
 *
 * {@link org.apache.directory.fortress.core.CreateException},{@link org.apache.directory.fortress.core.UpdateException},{@link org.apache.directory.fortress.core.RemoveException}),
 * or {@link org.apache.directory.fortress.core.ValidationException} as {@link SecurityException}s with appropriate
 * error id from {@link org.apache.directory.fortress.core.GlobalErrIds}.
 * <p>
 * This class uses one reference to synchronized data set {@link #policyCache} but is thread safe.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
final class PolicyP
{
    private static final String CLS_NM = PolicyP.class.getName();
    private static final Logger LOG = LoggerFactory.getLogger( CLS_NM );
    // This is 5 years duration in seconds:
    private static final int MAX_AGE = 157680000;

    // DAO class for ol pw policy data sets must be initialized before the other statics:
    private PolicyDAO olDao = new PolicyDAO();
    // this field is used to synchronize access to the above static data set:
    private static final ReadWriteLock policySetLock = new ReentrantReadWriteLock();
    // static field holds the list of names for all valid pw policies in effect:
    private static Cache policyCache;
    private static final int MIN_PW_LEN = 20;
    private static final int MAX_FAILURE = 100;
    private static final int MAX_GRACE_COUNT = 10;
    private static final int MAX_HISTORY = 100;
    private static final String POLICIES = "policies";
    private static final String FORTRESS_POLICIES = "fortress.policies";

    private void init()
    {
        CacheMgr cacheMgr = CacheMgr.getInstance();
        PolicyP.policyCache = cacheMgr.getCache( FORTRESS_POLICIES );
    }


    /**
     * Package private constructor.
     */
    PolicyP()
    {
        init();
    }


    /**
     * This function uses a case insensitive search.
     *
     * @param policy
     * @return true if valid, false otherwise.
     */
    boolean isValid( PwPolicy policy )
    {
        boolean result = false;

        try
        {
            policySetLock.readLock().lock();

            Set<String> policySet = getPolicySet( policy.getContextId() );

            if ( policySet != null )
            {
                result = policySet.contains( policy.getName() );
            }

            return result;
        }
        finally
        {
            policySetLock.readLock().unlock();
        }
    }


    /**
     * This method will return the password policy entity to the caller.  This command is valid
     * if and only if the policy entry is present in the POLICIES data set.
     *
     * @param policy contains the name of the policy entity.
     * @return PswdPolicy entity returns fully populated with attributes.
     * @throws SecurityException In the event policy entry not found, data validation or system error.
     */
    PwPolicy read( PwPolicy policy ) throws SecurityException
    {
        // Call the finder method for the primary key.
        return olDao.getPolicy( policy );
    }


    /**
     * This method will add a new policy entry to the POLICIES data set.  This command is valid
     * if and only if the policy entry is not already present in the POLICIES data set.
     *
     * @param policy Object contains the password policy attributes.
     * @throws SecurityException In the event of data validation or system error.
     */
    void add( PwPolicy policy ) throws SecurityException
    {
        validate( policy );
        olDao.create( policy );

        try
        {
            policySetLock.writeLock().lock();

            Set<String> policySet = getPolicySet( policy.getContextId() );

            if ( policySet != null )
            {
                policySet.add( policy.getName() );
            }
        }
        finally
        {
            policySetLock.writeLock().unlock();
        }
    }


    /**
     * This method will update an exiting policy entry to the POLICIES data set.  This command is valid
     * if and only if the policy entry is already present in the POLICIES data set.
     *
     * @param policy Object must contain the name of the policy entity.  All non-null attributes will
     *               be updated.  null attributes will be ignored.
     * @throws SecurityException In the event policy not found , data validation or system error.
     */
    void update( PwPolicy policy ) throws SecurityException
    {
        validate( policy );
        olDao.update( policy );
    }


    /**
     * This method will delete exiting policy entry from the POLICIES data set.  This command is valid
     * if and only if the policy entry is already present in the POLICIES data set.  Existing users that
     * are assigned this policy will be removed from association.
     *
     * @param policy Object must contain the name of the policy entity.
     * @throws SecurityException In the event policy entity not found or system error.
     */
    void delete( PwPolicy policy ) throws SecurityException
    {
        olDao.remove( policy );

        try
        {
            policySetLock.writeLock().lock();

            Set<String> policySet = getPolicySet( policy.getContextId() );

            if ( policySet != null )
            {
                policySet.remove( policy.getName() );
            }
        }
        finally
        {
            policySetLock.writeLock().unlock();
        }
    }


    /**
     * This method will return a list of all password policy entities that match a particular search string.
     * This command will return an empty list of no matching entries are found.
     *
     * @param policy contains the leading chars of a policy entity.  This search is not case sensitive.
     * @return List<PswdPolicy> contains all matching password policy entities. If no records found this will be empty.
     * @throws SecurityException In the event of data validation or system error.
     */
    List<PwPolicy> search( PwPolicy policy ) throws SecurityException
    {
        return olDao.findPolicy( policy );
    }


    /**
     * Method will perform simple validations to ensure the integrity of the OpenLDAP Password Policy entity targeted for insertion
     * or updating in directory.  Data reasonability checks will be performed on all non-null attributes.
     *
     * @param policy contains data targeted for insertion or update.
     * @throws ValidationException in the event of data validation error or DAO error on Org validation.
     */
    private void validate( PwPolicy policy ) throws ValidationException
    {
        int length = policy.getName().length();

        if ( length < 1 || length > GlobalIds.PWPOLICY_NAME_LEN )
        {
            String error = "validate policy name [" + policy.getName() + "] INVALID LENGTH [" + length + "]";
            LOG.error( error );
            throw new ValidationException( GlobalErrIds.PSWD_NAME_INVLD_LEN, error );
        }

        if ( policy.getCheckQuality() != null )
        {
            try
            {
                if ( policy.getCheckQuality() < 0 || policy.getCheckQuality() > 2 )
                {
                    String error = "validate policy name [" + policy.getName() + "] value checkQuality ["
                        + policy.getCheckQuality() + "] INVALID INT VALUE";
                    LOG.error( error );
                    throw new ValidationException( GlobalErrIds.PSWD_QLTY_INVLD, error );
                }
            }
            catch ( java.lang.NumberFormatException nfe )
            {
                String error = "validate policy name [" + policy.getName() + "] value checkQuality ["
                    + policy.getCheckQuality() + "] INVALID INT VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_QLTY_INVLD, error );
            }
        }

        if ( policy.getMaxAge() != null )
        {
            if ( policy.getMaxAge() > MAX_AGE )
            {
                String error = "validate policy name [" + policy.getName() + "] value maxAge [" + policy.getMaxAge()
                    + "] INVALID INT VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_MAXAGE_INVLD, error );
            }
        }

        if ( policy.getMinAge() != null )
        {
            // policy.minAge
            if ( policy.getMinAge() > MAX_AGE )
            {
                String error = "validate policy name [" + policy.getName() + "] value minAge [" + policy.getMinAge()
                    + "] INVALID INT VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_MINAGE_INVLD, error );
            }
        }

        if ( policy.getMinLength() != null )
        {
            if ( policy.getMinLength() > MIN_PW_LEN )
            {
                String error = "validate policy name [" + policy.getName() + "] value minLength ["
                    + policy.getMinLength() + "] INVALID INT VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_MINLEN_INVLD, error );
            }
        }

        if ( policy.getFailureCountInterval() != null )
        {
            if ( policy.getFailureCountInterval() > MAX_AGE )
            {
                String error = "validate policy name [" + policy.getName() + "] value failureCountInterval ["
                    + policy.getFailureCountInterval() + "] INVALID INT VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_INTERVAL_INVLD, error );
            }
        }

        if ( policy.getMaxFailure() != null )
        {
            if ( policy.getMaxFailure() > MAX_FAILURE )
            {
                String error = "validate policy name [" + policy.getName() + "] value maxFailure ["
                    + policy.getMaxFailure() + "] INVALID INT VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_MAXFAIL_INVLD, error );
            }
        }

        if ( policy.getInHistory() != null )
        {
            if ( policy.getInHistory() > MAX_HISTORY )
            {
                String error = "validate policy name [" + policy.getName() + "] value inHistory ["
                    + policy.getInHistory() + "] INVALID VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_HISTORY_INVLD, error );
            }
        }

        if ( policy.getGraceLoginLimit() != null )
        {
            if ( policy.getGraceLoginLimit() > MAX_GRACE_COUNT )
            {
                String error = "validate policy name [" + policy.getName() + "] value graceLoginLimit ["
                    + policy.getGraceLoginLimit() + "] INVALID VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_GRACE_INVLD, error );
            }
        }

        if ( policy.getLockoutDuration() != null )
        {
            if ( policy.getLockoutDuration() > MAX_AGE )
            {
                String error = "validate policy name [" + policy.getName() + "] value lockoutDuration ["
                    + policy.getLockoutDuration() + "] INVALID VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_LOCKOUTDUR_INVLD, error );
            }
        }

        if ( policy.getExpireWarning() != null )
        {
            if ( policy.getExpireWarning() > MAX_AGE )
            {
                String error = "validate policy name [" + policy.getName() + "] value expireWarning ["
                    + policy.getExpireWarning() + "] INVALID VALUE";
                LOG.error( error );
                throw new ValidationException( GlobalErrIds.PSWD_EXPWARN_INVLD, error );
            }
        }
    }


    /**
     * Load the cache with read only list of valid openldap policy names.
     *
     * @param contextId maps to sub-tree in DIT, e.g. ou=contextId, dc=example, dc=com.
     * @return Set of unique names.
     */
    private Set<String> loadPolicySet( String contextId )
    {
        Set<String> policySet = null;

        try
        {
            policySet = olDao.getPolicies( contextId );
        }
        catch ( SecurityException se )
        {
            String warning = "loadPolicySet static initializer caught SecurityException=" + se;
            LOG.info( warning );
        }

        policyCache.put( getKey( contextId ), policySet );

        return policySet;
    }


    /**
     *
     * @param contextId maps to sub-tree in DIT, e.g. ou=contextId, dc=example, dc=com.
     * @return set containing list of policy names active.
     */
    private Set<String> getPolicySet( String contextId )
    {
        try
        {
            policySetLock.readLock().lock();

            Set<String> policySet = ( Set<String> ) policyCache.get( getKey( contextId ) );

            if ( policySet == null )
            {
                policySet = loadPolicySet( contextId );
            }

            return policySet;
        }
        finally
        {
            policySetLock.readLock().unlock();
        }
    }


    /**
     *
     * @param contextId maps to sub-tree in DIT, e.g. ou=contextId, dc=example, dc=com.
     * @return key for tenant's cache entry.
     */
    private static String getKey( String contextId )
    {
        String key = POLICIES;

        if ( StringUtils.isNotEmpty( contextId ) && !contextId.equalsIgnoreCase( GlobalIds.NULL ) )
        {
            key += ":" + contextId;
        }

        return key;
    }
}
