/**
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * 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.jivesoftware.openfire.lockout;

import java.util.Date;
import java.util.Map;

import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The LockOutManager manages the LockOutProvider configured for this server, caches knowledge of
 * whether accounts are disabled or enabled, and provides a single point of entry for handling
 * locked/disabled accounts.
 *
 * The provider can be specified in system properties by adding:
 *
 * <ul>
 * <li><tt>provider.lockout.className = my.lock.out.provider</tt></li>
 * </ul>
 *
 * @author Daniel Henninger
 */
public class LockOutManager {

	private static final Logger Log = LoggerFactory.getLogger(LockOutManager.class);

    // Wrap this guy up so we can mock out the LockOutManager class.
    private static class LockOutManagerContainer {
        private static LockOutManager instance = new LockOutManager();
    }

    /**
     * Returns the currently-installed LockOutProvider. <b>Warning:</b> in virtually all
     * cases the lockout provider should not be used directly. Instead, the appropriate
     * methods in LockOutManager should be called. Direct access to the lockout provider is
     * only provided for special-case logic.
     *
     * @return the current LockOutProvider.
     */
    public static LockOutProvider getLockOutProvider() {
        return LockOutManagerContainer.instance.provider;
    }

    /**
     * Returns a singleton instance of LockOutManager.
     *
     * @return a LockOutManager instance.
     */
    public static LockOutManager getInstance() {
        return LockOutManagerContainer.instance;
    }

    /* Cache of locked out accounts */
    private Cache<String,LockOutFlag> lockOutCache;
    private LockOutProvider provider;

    /**
     * Constructs a LockOutManager, setting up it's cache, propery listener, and setting up the provider.
     */
    private LockOutManager() {
        // Initialize the lockout cache.
        lockOutCache = CacheFactory.createCache("Locked Out Accounts");

        // Load an lockout provider.
        initProvider();

        // Detect when a new lockout provider class is set 
        PropertyEventListener propListener = new PropertyEventListener() {
            @Override
            public void propertySet(String property, Map params) {
                if ("provider.lockout.className".equals(property)) {
                    initProvider();
                }
            }

            @Override
            public void propertyDeleted(String property, Map params) {
                //Ignore
            }

            @Override
            public void xmlPropertySet(String property, Map params) {
                //Ignore
            }

            @Override
            public void xmlPropertyDeleted(String property, Map params) {
                //Ignore
            }
        };
        PropertyEventDispatcher.addListener(propListener);
    }

    /**
     * Initializes the server's lock out provider, based on configuration and defaults to
     * DefaultLockOutProvider if the specified provider is not valid or not specified.
     */
    private void initProvider() {
        // Convert XML based provider setup to Database based
        JiveGlobals.migrateProperty("provider.lockout.className");

        String className = JiveGlobals.getProperty("provider.lockout.className",
                "org.jivesoftware.openfire.lockout.DefaultLockOutProvider");
        // Check if we need to reset the provider class
        if (provider == null || !className.equals(provider.getClass().getName())) {
            try {
                Class c = ClassUtils.forName(className);
                provider = (LockOutProvider) c.newInstance();
            }
            catch (Exception e) {
                Log.error("Error loading lockout provider: " + className, e);
                provider = new DefaultLockOutProvider();
            }
        }
    }

    /**
     * Returns a LockOutFlag for a given username, which contains information about the time
     * period that the specified account is going to be disabled.
     *
     * @param username Username of account to request status of.
     * @return The LockOutFlag instance describing the accounts disabled status or null if user
     *         account specified is not currently locked out (disabled).
     */
    public LockOutFlag getDisabledStatus(String username) {
        if (username == null) {
            throw new UnsupportedOperationException("Null username not allowed!");
        }
		LockOutFlag lockOutFlag = getUserLockOut(username);
		return getUnExpiredLockout(lockOutFlag);
    }

    /**
     * Returns true or false if an account is currently locked out.
     *
     * @param username Username of account to check on.
     * @return True or false if the account is currently locked out.
     */
    public boolean isAccountDisabled(String username) {
        LockOutFlag flag = getDisabledStatus(username);
        if (flag == null) {
            return false;
        }
        Date curDate = new Date();
        if (flag.getStartTime() != null && curDate.before(flag.getStartTime())) {
            return false;
        }
        if (flag.getEndTime() != null && curDate.after(flag.getEndTime())) {
            return false;
        }
        return true;
    }

    /**
     * Sets an account to disabled, starting at an optional time and ending at an optional time.
     * If either times are set to null, the lockout is considered "forever" in that direction.
     * For example, if you had a start time of 2 hours from now, and a null end time, then the account
     * would be locked out in two hours, and never unlocked until someone manually unlcoked the account.
     *
     * @param username User whose account will be disabled.
     * @param startTime When to start the lockout, or null if immediately.
     * @param endTime When to end the lockout, or null if forever.
     * @throws UnsupportedOperationException if the provider is readonly.
     */
    public void disableAccount(String username, Date startTime, Date endTime) throws UnsupportedOperationException {
        if (provider.isReadOnly()) {
            throw new UnsupportedOperationException();
        }
        LockOutFlag flag = new LockOutFlag(username, startTime, endTime);
        provider.setDisabledStatus(flag);
        if (!provider.shouldNotBeCached()) {
            // Add lockout data to cache.
            lockOutCache.put(username, flag);
        }
        // Fire event.
        LockOutEventDispatcher.accountLocked(flag);
    }

    /**
     * Enables an account that may or may not have previously been disabled.  This erases any
     * knowledge of a lockout, including one that wasn't necessarily in effect at the time the
     * method was called.
     *
     * @param username User to enable.
     * @throws UnsupportedOperationException if the provider is readonly.
     */
    public void enableAccount(String username) throws UnsupportedOperationException {
        if (provider.isReadOnly()) {
            throw new UnsupportedOperationException();
        }
        provider.unsetDisabledStatus(username);
        if (!provider.shouldNotBeCached()) {
            // Remove lockout data from cache.
            lockOutCache.remove(username);
        }
        // Fire event.
        LockOutEventDispatcher.accountUnlocked(username);
    }

    /**
     * "Records" (notifies all listeners) that a failed login occurred.
     *
     * @param username Locked out user that attempted to login.
     */
    public void recordFailedLogin(String username) {
        // Fire event.
        LockOutEventDispatcher.lockedAccountDenied(username);
    }
    
    /**
	 * Gets the user lock out.
	 *
	 * @param username
	 *            the username
	 * @return the user lock out
	 */
	private LockOutFlag getUserLockOut(String username) {
		if (provider.shouldNotBeCached()) {
			return provider.getDisabledStatus(username);
		}
		LockOutFlag flag = lockOutCache.get(username);
		// If ID wan't found in cache, load it up and put it there.
		if (flag == null) {
			synchronized (username.intern()) {
				flag = lockOutCache.get(username);
				// If group wan't found in cache, load it up and put it there.
				if (flag == null) {
					flag = provider.getDisabledStatus(username);
					if (flag != null) lockOutCache.put(username, flag);
				}
			}
		}
		return flag;
	}

	/**
	 * Check if lockout flag is expired.
	 *
	 * @param flag
	 *            the flag
	 */
	private LockOutFlag getUnExpiredLockout(LockOutFlag flag) {
		if (flag != null) {
			Date curDate = new Date();
			if (flag.getEndTime() != null && curDate.after(flag.getEndTime())) {
				// Remove expired lockout entry
				lockOutCache.remove(flag.getUsername());
				provider.unsetDisabledStatus(flag.getUsername());
				return null;
			}
		}
		return flag;
	}

}
