/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.web.security.errorlogin;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.mail.MailService;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.log.ExoLogger;

/**
 * Service can be used to track invalid login attempts of users and do some actions when some number of successive login
 * attempts is detected.
 *
 */
public class InvalidLoginAttemptsService {
  private static final Log                                log        = ExoLogger.getLogger(InvalidLoginAttemptsService.class);

    /**
     * If false, then feature of sending mails to administrator about bad login attempts is disabled.
     */
    private final Boolean sendMailEnabled;

    /**
     * Number of successive invalid login attempts of user, after the mail will be send.
     */
    private final Integer numberOfFailedAttempts;

    /**
     * Policy for indication that invalid login attempts are coming from same source. Valid values are SESSION,
     * SESSION_AND_USER, SERVER. SESSION is default and it means that login attempts are coming from same HTTP session.
     * SESSION_AND_USER is indicating login attempts of same user and from same HTTP session. SERVER means login attempts from
     * same remote server.
     */
    private InvalidLoginPolicy invalidLoginPolicy;

    /**
     * This will be used as 'from' header in admin mail
     */
    private final String mailFrom;

    /**
     * This should be admin e-mail address, where e-mail about invalid login attempts will be send.
     */
    private final String mailTo;

    /**
     * Subject of email about invalid login attempts.
     */
    private final String mailSubject;

    /**
     * Content of mail message, which will be send to administrator. Real content will be based on value of parameter
     * "invalidLoginPolicy". Tokens like ${username}, ${sessionId}, ${hostname} will be replaced with real values from attacker.
     */
    private final String mailMessage;

    /**
     * MailService injected by exo kernel.
     */
    private final MailService mailService;

    /**
     * Helper map to track login attempts from different users.
     */
    private final ConcurrentMap<InvalidAttemptKey, Integer> attemptMap = new ConcurrentHashMap<InvalidAttemptKey, Integer>();

    public InvalidLoginAttemptsService(InitParams params, MailService mailService) {
        this.mailService = mailService;
        this.sendMailEnabled = Boolean.valueOf(params.getValueParam("sendingMailsEnabled").getValue());
        this.numberOfFailedAttempts = Integer.parseInt(params.getValueParam("numberOfFailedAttempts").getValue());
        this.invalidLoginPolicy = InvalidLoginPolicy.valueOf(params.getValueParam("invalidLoginPolicy").getValue());
        this.mailFrom = params.getValueParam("mailFrom").getValue();
        this.mailTo = params.getValueParam("mailTo").getValue();
        this.mailSubject = params.getValueParam("mailSubject").getValue();
        this.mailMessage = params.getPropertiesParam("mailMessage").getProperty(invalidLoginPolicy.toString());
    }

    /**
     * This should be called each time when invalid login attempt is detected (typically from HttpFilter)
     *
     * @param sessionId
     * @param username
     * @param hostname
     */
    public void badLoginAttempt(String sessionId, String username, String hostname) {
        if (log.isDebugEnabled()) {
            log.debug(new StringBuilder("Detected invalid login attempt. Session id=").append(sessionId).append(", username=")
                    .append(username).append(", hostname=").append(hostname));
        }
        if (numberOfFailedAttempts == 1) {
            sendMail(sessionId, username, hostname);
            return;
        }

        InvalidAttemptKey key = InvalidAttemptKey.createKey(invalidLoginPolicy, sessionId, username, hostname);

        // TODO: better synchronization
        int currentCount = 1;
        if (attemptMap.containsKey(key)) {
            currentCount = attemptMap.get(key) + 1;
            if (currentCount == numberOfFailedAttempts) {
                sendMail(sessionId, username, hostname);
                attemptMap.remove(key);
            } else {
                attemptMap.put(key, currentCount);
            }
        } else {
            attemptMap.put(key, currentCount);
        }
    }

    /**
     * This should be called each time successful login attempt is detected. We need to use it to clean previous bad attempts
     * when we have good attempt.
     *
     * @param sessionId
     * @param username
     * @param hostname
     */
    public void successfulLoginAttempt(String sessionId, String username, String hostname) {
        if (log.isDebugEnabled()) {
            log.debug(new StringBuilder("Detected successful login attempt. Session id=").append(sessionId)
                    .append(", username=").append(username).append(", hostname=").append(hostname));
        }
        InvalidAttemptKey key = InvalidAttemptKey.createKey(invalidLoginPolicy, sessionId, username, hostname);
        attemptMap.remove(key);
    }

    /**
     * Allows to set custom invalidLoginPolicy
     *
     * @param invalidLoginPolicy
     */
    public void setInvalidLoginPolicy(InvalidLoginPolicy invalidLoginPolicy) {
        this.invalidLoginPolicy = invalidLoginPolicy;
    }

    private void sendMail(String sessionId, String username, String hostname) {
        // return if sending mails disabled in configuration.
        if (!sendMailEnabled) {
            if (log.isDebugEnabled()) {
                log.debug("Sending of mails disabled. Mail won't be send about invalid login attempts.");
            }
            return;
        }

        // replace tokens from configuration with real values.
        String result = mailMessage.replaceAll("\\$\\{sessionId\\}", sessionId);
        result = result.replaceAll("\\$\\{username\\}", username);
        result = result.replaceAll("\\$\\{hostname\\}", hostname);
        result = result.replaceAll("\\$\\{number\\}", String.valueOf(numberOfFailedAttempts));

        try {
            if (log.isDebugEnabled()) {
                log.debug("Sending mail about the invalid login attempts. Mail message is: " + result);
            }
            mailService.sendMessage(mailFrom, mailTo, mailSubject, result);
        } catch (Exception e) {
            // log exception but not throw it when sending of mail happen
            log.error("Error when sending mail to admin after detected invalid number of login attempts.", e);
        }
    }

}
