package org.gcube.keycloak.oidc.avatar.importer;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.ArrayUtils;
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
import org.gcube.keycloak.oidc.avatar.importer.libravatar.Libravatar;
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarDefaultImage;
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarException;
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarOptions;
import org.jboss.logging.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
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.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.social.facebook.FacebookIdentityProviderFactory;
import org.keycloak.social.google.GoogleIdentityProviderFactory;
import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;

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

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

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

    public static final String[] PROVIDERS_WITH_SPECIFIC_CODE = {
            FacebookIdentityProviderFactory.PROVIDER_ID,
            LinkedInIdentityProviderFactory.PROVIDER_ID
    };

    public static final String[] COMPATIBLE_PROVIDERS = (String[]) ArrayUtils.addAll(PROVIDERS_WITH_PICTURE_CLAIM,
            PROVIDERS_WITH_SPECIFIC_CODE);

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

    public static final Integer DEFAULT_AVATAR_SIZE = 160;

    public static final String USE_LIBRAVATAR_PROPERTY = "use-libravatar";
    public static final String FORK_IMPORT_THREAD_PROPERTY = "import-with-thread";

    static {
        configProperties = ProviderConfigurationBuilder.create()
                .property().name(USE_LIBRAVATAR_PROPERTY)
                .label("Use Libravater service")
                .helpText("If the provider not provide the image claim, the email is used to search the avatar "
                        + "by using libravatar service.")
                .defaultValue(Boolean.TRUE)
                .type(ProviderConfigProperty.BOOLEAN_TYPE).add()
                .property().name(FORK_IMPORT_THREAD_PROPERTY)
                .label("Import avatar in a separate thread")
                .helpText("Import the avatar by forking a new thread and don't wait the end of import.")
                .defaultValue(Boolean.FALSE)
                .type(ProviderConfigProperty.BOOLEAN_TYPE).add()
                .build();
    }

    public static final String MAPPER_ID = "avatar-importer";

    @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 MAPPER_ID;
    }

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

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

    @Override
    public String getDisplayType() {
        return "Avatar importer";
    }

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

        logger.debug("Importing avatar for brokered user legacy");
        importNewUser(session, realm, user, mapperModel, context);
    }

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

        logger.debug("Importing avatar for brokered user");
        importNewUser(session, realm, user, mapperModel, context);
    }

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

        final boolean useLibravatar = Boolean.valueOf(mapperModel.getConfig().get(USE_LIBRAVATAR_PROPERTY));
        final boolean forkNewThread = Boolean.valueOf(mapperModel.getConfig().get(FORK_IMPORT_THREAD_PROPERTY));

        final String identityProviderAlias = mapperModel.getIdentityProviderAlias();

        Runnable importAvatarRunnable = new Runnable() {
            @Override
            public void run() {
                AvatarStorageProvider avatarStorageProvider = session.getProvider(AvatarStorageProvider.class);
                if (avatarStorageProvider == null) {
                    logger.warn("Cannot perform avatar import since the avatar storage provider is null");
                    return;
                }
                InputStream avatarInputStream = null;
                if (ArrayUtils.contains(AvatarImporter.PROVIDERS_WITH_PICTURE_CLAIM, identityProviderAlias)) {
                    String imageClaim = (String) getClaimValue(context, "picture");
                    if (imageClaim != null && !"".equals(imageClaim)) {
                        logger.infof("Getting avatar from token claim. Value is: %s", imageClaim);
                        try {
                            avatarInputStream = new URL(imageClaim).openStream();
                        } catch (IOException e) {
                            logger.info("Cannot load avatar image from claim: " + imageClaim, e);
                        }
                    }
                } else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(identityProviderAlias)) {
                    avatarInputStream = loadAvatarFromLinkedIn(context);
                } else if (FacebookIdentityProviderFactory.PROVIDER_ID.equals(identityProviderAlias)) {
                    avatarInputStream = loadAvatarFromFacebook(context);
                }
                if (avatarInputStream == null) {
                    if (useLibravatar) {
                        logger.debugf("Trying getting avatar from libravatar service");
                        String email = user.getEmail();
                        try {
                            avatarInputStream = Libravatar.from(email)
                                    .withOptions(
                                            new LibravatarOptions().withHttps().defaultingTo(LibravatarDefaultImage.NOT_FOUND)
                                                    .withImageSize(DEFAULT_AVATAR_SIZE))
                                    .download();
                        } catch (LibravatarException e) {
                            logger.infof("Avatar not found via libravatar for email: %s", email);
                        }
                    } else {
                        logger.debug("Skipped search on libravatar due to mapper configuration");
                    }
                }
    
                if (avatarInputStream != null) {
                    logger.debug("Saving the image via avatar storage provider");
                    avatarStorageProvider.saveAvatarImage(realm, user, avatarInputStream);
                } else {
                    logger.debugf("No avatar found for user: %s", user);
                }
            }
        };

        if (forkNewThread) {
            logger.debug("Forking new thread to perform the avatar import");
            new Thread(importAvatarRunnable, "oidc-avatar-import").start();
        } else {
            logger.debug("Performing the avatar import in the same thread");
            importAvatarRunnable.run();
        }
    }

    private InputStream loadAvatarFromLinkedIn(BrokeredIdentityContext context) {
        logger.info("Getting avatar from LinkedIn profile picture prjection");
        try {
            JSONObject displayImageProjection = (JSONObject) new JSONParser().parse(new InputStreamReader((new URL(
                    "https://api.linkedin.com/v2/me?projection=(profilePicture(displayImage~:playableStreams))&oauth2_access_token="
                            + getAccessTokenString(context)).openStream())));

            String imageURL = null;

            @SuppressWarnings("unchecked")
            Iterator<JSONObject> elementsIterator = ((JSONArray) ((JSONObject) ((JSONObject) displayImageProjection
                    .get("profilePicture")).get("displayImage~")).get("elements")).iterator();

            while (elementsIterator.hasNext()) {
                JSONObject element = elementsIterator.next();
                if ((Double) ((JSONObject) ((JSONObject) ((JSONObject) ((JSONObject) element).get("data"))
                        .get("com.linkedin.digitalmedia.mediaartifact.StillImage")).get("displaySize"))
                                .get("width") == 100.0) {

                    imageURL = (String) ((JSONObject) ((JSONArray) ((JSONObject) element).get("identifiers"))
                            .get(0)).get("identifier");
                }
            }
            if (imageURL != null) {
                logger.infof("Opening stream connnection to %s", imageURL);
                return new URL(imageURL).openStream();
            }
        } catch (IOException | ParseException e) {
            logger.info("Cannot load LinkedIn avatar image from projection", e);
        }
        return null;
    }

    protected InputStream loadAvatarFromFacebook(BrokeredIdentityContext context) {
        logger.info("Getting avatar from Facebook Graph API call");
        try {
            return new URL(String.format("https://graph.facebook.com/%s/picture?type=normal", context.getId()))
                    .openStream();
        } catch (IOException e) {
            logger.info("Cannot load Facebook avatar image from Graph API", e);
            return null;
        }
    }

    protected String getAccessTokenString(BrokeredIdentityContext context) {
        return (String) context.getContextData().get(KeycloakOIDCIdentityProvider.FEDERATED_ACCESS_TOKEN);
    }

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

    }

    @Override
    public String getHelpText() {
        return "Import the IdP avatar image or use the libravatr service to find it.";
    }

}
