/*
 * Copyright (c) 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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.wso2.carbon.identity.mgt;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.identity.base.IdentityException;
import org.wso2.carbon.identity.mgt.constants.IdentityMgtConstants;
import org.wso2.carbon.identity.mgt.dto.ChallengeQuestionDTO;
import org.wso2.carbon.identity.mgt.dto.ChallengeQuestionIdsDTO;
import org.wso2.carbon.identity.mgt.dto.UserChallengesDTO;
import org.wso2.carbon.identity.mgt.internal.IdentityMgtServiceComponent;
import org.wso2.carbon.identity.mgt.util.Utils;
import org.wso2.carbon.registry.core.Collection;
import org.wso2.carbon.registry.core.Registry;
import org.wso2.carbon.registry.core.RegistryConstants;
import org.wso2.carbon.registry.core.Resource;
import org.wso2.carbon.registry.core.exceptions.RegistryException;
import org.wso2.carbon.user.core.UserCoreConstants;
import org.wso2.carbon.user.core.UserStoreException;
import org.wso2.carbon.user.core.service.RealmService;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * process user challenges and questions
 */
public class ChallengeQuestionProcessor {

    private static final Log log = LogFactory.getLog(ChallengeQuestionProcessor.class);

    /**
     * @return
     * @throws IdentityException
     */
    public List<ChallengeQuestionDTO> getAllChallengeQuestions() throws IdentityException {

        List<ChallengeQuestionDTO> questionDTOs = new ArrayList<ChallengeQuestionDTO>();
        try {
            int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
            Registry registry = IdentityMgtServiceComponent.getRegistryService().
                    getConfigSystemRegistry(tenantId);
            if (registry.resourceExists(IdentityMgtConstants.IDENTITY_MANAGEMENT_QUESTIONS)) {
                Collection collection = (Collection) registry.
                        get(IdentityMgtConstants.IDENTITY_MANAGEMENT_QUESTIONS);
                String[] children = collection.getChildren();
                for (String child : children) {
                    Resource resource = registry.get(child);
                    String question = resource.getProperty("question");
                    String isPromoteQuestion = resource.getProperty("isPromoteQuestion");
                    String questionSetId = resource.getProperty("questionSetId");
                    if (question != null) {
                        ChallengeQuestionDTO questionDTO = new ChallengeQuestionDTO();
                        questionDTO.setQuestion(question);
                        if (isPromoteQuestion != null) {
                            questionDTO.setPromoteQuestion(Boolean.parseBoolean(isPromoteQuestion));
                        }
                        if (questionSetId != null) {
                            questionDTO.setQuestionSetId(questionSetId);
                        }
                        questionDTO.setPromoteQuestion(false);
                        questionDTOs.add(questionDTO);
                    }
                }

            }
        } catch (RegistryException e) {
            throw IdentityException.error(e.getMessage(), e);
        }
        return questionDTOs;
    }

    /**
     * @param questionDTOs
     * @throws IdentityException
     */
    public void setChallengeQuestions(ChallengeQuestionDTO[] questionDTOs) throws IdentityException {
        Registry registry = null;
        try {
            int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
            registry = IdentityMgtServiceComponent.getRegistryService().getConfigSystemRegistry(tenantId);

            if (!registry.resourceExists(IdentityMgtConstants.IDENTITY_MANAGEMENT_PATH)) {
                Collection securityQuestionResource = registry.newCollection();
                registry.put(IdentityMgtConstants.IDENTITY_MANAGEMENT_PATH, securityQuestionResource);
            }
            Resource identityMgtResource = registry.get(IdentityMgtConstants.IDENTITY_MANAGEMENT_PATH);
            if (identityMgtResource != null) {
                String questionCollectionPath = IdentityMgtConstants.IDENTITY_MANAGEMENT_QUESTIONS;
                if (registry.resourceExists(questionCollectionPath)) {
                    registry.delete(questionCollectionPath);
                }

                Collection questionCollection = registry.newCollection();
                registry.put(questionCollectionPath, questionCollection);

                for (int i = 0; i < questionDTOs.length; i++) {
                    Resource resource = registry.newResource();
                    resource.addProperty("question", questionDTOs[i].getQuestion());
                    resource.addProperty("isPromoteQuestion", String.valueOf(questionDTOs[i].isPromoteQuestion()));
                    resource.addProperty("questionSetId", questionDTOs[i].getQuestionSetId());
                    registry.put(IdentityMgtConstants.IDENTITY_MANAGEMENT_QUESTIONS +
                            RegistryConstants.PATH_SEPARATOR + "question" + i +
                            RegistryConstants.PATH_SEPARATOR, resource);
                }
            }
        } catch (RegistryException e) {
            throw IdentityException.error("Error while setting challenge question.", e);
        }

    }

    /**
     * // TODO manage oder
     *
     * @param userName
     * @param tenantId
     * @param adminService
     * @return
     */
    public UserChallengesDTO[] getChallengeQuestionsOfUser(String userName, int tenantId,
                                                           boolean adminService) {

        List<UserChallengesDTO> challengesDTOs = new ArrayList<UserChallengesDTO>();
        try {
            if (log.isDebugEnabled()) {
                log.debug("Retrieving Challenge question from the user profile.");
            }
            List<String> challengesUris = getChallengeQuestionUris(userName, tenantId);

            for (int i = 0; i < challengesUris.size(); i++) {
                String challengesUri = challengesUris.get(i).trim();
                String challengeValue = Utils.getClaimFromUserStoreManager(userName,
                        tenantId, challengesUri);

                String[] challengeValues = challengeValue.
                        split(IdentityMgtConfig.getInstance().getChallengeQuestionSeparator());
                if (challengeValues != null && challengeValues.length == 2) {
                    UserChallengesDTO dto = new UserChallengesDTO();
                    dto.setId(challengesUri);
                    dto.setQuestion(challengeValues[0].trim());
                    if (adminService) {
                        dto.setAnswer(challengeValues[1].trim());
                    }
                    dto.setOrder(i);
                    dto.setPrimary(false);
                    challengesDTOs.add(dto);
                }
            }

        } catch (Exception e) {
            String msg = "No associated challenge question found for the user";
            if(log.isDebugEnabled()){
                log.debug(msg, e);
            }
        }

        if (!challengesDTOs.isEmpty()) {
            return challengesDTOs.toArray(new UserChallengesDTO[challengesDTOs.size()]);
        } else {
            return new UserChallengesDTO[0];
        }

    }

    /**
     * Retrieves challenge questions associated with the user.
     *
     * @param userName username of the user
     * @param tenantId tenant user belongs to
     * @return an array of UserChallengesDTO instances which holds the challenge questions
     * @throws IdentityException
     */
    public UserChallengesDTO[] getUserChallengeQuestions(String userName, int tenantId) throws
                                                                                        IdentityException {

        List<UserChallengesDTO> userChallengesDTOList = new ArrayList<>();

        try {
            if (log.isDebugEnabled()) {
                log.debug("Retrieving Challenge questions from the user profile.");
            }

            List<String> challengeQuestionUris = getChallengeQuestionUris(userName, tenantId);

            String[] challengeQuestionUriArray = new String[challengeQuestionUris.size()];
            Map<String, String> challengeQuestionClaimValues =
                    Utils.getClaimsFromUserStoreManager(userName, tenantId, challengeQuestionUris.toArray(
                            challengeQuestionUriArray));

            for (Map.Entry<String, String> challengeQuestionClaimValue : challengeQuestionClaimValues.entrySet()) {
                String[] challengeQuestionItems = challengeQuestionClaimValue.getValue().split(
                        IdentityMgtConfig.getInstance().getChallengeQuestionSeparator());

                UserChallengesDTO dto = new UserChallengesDTO();
                dto.setId(challengeQuestionClaimValue.getKey());
                dto.setQuestion(challengeQuestionItems[0]);
                userChallengesDTOList.add(dto);
            }

        } catch (Exception e) {
            throw IdentityException.error("No associated challenge questions found for the user.", e);
        }

        UserChallengesDTO[] userChallengesDTOs = new UserChallengesDTO[userChallengesDTOList.size()];
        return userChallengesDTOList.toArray(userChallengesDTOs);
    }

    public UserChallengesDTO getUserChallengeQuestion(String userName, int tenantId,
                                                      boolean adminService) throws IdentityMgtServiceException {

        UserChallengesDTO dto = null;

        List<UserChallengesDTO> challengesDTOs = new ArrayList<UserChallengesDTO>();
        try {
            if (log.isDebugEnabled()) {
                log.debug("Retrieving Challenge question from the user profile.");
            }
            List<String> challengesUris = getChallengeQuestionUris(userName, tenantId);

            for (int i = 0; i < challengesUris.size(); i++) {
                String challengesUri = challengesUris.get(i).trim();
                String challengeValue = Utils.getClaimFromUserStoreManager(userName, tenantId,
                        challengesUri);

                String[] challengeValues = challengeValue.split(IdentityMgtConfig.getInstance()
                        .getChallengeQuestionSeparator());
                if (challengeValues != null && challengeValues.length == 2) {
                    dto = new UserChallengesDTO();
                    dto.setId(challengesUri);
                    dto.setQuestion(challengeValues[0].trim());
                    if (adminService) {
                        dto.setAnswer(challengeValues[1].trim());
                    }
                    dto.setOrder(i);
                    dto.setPrimary(false);
                    challengesDTOs.add(dto);
                }
            }

        } catch (Exception e) {
            String msg = "No associated challenge question found for the user";
            if(log.isDebugEnabled()){
                log.debug(msg, e);
            }
        }

        return dto;
    }

    public UserChallengesDTO getUserChallengeQuestion(String userName, int tenantId,
                                                      String challengesUri) throws IdentityMgtServiceException {

        UserChallengesDTO dto = null;

        try {
            if (log.isDebugEnabled()) {
                log.debug("Retrieving Challenge question from the user profile.");
            }

            String challengeValue = Utils.getClaimFromUserStoreManager(userName, tenantId,
                    challengesUri);

            if (challengeValue != null) {

                String[] challengeValues = challengeValue.split(IdentityMgtConfig.getInstance()
                        .getChallengeQuestionSeparator());
                if (challengeValues != null && challengeValues.length == 2) {
                    dto = new UserChallengesDTO();
                    dto.setId(challengesUri);
                    dto.setQuestion(challengeValues[0].trim());

                }
            } else {
                dto = new UserChallengesDTO();
                dto.setError("Challenge questions have not been answered by the user: " + userName);
            }

        } catch (Exception e) {
            String errorMsg = "Error while getting the challenge questions for the user: "
                    + userName;
            if(log.isDebugEnabled()){
                log.debug(errorMsg, e);
            }
            dto = new UserChallengesDTO();
            dto.setError(errorMsg);
            throw new IdentityMgtServiceException(errorMsg,e);
        }

        return dto;

    }

    public ChallengeQuestionIdsDTO getUserChallengeQuestionIds(String userName, int tenantId)
            throws IdentityMgtServiceException {

        ChallengeQuestionIdsDTO dto = new ChallengeQuestionIdsDTO();

        if (log.isDebugEnabled()) {
            log.debug("Retrieving Challenge question ids from the user profile.");
        }
        List<String> challengesUris = getChallengeQuestionUris(userName, tenantId);

        if (challengesUris.isEmpty()) {
            String msg = "No associated challenge question found for the user";
            if (log.isDebugEnabled()) {
                log.debug(msg);
            }
            throw new IdentityMgtServiceException(msg);
        }

        String[] ids = new String[challengesUris.size()];

        for (int i = 0; i < challengesUris.size(); i++) {
            ids[i] = challengesUris.get(i).trim();
        }

        dto.setIds(ids);

        return dto;

    }

    /**
     * @param userName
     * @param tenantId
     * @return
     */
    public List<String> getChallengeQuestionUris(String userName, int tenantId) throws IdentityMgtServiceException{

        if (log.isDebugEnabled()) {
            log.debug("Challenge Question from the user profile.");
        }

        List<String> challenges = new ArrayList<String>();
        String claimValue = null;
        String[] challengesUris;

        try {
            claimValue = Utils.getClaimFromUserStoreManager(userName, tenantId,
                    "http://wso2.org/claims/challengeQuestionUris");
        } catch (IdentityException e) {
            throw new IdentityMgtServiceException("Error while getting cliams.", e);
        }

        if (claimValue != null) {
            if (claimValue.contains(IdentityMgtConfig.getInstance().getChallengeQuestionSeparator())) {
                challengesUris = claimValue.split(IdentityMgtConfig.getInstance().getChallengeQuestionSeparator());
            } else {
                challengesUris = new String[]{claimValue.trim()};
            }

            for (String challengesUri : challengesUris) {
                if (StringUtils.isNotBlank(challengesUri)) {
                    challenges.add(challengesUri.trim());
                }
            }
        }

        return challenges;
    }

    /**
     * @param userName
     * @param tenantId
     * @return
     */
    public int getNoOfChallengeQuestions(String userName, int tenantId) throws IdentityMgtServiceException {

        List<String> questions = getChallengeQuestionUris(userName, tenantId);
        return questions.size();
    }

    /**
     * @param userName
     * @param tenantId
     * @param challengesDTOs
     * @throws IdentityException
     */
    public void setChallengesOfUser(String userName, int tenantId,
                                    UserChallengesDTO[] challengesDTOs) throws IdentityException {
        try {
            if (log.isDebugEnabled()) {
                log.debug("Challenge Question from the user profile.");
            }
            List<String> challengesUris = new ArrayList<String>();
            String challengesUrisValue = "";
            String separator = IdentityMgtConfig.getInstance().getChallengeQuestionSeparator();
            Map<String,String> oldClaims = new HashMap<String,String>();
            Map<String,String> newClaims = new HashMap<String,String>();
            String[] requestclaims = new String[challengesDTOs.length];

            int x = 0;
            for(UserChallengesDTO claimDto: challengesDTOs) {
                requestclaims[x++] = claimDto.getId();
            }

            // Getting user store manager here to reduce the calls for claim retrieval.
            // TODO need to put into a new method in a new release version. Used to avoid API changes in patch.
            org.wso2.carbon.user.core.UserStoreManager userStoreManager = null;
            RealmService realmService = IdentityMgtServiceComponent.getRealmService();
            try {
                if (realmService.getTenantUserRealm(tenantId) != null) {
                    userStoreManager = (org.wso2.carbon.user.core.UserStoreManager) realmService
                            .getTenantUserRealm(tenantId).getUserStoreManager();
                }
            } catch (Exception e) {
                String msg = "Error retrieving the user store manager for the tenant";
                log.error(msg, e);
                throw IdentityException.error(msg, e);
            }

            if(userStoreManager != null) {
                oldClaims = userStoreManager.getUserClaimValues(userName, requestclaims, null);
            }

            if (!ArrayUtils.isEmpty(challengesDTOs)) {
                for (UserChallengesDTO dto : challengesDTOs) {
                    if (dto.getId() != null && dto.getQuestion() != null && dto.getAnswer() != null) {

                        String oldClaimValue = oldClaims.get(dto.getId());
                        if((oldClaimValue != null) && oldClaimValue.contains(separator)) {
                            String oldAnswer = oldClaimValue.split(separator)[1];
                            if (!oldAnswer.trim().equals(dto.getAnswer().trim())) {
                                String claimValue = dto.getQuestion().trim() + separator +
                                        Utils.doHash(dto.getAnswer().trim().toLowerCase());
                                if(!oldClaimValue.equals(claimValue)) {
                                    newClaims.put(dto.getId().trim(), claimValue);
                                }
                            }
                        } else {
                            String claimValue = dto.getQuestion().trim() + separator +
                                    Utils.doHash(dto.getAnswer().trim().toLowerCase());
                            newClaims.put(dto.getId().trim(), claimValue);
                        }
                        challengesUris.add(dto.getId().trim());
                    }
                }

                for (String challengesUri : challengesUris) {
                    if ("".equals(challengesUrisValue)) {
                        challengesUrisValue = challengesUri;
                    } else {
                        challengesUrisValue = challengesUrisValue +
                                IdentityMgtConfig.getInstance().getChallengeQuestionSeparator() + challengesUri;
                    }
                }

                newClaims.put("http://wso2.org/claims/challengeQuestionUris", challengesUrisValue);

                // Single call to save all challenge questions.
                userStoreManager.setUserClaimValues(userName, newClaims, UserCoreConstants.DEFAULT_PROFILE);

            }
        } catch (org.wso2.carbon.user.api.UserStoreException e) {
            String msg = "No associated challenge question found for the user";
            throw IdentityException.error(msg, e);
        }
    }

    /**
     * @param userName
     * @param tenantId
     * @param challengesDTOs
     * @return
     */
    public boolean verifyChallengeQuestion(String userName, int tenantId,
                                           UserChallengesDTO[] challengesDTOs) {

        boolean verification = false;
        try {
            if (log.isDebugEnabled()) {
                log.debug("Challenge Question from the user profile.");
            }

            UserChallengesDTO[] storedDto = getChallengeQuestionsOfUser(userName, tenantId, true);

            for (UserChallengesDTO challengesDTO : challengesDTOs) {
                if (challengesDTO.getAnswer() == null || challengesDTO.getAnswer().trim().length() < 1) {
                    return false;
                }

                for (UserChallengesDTO dto : storedDto) {
                    if ((challengesDTO.getId() == null || !challengesDTO.getId().trim().equals(dto.getId())) &&
                            (challengesDTO.getQuestion() == null || !challengesDTO.getQuestion().
                            trim().equals(dto.getQuestion()))) {
                        continue;

                    }

                    String hashedAnswer = Utils.doHash(challengesDTO.getAnswer().trim().toLowerCase());
                    if (hashedAnswer.equals(dto.getAnswer())) {
                        verification = true;
                    } else {
                        return false;
                    }
                }
            }
        } catch (Exception e) {
            String msg = "No associated challenge question found for the user";
            log.debug(msg, e);
        }

        return verification;
    }

    public boolean verifyUserChallengeAnswer(String userName, int tenantId,
                                             UserChallengesDTO challengesDTO) {

        boolean verification = false;
        try {
            if (log.isDebugEnabled()) {
                log.debug("Challenge Question from the user profile.");
            }

            UserChallengesDTO[] storedDto = getChallengeQuestionsOfUser(userName, tenantId, true);

            if (challengesDTO.getAnswer() == null || challengesDTO.getAnswer().trim().length() < 1) {
                return false;
            }

            for (UserChallengesDTO dto : storedDto) {

                if (dto.getId().equals(challengesDTO.getId())) {

                    String hashedAnswer = Utils.doHash(challengesDTO.getAnswer().trim()
                            .toLowerCase());
                    if (hashedAnswer.equals(dto.getAnswer())) {
                        verification = true;
                    } else {
                        return false;
                    }
                }

            }
        } catch (Exception e) {
            String msg = "No associated challenge question found for the user";
            log.debug(msg, e);
        }

        return verification;
    }

    /**
     * Verifies challenge question answers.
     *
     * @param userName username of the user
     * @param tenantId tenant user belongs to
     * @param userChallengesDTOs an array of UserChallengesDTO instances which holds the answer and the question id
     * @return true if answers are correct and false otherwise
     */
    public boolean verifyUserChallengeAnswers(String userName, int tenantId,
                                              UserChallengesDTO[] userChallengesDTOs) {

        boolean verification = true;
        try {

            if (log.isDebugEnabled()) {
                log.debug("verifying challenge question answers");
            }

            UserChallengesDTO[] storedUserChallengeDTOs = getChallengeQuestionsOfUser(userName, tenantId, true);

            int count = 0;
            for (UserChallengesDTO storedUserChallengesDTO : storedUserChallengeDTOs) {
                for (UserChallengesDTO receivedUserChallengesDTO : userChallengesDTOs) {
                    if (storedUserChallengesDTO.getId().equals(receivedUserChallengesDTO.getId())) {
                        count++;
                        String hashedAnswer = Utils.doHash(receivedUserChallengesDTO.getAnswer().trim().toLowerCase());
                        if (!hashedAnswer.equals(storedUserChallengesDTO.getAnswer())) {
                            verification = false;
                            break;
                        }
                    }
                }
            }

            if (verification) {
                verification = (storedUserChallengeDTOs.length == count);
            }
        } catch (Exception e) {
            log.error("Error while verifying challenge question answers", e);
            verification = false;
        }

        return verification;
    }

    /**
     * @param userName
     * @param tenantId
     * @return
     */
    public UserChallengesDTO[] getPrimaryChallengeQuestionsOfUser(String userName, int tenantId) {

        List<UserChallengesDTO> challengesDTOs = new ArrayList<>();
        try {
            if (log.isDebugEnabled()) {
                log.debug("Challenge Question from the user profile.");
            }
            String claimValue;

            claimValue = Utils.getClaimFromUserStoreManager(userName, tenantId,
                    "http://wso2.org/claims/primaryChallengeQuestion");

            String[] challenges = claimValue.split(IdentityMgtConfig.getInstance().getChallengeQuestionSeparator());
            for (String challenge : challenges) {
                UserChallengesDTO dto = new UserChallengesDTO();
                String question = challenge.substring(0, challenge.indexOf("="));
                dto.setQuestion(question);
                dto.setPrimary(true);
                challengesDTOs.add(dto);
            }

        } catch (Exception e) {
            String msg = "No associated challenge question found for the user";
            log.debug(msg, e);
        }

        if (!challengesDTOs.isEmpty()) {
            return challengesDTOs.toArray(new UserChallengesDTO[challengesDTOs.size()]);
        } else {
            return new UserChallengesDTO[0];
        }
    }

    /**
     * @param userName
     * @param tenantId
     * @param userChallengesDTOs
     * @return
     * @throws UserStoreException
     */
    public boolean verifyPrimaryChallengeQuestion(String userName, int tenantId,
                                                  UserChallengesDTO[] userChallengesDTOs) {

        boolean verification = false;
        try {
            if (log.isDebugEnabled()) {
                log.debug("Challenge Question from the user profile for user " + userName);
            }
            String claimValue = Utils.getClaimFromUserStoreManager(userName, tenantId,
                    "http://wso2.org/claims/primaryChallengeQuestion");

            if (claimValue == null) {
                log.debug("No associated challenge question found for the user " + userName);
                return false;
            }

            String[] challenges = claimValue.split(IdentityMgtConfig.getInstance().getChallengeQuestionSeparator());
            Map<String, String> challengeMap = new HashMap<String, String>();
            for (int i = 0; i < challenges.length; i = i + 2) {
                challengeMap.put(challenges[i], challenges[i + 1]);
            }

            for (UserChallengesDTO userChallengesDTO : userChallengesDTOs) {
                for (Map.Entry<String, String> entry : challengeMap.entrySet()) {
                    String challengeQuestion = entry.getKey();
                    if (challengeQuestion.equals(userChallengesDTO.getQuestion().trim())) {
                        String challengeAnswer = entry.getValue();
                        if (challengeAnswer.equals(Utils.
                                doHash(userChallengesDTO.getAnswer().trim().toLowerCase()))) {
                            verification = true;
                        } else {
                            return false;
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.debug("No associated challenge question found for the user " + userName, e);
        }

        return verification;
    }

}
