/*
 * Copyright (C) 2004-2009 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.admin;

import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;

import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.TaskEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handles recording admin console login attempts and handling temporary lockouts where necessary.
 *
 * @author Daniel Henninger
 */
public class LoginLimitManager {

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

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

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

    // Max number of attempts per ip address that can be performed in given time frame
    private long maxAttemptsPerIP;
    // Time frame before attempts per ip addresses are reset
    private long millisecondsBetweenPerIP;

    // Max number of attempts per username that can be performed in a given time frame
    private long maxAttemptsPerUsername;
    // Time frame before attempts per username are reset
    private long millisecondsBetweenPerUsername;

    // Record of attempts per IP address
    private Map<String,Long> attemptsPerIP;
    // Record of attempts per username
    private Map<String,Long> attemptsPerUsername;

    /**
     * Constructs a new login limit manager.
     */
    private LoginLimitManager() {
        // Set up initial maps
        attemptsPerIP = new ConcurrentHashMap<>();
        attemptsPerUsername = new ConcurrentHashMap<>();

        // Max number of attempts per ip address that can be performed in given time frame (10 attempts default)
        maxAttemptsPerIP = JiveGlobals.getLongProperty("adminConsole.maxAttemptsPerIP", 10);
        // Time frame before attempts per ip addresses are reset (15 minutes default)
        millisecondsBetweenPerIP = JiveGlobals.getLongProperty("adminConsole.perIPAttemptResetInterval", 900000);
        // Max number of attempts per username that can be performed in a given time frame (10 attempts default)
        maxAttemptsPerUsername = JiveGlobals.getLongProperty("adminConsole.maxAttemptsPerUsername", 10);
        // Time frame before attempts per ip addresses are reset (15 minutes default)
        millisecondsBetweenPerUsername = JiveGlobals.getLongProperty("adminConsole.perUsernameAttemptResetInterval", 900000);
        // Set up per username attempt reset task
        TaskEngine.getInstance().scheduleAtFixedRate(new PerUsernameTask(), 0, millisecondsBetweenPerUsername);
        // Set up per IP attempt reset task
        TaskEngine.getInstance().scheduleAtFixedRate(new PerIPAddressTask(), 0, millisecondsBetweenPerIP);
    }

    /**
     * Returns true of the entered username or connecting IP address has hit it's attempt limit.
     *
     * @param username Username being checked.
     * @param address IP address that is connecting.
     * @return True if the login attempt limit has been hit.
     */
    public boolean hasHitConnectionLimit(String username, String address) {
        if (attemptsPerIP.get(address) != null && attemptsPerIP.get(address) > maxAttemptsPerIP) {
            return true;
        }
        if (attemptsPerUsername.get(username) != null && attemptsPerUsername.get(username) > maxAttemptsPerUsername) {
            return true;
        }
        // No problem then, no limit hit.
        return false;
    }

    /**
     * Records a failed connection attempt.
     *
     * @param username Username being attempted.
     * @param address IP address that is attempting.
     */
    public void recordFailedAttempt(String username, String address) {
        Log.warn("Failed admin console login attempt by "+username+" from "+address);

        Long cnt = (long)0;
        if (attemptsPerIP.get(address) != null) {
            cnt = attemptsPerIP.get(address);
        }
        cnt++;
        attemptsPerIP.put(address, cnt);
        if (cnt > maxAttemptsPerIP) {
            Log.warn("Login attempt limit breeched for address "+address);
        }

        cnt = (long)0;
        if (attemptsPerUsername.get(username) != null) {
            cnt = attemptsPerUsername.get(username);
        }
        cnt++;
        attemptsPerUsername.put(username, cnt);
        if (cnt > maxAttemptsPerUsername) {
            Log.warn("Login attempt limit breeched for username "+username);
        }
    }

    /**
     * Clears failed login attempts if a success occurs.
     *
     * @param username Username being attempted.
     * @param address IP address that is attempting.
     */
    public void recordSuccessfulAttempt(String username, String address) {
        attemptsPerIP.remove(address);
        attemptsPerUsername.remove(username);
    }

    /**
     * Runs at configured interval to clear out attempts per username, thereby wiping lockouts.
     */
    private class PerUsernameTask extends TimerTask {

        /**
         * Wipes failed attempt list for usernames.
         */
        @Override
        public void run() {
            attemptsPerUsername.clear();
        }
    }

    /**
     * Runs at configured interval to clear out attempts per ip address, thereby wiping lockouts.
     */
    private class PerIPAddressTask extends TimerTask {

        /**
         * Wipes failed attempt list for ip addresses.
         */
        @Override
        public void run() {
            attemptsPerIP.clear();
        }
    }

}
