/*
 * 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.syncope.core.connid;

import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.syncope.common.types.PasswordPolicySpec;
import org.apache.syncope.core.persistence.beans.ExternalResource;
import org.apache.syncope.core.persistence.beans.PasswordPolicy;
import org.apache.syncope.core.persistence.beans.role.SyncopeRole;
import org.apache.syncope.core.persistence.beans.user.SyncopeUser;
import org.apache.syncope.core.persistence.dao.PolicyDAO;
import org.apache.syncope.core.policy.PolicyPattern;
import org.apache.syncope.core.util.InvalidPasswordPolicySpecException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Generate random passwords according to given policies.
 *
 * @see PasswordPolicy
 */
@Component
public class PasswordGenerator {

    private static final String[] SPECIAL_CHARS = {"", "!", "£", "%", "&", "(", ")", "?", "#", "_", "$"};

    @Autowired
    private PolicyDAO policyDAO;

    public String generate(final List<PasswordPolicySpec> ppSpecs)
            throws InvalidPasswordPolicySpecException {

        PasswordPolicySpec policySpec = merge(ppSpecs);

        check(policySpec);

        return generate(policySpec);
    }

    public String generate(final SyncopeUser user)
            throws InvalidPasswordPolicySpecException {

        List<PasswordPolicySpec> ppSpecs = new ArrayList<PasswordPolicySpec>();

        PasswordPolicy globalPP = policyDAO.getGlobalPasswordPolicy();
        if (globalPP != null && globalPP.getSpecification() != null) {
            ppSpecs.add(globalPP.<PasswordPolicySpec>getSpecification());
        }

        for (SyncopeRole role : user.getRoles()) {
            if (role.getPasswordPolicy() != null && role.getPasswordPolicy().getSpecification() != null) {
                ppSpecs.add(role.getPasswordPolicy().<PasswordPolicySpec>getSpecification());
            }
        }

        for (ExternalResource resource : user.getResources()) {
            if (resource.getPasswordPolicy() != null && resource.getPasswordPolicy().getSpecification() != null) {
                ppSpecs.add(resource.getPasswordPolicy().<PasswordPolicySpec>getSpecification());
            }
        }

        PasswordPolicySpec policySpec = merge(ppSpecs);
        check(policySpec);
        return generate(policySpec);
    }

    private PasswordPolicySpec merge(final List<PasswordPolicySpec> ppSpecs) {
        PasswordPolicySpec fpps = new PasswordPolicySpec();
        fpps.setMinLength(0);
        fpps.setMaxLength(1000);

        for (PasswordPolicySpec policySpec : ppSpecs) {
            if (policySpec.getMinLength() > fpps.getMinLength()) {
                fpps.setMinLength(policySpec.getMinLength());
            }

            if ((policySpec.getMaxLength() != 0) && ((policySpec.getMaxLength() < fpps.getMaxLength()))) {
                fpps.setMaxLength(policySpec.getMaxLength());
            }
            fpps.setPrefixesNotPermitted(policySpec.getPrefixesNotPermitted());
            fpps.setSuffixesNotPermitted(policySpec.getSuffixesNotPermitted());

            if (!fpps.isNonAlphanumericRequired()) {
                fpps.setNonAlphanumericRequired(policySpec.isNonAlphanumericRequired());
            }

            if (!fpps.isAlphanumericRequired()) {
                fpps.setAlphanumericRequired(policySpec.isAlphanumericRequired());
            }
            if (!fpps.isDigitRequired()) {
                fpps.setDigitRequired(policySpec.isDigitRequired());
            }

            if (!fpps.isLowercaseRequired()) {
                fpps.setLowercaseRequired(policySpec.isLowercaseRequired());
            }
            if (!fpps.isUppercaseRequired()) {
                fpps.setUppercaseRequired(policySpec.isUppercaseRequired());
            }
            if (!fpps.isMustStartWithDigit()) {
                fpps.setMustStartWithDigit(policySpec.isMustStartWithDigit());
            }
            if (!fpps.isMustntStartWithDigit()) {
                fpps.setMustntStartWithDigit(policySpec.isMustntStartWithDigit());
            }
            if (!fpps.isMustEndWithDigit()) {
                fpps.setMustEndWithDigit(policySpec.isMustEndWithDigit());
            }
            if (fpps.isMustntEndWithDigit()) {
                fpps.setMustntEndWithDigit(policySpec.isMustntEndWithDigit());
            }
            if (!fpps.isMustStartWithAlpha()) {
                fpps.setMustStartWithAlpha(policySpec.isMustStartWithAlpha());
            }
            if (!fpps.isMustntStartWithAlpha()) {
                fpps.setMustntStartWithAlpha(policySpec.isMustntStartWithAlpha());
            }
            if (!fpps.isMustStartWithNonAlpha()) {
                fpps.setMustStartWithNonAlpha(policySpec.isMustStartWithNonAlpha());
            }
            if (!fpps.isMustntStartWithNonAlpha()) {
                fpps.setMustntStartWithNonAlpha(policySpec.isMustntStartWithNonAlpha());
            }
            if (!fpps.isMustEndWithNonAlpha()) {
                fpps.setMustEndWithNonAlpha(policySpec.isMustEndWithNonAlpha());
            }
            if (!fpps.isMustntEndWithNonAlpha()) {
                fpps.setMustntEndWithNonAlpha(policySpec.isMustntEndWithNonAlpha());
            }
            if (!fpps.isMustEndWithAlpha()) {
                fpps.setMustEndWithAlpha(policySpec.isMustEndWithAlpha());
            }
            if (!fpps.isMustntEndWithAlpha()) {
                fpps.setMustntEndWithAlpha(policySpec.isMustntEndWithAlpha());
            }
        }
        return fpps;
    }

    private void check(final PasswordPolicySpec policySpec)
            throws InvalidPasswordPolicySpecException {

        if (policySpec.getMinLength() == 0) {
            throw new InvalidPasswordPolicySpecException("Minimum length is zero");
        }
        if (policySpec.isMustEndWithAlpha() && policySpec.isMustntEndWithAlpha()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustEndWithAlpha and mustntEndWithAlpha are both true");
        }
        if (policySpec.isMustEndWithAlpha() && policySpec.isMustEndWithDigit()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustEndWithAlpha and mustEndWithDigit are both true");
        }
        if (policySpec.isMustEndWithDigit() && policySpec.isMustntEndWithDigit()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustEndWithDigit and mustntEndWithDigit are both true");
        }
        if (policySpec.isMustEndWithNonAlpha() && policySpec.isMustntEndWithNonAlpha()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustEndWithNonAlpha and mustntEndWithNonAlpha are both true");
        }
        if (policySpec.isMustStartWithAlpha() && policySpec.isMustntStartWithAlpha()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustStartWithAlpha and mustntStartWithAlpha are both true");
        }
        if (policySpec.isMustStartWithAlpha() && policySpec.isMustStartWithDigit()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustStartWithAlpha and mustStartWithDigit are both true");
        }
        if (policySpec.isMustStartWithDigit() && policySpec.isMustntStartWithDigit()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustStartWithDigit and mustntStartWithDigit are both true");
        }
        if (policySpec.isMustStartWithNonAlpha() && policySpec.isMustntStartWithNonAlpha()) {
            throw new InvalidPasswordPolicySpecException(
                    "mustStartWithNonAlpha and mustntStartWithNonAlpha are both true");
        }
        if (policySpec.getMinLength() > policySpec.getMaxLength()) {
            throw new InvalidPasswordPolicySpecException("Minimun length (" + policySpec.getMinLength() + ")"
                    + "is greater than maximum length (" + policySpec.getMaxLength() + ")");
        }
    }

    private String generate(final PasswordPolicySpec policySpec) {
        String[] generatedPassword = new String[policySpec.getMinLength()];

        for (int i = 0; i < generatedPassword.length; i++) {
            generatedPassword[i] = "";
        }

        checkStartChar(generatedPassword, policySpec);

        checkEndChar(generatedPassword, policySpec);

        checkRequired(generatedPassword, policySpec);

        //filled empty chars
        for (int firstEmptyChar = firstEmptyChar(generatedPassword);
                firstEmptyChar < generatedPassword.length - 1; firstEmptyChar++) {
            generatedPassword[firstEmptyChar] = RandomStringUtils.randomAlphabetic(1);
        }

        checkPrefixAndSuffix(generatedPassword, policySpec);

        return StringUtils.join(generatedPassword);
    }

    private int randomNumber(final int range) {
        int randomNumber = (int) (Math.random() * (range - 1));
        return randomNumber == 0 ? 1 : randomNumber;
    }

    private void checkStartChar(final String[] generatedPassword, final PasswordPolicySpec policySpec) {
        if (policySpec.isMustStartWithAlpha()) {
            generatedPassword[0] = RandomStringUtils.randomAlphabetic(1);
        }
        if (policySpec.isMustStartWithNonAlpha() || policySpec.isMustStartWithDigit()) {
            generatedPassword[0] = RandomStringUtils.randomNumeric(1);
        }
        if (policySpec.isMustntStartWithAlpha()) {
            generatedPassword[0] = RandomStringUtils.randomNumeric(1);

        }
        if (policySpec.isMustntStartWithDigit()) {
            generatedPassword[0] = RandomStringUtils.randomAlphabetic(1);

        }
        if (policySpec.isMustntStartWithNonAlpha()) {
            generatedPassword[0] = RandomStringUtils.randomAlphabetic(1);

        }
    }

    private void checkEndChar(final String[] generatedPassword, final PasswordPolicySpec policySpec) {
        if (policySpec.isMustEndWithAlpha()) {
            generatedPassword[policySpec.getMinLength() - 1] = RandomStringUtils.randomAlphabetic(1);
        }
        if (policySpec.isMustEndWithNonAlpha() || policySpec.isMustEndWithDigit()) {
            generatedPassword[policySpec.getMinLength() - 1] = RandomStringUtils.randomNumeric(1);
        }

        if (policySpec.isMustntEndWithAlpha()) {
            generatedPassword[policySpec.getMinLength() - 1] = RandomStringUtils.randomNumeric(1);
        }
        if (policySpec.isMustntEndWithDigit()) {
            generatedPassword[policySpec.getMinLength() - 1] = RandomStringUtils.randomAlphabetic(1);
        }
        if (policySpec.isMustntEndWithNonAlpha()) {
            generatedPassword[policySpec.getMinLength() - 1] = RandomStringUtils.randomAlphabetic(1);

        }
    }

    private int firstEmptyChar(final String[] generatedPStrings) {
        int index = 0;
        while (!generatedPStrings[index].isEmpty()) {
            index++;
        }
        return index;
    }

    private void checkRequired(final String[] generatedPassword, final PasswordPolicySpec policySpec) {
        if (policySpec.isDigitRequired()
                && !PolicyPattern.DIGIT.matcher(StringUtils.join(generatedPassword)).matches()) {

            generatedPassword[firstEmptyChar(generatedPassword)] = RandomStringUtils.randomNumeric(1);
        }

        if (policySpec.isUppercaseRequired()
                && !PolicyPattern.ALPHA_UPPERCASE.matcher(StringUtils.join(generatedPassword)).matches()) {

            generatedPassword[firstEmptyChar(generatedPassword)] = RandomStringUtils.randomAlphabetic(1).toUpperCase();
        }

        if (policySpec.isLowercaseRequired()
                && !PolicyPattern.ALPHA_LOWERCASE.matcher(StringUtils.join(generatedPassword)).matches()) {

            generatedPassword[firstEmptyChar(generatedPassword)] = RandomStringUtils.randomAlphabetic(1).toLowerCase();
        }

        if (policySpec.isNonAlphanumericRequired()
                && !PolicyPattern.NON_ALPHANUMERIC.matcher(StringUtils.join(generatedPassword)).matches()) {

            generatedPassword[firstEmptyChar(generatedPassword)] =
                    SPECIAL_CHARS[randomNumber(SPECIAL_CHARS.length - 1)];
        }
    }

    private void checkPrefixAndSuffix(final String[] generatedPassword, final PasswordPolicySpec policySpec) {
        for (String prefix : policySpec.getPrefixesNotPermitted()) {
            if (StringUtils.join(generatedPassword).startsWith(prefix)) {
                checkStartChar(generatedPassword, policySpec);
            }
        }

        for (String suffix : policySpec.getSuffixesNotPermitted()) {
            if (StringUtils.join(generatedPassword).endsWith(suffix)) {
                checkEndChar(generatedPassword, policySpec);
            }
        }
    }
}
