/*
 * 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
 *
 *      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.apache.jetspeed.security.spi.impl;

import java.sql.Timestamp;
import java.util.Date;
import java.util.List;

import org.apache.jetspeed.security.CredentialPasswordEncoder;
import org.apache.jetspeed.security.CredentialPasswordValidator;
import org.apache.jetspeed.security.InvalidPasswordException;
import org.apache.jetspeed.security.PasswordCredential;
import org.apache.jetspeed.security.SecurityException;
import org.apache.jetspeed.security.spi.AlgorithmUpgradeCredentialPasswordEncoder;
import org.apache.jetspeed.security.spi.PasswordCredentialInterceptor;
import org.apache.jetspeed.security.spi.UserPasswordCredentialPolicyManager;

/**
 * @version $Id: UserPasswordCredentialPolicyManagerImpl.java 1101694 2011-05-11 00:31:25Z ate $
 *
 */
public class UserPasswordCredentialPolicyManagerImpl implements UserPasswordCredentialPolicyManager
{
    private CredentialPasswordEncoder encoder;
    private CredentialPasswordValidator validator;
    private PasswordCredentialInterceptor[] interceptors;
            
    public UserPasswordCredentialPolicyManagerImpl()
    {
        this.interceptors = new PasswordCredentialInterceptor[0];
    }

    public UserPasswordCredentialPolicyManagerImpl(CredentialPasswordEncoder encoder, CredentialPasswordValidator validator, List<?> interceptors)
    {
        this.encoder = encoder;
        this.validator = validator;
        if(interceptors !=null)
        {
            this.interceptors = (PasswordCredentialInterceptor[]) interceptors.toArray(new PasswordCredentialInterceptor[interceptors.size()]);
        }
        else
        {
            this.interceptors = new PasswordCredentialInterceptor[0];
        }
    }

    public CredentialPasswordEncoder getCredentialPasswordEncoder()
    {
        return encoder;
    }

    public CredentialPasswordValidator getCredentialPasswordValidator()
    {
        return validator;
    }

    public boolean onLoad(PasswordCredential credential, String userName) throws SecurityException
    {
        boolean update = false;
        for (PasswordCredentialInterceptor pci : interceptors)
        {
            if (pci.afterLoad(userName, credential, encoder, validator))
            {
                update = true;
            }
        }
        return update;
    }

    public boolean authenticate(PasswordCredential credential, String userName, String password) throws SecurityException
    {
        return authenticate(credential, userName, password, false);
    }
    
    public boolean authenticate(PasswordCredential credential, String userName, String password, boolean authenticated) throws SecurityException
    {
        if (!authenticated)
        {
            String encodedPassword = password;
            if (encoder != null && credential.isEncoded())
            {
                if (encoder instanceof AlgorithmUpgradeCredentialPasswordEncoder)
                {
                    encodedPassword = ((AlgorithmUpgradeCredentialPasswordEncoder)encoder).encode(credential, password);
                }
                else
                {
                    encodedPassword = encoder.encode(userName, password);
                }
                authenticated = credential.getPassword().equals(encodedPassword);            
            }
        }
        boolean update = false;

        for (PasswordCredentialInterceptor pci : interceptors)
        {
            if (pci.afterAuthenticated(credential, authenticated))
            {
                update = true;
            }
        }
        if (update && (!credential.isEnabled() || credential.isExpired()))
        {
            authenticated = false;
        }

        if (authenticated)
        {
            credential.setAuthenticationFailures(0);
            if (encoder != null && encoder instanceof AlgorithmUpgradeCredentialPasswordEncoder)
            {
                ((AlgorithmUpgradeCredentialPasswordEncoder)encoder).recodeIfNeeded(credential, password);
                credential.clearNewPasswordSet();
            }
            credential.setPreviousAuthenticationDate(credential.getLastAuthenticationDate());
            credential.setLastAuthenticationDate(new Timestamp(new Date().getTime()));
            update = true;
        }
        else
        {
            credential.setAuthenticationFailures(credential.getAuthenticationFailures()+1);
        }
        
        return update;
    }

    public void onStore(PasswordCredential credential) throws SecurityException
    {
        onStore(credential, false);
    }
    
    public void onStore(PasswordCredential credential, boolean authenticated) throws SecurityException
    {
        if (credential.isNewPasswordSet())
        {
            String newPassword = null;
            if (credential.getNewPassword() != null)
            {
                if (credential.getOldPassword() != null && !authenticated)
                {
                    String validatingOldPassword = credential.getOldPassword();
                    if (credential.isEncoded() && encoder != null)
                    {
                        if (encoder instanceof AlgorithmUpgradeCredentialPasswordEncoder)
                        {
                            validatingOldPassword = ((AlgorithmUpgradeCredentialPasswordEncoder)encoder).encode(credential, validatingOldPassword);
                        }
                        else
                        {
                            validatingOldPassword = encoder.encode(credential.getUserName(), validatingOldPassword);
                        }
                    }
                    if (credential.getPassword() == null || !credential.getPassword().equals(validatingOldPassword))
                    {
                        throw new InvalidPasswordException();
                    }
                    authenticated = true;
                }
                if (validator != null)
                {
                    if (!authenticated)
                    {
                        // Note: authenticated is also forced set to true during synchronization like from Ldap
                        // this might means the initial password isn't valid, but needs to be accepted anyway
                        // but will be forced to be changed after first login.
                        validator.validate(credential.getNewPassword());
                    }
                }
                newPassword = credential.getNewPassword();
                if (encoder != null)
                {
                    newPassword = encoder.encode(credential.getUserName(), newPassword);
                }
                
            }
            else
            {
                newPassword = credential.getPassword();
                if (encoder != null && !credential.isEncoded())
                {
                    newPassword = encoder.encode(credential.getUserName(), newPassword);
                }
            }
            
            if (!credential.isNew())
            {
                for (PasswordCredentialInterceptor pci : interceptors)
                {
                    pci.beforeSetPassword(credential, newPassword, authenticated);
                }
                credential.setUpdateRequired(false);
            }
            credential.setPassword(newPassword, encoder != null);
            credential.clearNewPasswordSet();
            if (!authenticated)
            {
                if (encoder != null && encoder instanceof AlgorithmUpgradeCredentialPasswordEncoder)
                {
                    // set current time in previous auth date, and clear last authentication date
                    // !!! While this might be a bit strange logic, it is *required* for the AlgorithmUpgradePBEPasswordEncodingService
                    // to be able to distinguise password changes from other changes
                    credential.setPreviousAuthenticationDate(new Timestamp(new Date().getTime()));
                    credential.setLastAuthenticationDate(null);
                }
            }
            else
            {
                // authenticated password change (by user itself)
                credential.setPreviousAuthenticationDate(credential.getLastAuthenticationDate());
                credential.setLastAuthenticationDate(new Timestamp(new Date().getTime()));
            }
        }
        if (credential.isNew())
        {
            for (PasswordCredentialInterceptor pci : interceptors)
            {
                pci.beforeCreate(credential);
            }
        }
    }
}
