package org.gcube.keycloak.broker.oidc.mappers;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.mappers.AbstractClaimMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory;
import org.keycloak.social.facebook.FacebookIdentityProviderFactory;
import org.keycloak.social.github.GitHubIdentityProviderFactory;
import org.keycloak.social.gitlab.GitLabIdentityProviderFactory;
import org.keycloak.social.google.GoogleIdentityProviderFactory;
import org.keycloak.social.instagram.InstagramIdentityProviderFactory;
import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
import org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory;
import org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory;
import org.keycloak.social.openshift.OpenshiftV4IdentityProviderFactory;
import org.keycloak.social.paypal.PayPalIdentityProviderFactory;
import org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory;
import org.keycloak.social.twitter.TwitterIdentityProviderFactory;

/**
 * @author <a href="mailto:mauro.mugnaini@nubisware.com">Mauro Mugnaini</a>
 */
public class UsernameFromMailMapper extends AbstractClaimMapper {

    private static final Logger logger = Logger.getLogger(UsernameFromMailMapper.class);

    private static final Character PERIOD = '.';
    private static final Character DASH = '-';
    private static final String CYRUS = "cyrus";
    private static final String POSTFIX = "postfix";
    private static final String COMMA = ",";

    public static final String[] COMPATIBLE_PROVIDERS = {
            OIDCIdentityProviderFactory.PROVIDER_ID,
            BitbucketIdentityProviderFactory.PROVIDER_ID,
            FacebookIdentityProviderFactory.PROVIDER_ID,
            KeycloakOIDCIdentityProviderFactory.PROVIDER_ID,
            GitHubIdentityProviderFactory.PROVIDER_ID,
            GitLabIdentityProviderFactory.PROVIDER_ID,
            GoogleIdentityProviderFactory.PROVIDER_ID,
            InstagramIdentityProviderFactory.PROVIDER_ID,
            LinkedInIdentityProviderFactory.PROVIDER_ID,
            MicrosoftIdentityProviderFactory.PROVIDER_ID,
            OpenshiftV3IdentityProviderFactory.PROVIDER_ID,
            OpenshiftV4IdentityProviderFactory.PROVIDER_ID,
            PayPalIdentityProviderFactory.PROVIDER_ID,
            StackoverflowIdentityProviderFactory.PROVIDER_ID,
            TwitterIdentityProviderFactory.PROVIDER_ID
    };

    private static final List<ProviderConfigProperty> configProperties;
    private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(
            Arrays.asList(IdentityProviderSyncMode.values()));

    public static final String RESERVED_USERNAMES = "reserved-usernames";
    public static final String AUTO_RESOLVE_CONFLICT = "auto-resolve";

    static {
        configProperties = ProviderConfigurationBuilder.create().property().name(RESERVED_USERNAMES)
                .label("Reserved Usernames")
                .helpText("List of reserved usernames (comma separated) that cannot be accepted. "
                        + "If found a progressive suffix number will we added.")
                .type(ProviderConfigProperty.STRING_TYPE).defaultValue(CYRUS + COMMA + POSTFIX)
                .add().property().name(AUTO_RESOLVE_CONFLICT).label("Auto resolve conflicts")
                .helpText("Automatically add a numeric suffix to avoid already existing username conflict.")
                .type(ProviderConfigProperty.BOOLEAN_TYPE).add().build();
    }

    public static final String PROVIDER_ID = "username-from-idp-email-mapper";

    @Override
    public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
        return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configProperties;
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public String[] getCompatibleProviders() {
        return COMPATIBLE_PROVIDERS;
    }

    @Override
    public String getDisplayCategory() {
        return "Preprocessor";
    }

    @Override
    public String getDisplayType() {
        return "Username from IdP email importer";
    }

    @Override
    public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user,
            IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
    }

    @Override
    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user,
            IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {

        // preprocessFederatedIdentity gets called anyways, so we only need to set the username if necessary.
        // However, we don't want to set the username when the email is used as username
        if (!realm.isRegistrationEmailAsUsername()) {
            user.setUsername(context.getModelUsername());
        }
    }

    @Override
    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm,
            IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {

        Set<String> reservedUsernames = getConfigValuesOrEmptySetIfNullOrEmptyString(
                mapperModel.getConfig().get(RESERVED_USERNAMES));

        logger.debugf("Reserved usernames are: %s", reservedUsernames);

        boolean autoResolveConflicts = Boolean.valueOf(mapperModel.getConfig().get(AUTO_RESOLVE_CONFLICT));
        logger.debugf("Auto resolve conflict setting is: %b", autoResolveConflicts);

        String email = context.getEmail();
        logger.debugf("Email address is: %s", email);

        String username = email.substring(0, email.indexOf('@')).toLowerCase();
        logger.debugf("Extracted raw username is: %s", username);

        for (Character c : username.chars().mapToObj(e -> (char) e).collect(Collectors.toSet())) {
            if (!isChar(c) && !isDigit(c) && (c != DASH) && (c != PERIOD)) {
                logger.debugf("Replacing unneeded char (%c) with %c", c, PERIOD);
                username = username.replace(c, PERIOD);
            }
        }

        boolean usernameAlreadyExists = usernameAlreadyExists(session, realm, username);
        if ((usernameAlreadyExists && autoResolveConflicts)
                || reservedUsernames.contains(username)) {

            if (usernameAlreadyExists) {
                logger.debugf("Username already exists: %s", username);
            } else {
                logger.info("Username is one of the reserved usernames");
            }

            for (int i = 1;; i++) {
                String tempUsername = username + PERIOD + i;
                logger.tracef("Trying with username: %s", tempUsername);
                if (usernameAlreadyExists(session, realm, tempUsername)) {
                    logger.tracef("Username already exists: %s", tempUsername);
                } else {
                    logger.tracef("Username is OK: %s", tempUsername);
                    username = tempUsername;
                    break;
                }
            }
        }
        logger.debugf("Final computed username is: %s", username);
        context.setModelUsername(username);
    }

    private static boolean isChar(char c) {
        return (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')));
    }

    private static boolean isDigit(char c) {
        return ((c >= '0') && (c <= '9'));
    }

    private boolean usernameAlreadyExists(KeycloakSession session, RealmModel realm, String username) {
        return KeycloakModelUtils.findUserByNameOrEmail(session, realm, username) != null;
    }

    @Override
    public String getHelpText() {
        return "Extract the IdP username from the e-mail address (before the '@' char).";
    }

    protected Set<String> getConfigValuesOrEmptySetIfNullOrEmptyString(String str) {
        if (str == null || "".equals(str)) {
            return Collections.emptySet();
        } else {
            String[] objClasses = str.split(COMMA);
            Set<String> trimmed = new HashSet<>();
            for (String objectClass : objClasses) {
                objectClass = objectClass.trim();
                if (objectClass.length() > 0) {
                    trimmed.add(objectClass);
                }
            }
            return trimmed;
        }
    }

}
