/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed 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.keycloak.connections.mongo.updater.impl.updates;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import org.keycloak.Config;
import org.keycloak.connections.mongo.impl.types.MapMapper;
import org.keycloak.migration.MigrationProvider;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;

import java.util.List;
import java.util.Map;

/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class Update1_2_0_Beta1 extends Update {

    @Override
    public String getId() {
        return "1.2.0.Beta1";
    }

    @Override
    public void update(KeycloakSession session) {
        deleteEntries("clientSessions");
        deleteEntries("sessions");

        convertSocialToIdFedRealms();
        convertSocialToIdFedUsers();
        addAccessCodeLoginTimeout();
        addNewAdminRoles();
        addDefaultProtocolMappers(session);
    }


    private void convertSocialToIdFedRealms() {
        DBCollection realms = db.getCollection("realms");
        DBCursor realmsCursor = realms.find();

        try {
            while (realmsCursor.hasNext()) {
                BasicDBObject realm = (BasicDBObject) realmsCursor.next();
                boolean updateProfileOnInitialSocialLogin = realm.getBoolean("updateProfileOnInitialSocialLogin");
                BasicDBObject socialConfig = (BasicDBObject) realm.get("socialConfig");

                BasicDBList identityProviders = (BasicDBList) realm.get("identityProviders");
                if (identityProviders == null) {
                    identityProviders = new BasicDBList();
                    realm.put("identityProviders", identityProviders);
                }

                if (socialConfig != null) {
                    for (Map.Entry<String, Object> entry : socialConfig.entrySet()) {
                        if (entry.getKey().endsWith("###key")) {
                            String socialProviderId = entry.getKey().substring(0, entry.getKey().indexOf("###"));
                            String clientId = (String) entry.getValue();
                            String clientSecret = socialConfig.getString(socialProviderId + "###secret");

                            DBObject identityProviderConfig = new BasicDBObjectBuilder()
                                    .add("clientId", clientId)
                                    .add("clientSecret", clientSecret).get();

                            DBObject identityProvider = new BasicDBObjectBuilder()
                                    .add("internalId", KeycloakModelUtils.generateId())
                                    .add("providerId", socialProviderId)
                                    .add("alias", socialProviderId)
                                    .add("updateProfileFirstLogin", updateProfileOnInitialSocialLogin)
                                    .add("enabled", true)
                                    .add("storeToken", false)
                                    .add("authenticateByDefault", false)
                                    .add("config", identityProviderConfig).get();

                            identityProviders.add(identityProvider);
                            log.debugv("Converted social provider {0} to identity provider", socialProviderId);
                        }
                    }
                }

                // Remove obsolete keys from realm
                realm.remove("social");
                realm.remove("updateProfileOnInitialSocialLogin");
                realm.remove("socialConfig");

                // Update realm in DB now
                realms.save(realm);

                log.debugv("Social providers of realm {0} converted to identity providers", realm.get("_id"));
            }
        } finally {
            realmsCursor.close();
        }
    }

    private void convertSocialToIdFedUsers() {
        DBCollection users = db.getCollection("users");
        DBCursor usersCursor = users.find();

        try {
            while (usersCursor.hasNext()) {
                BasicDBObject user = (BasicDBObject) usersCursor.next();

                BasicDBList socialLinks = (BasicDBList) user.get("socialLinks");
                if (socialLinks != null) {
                    BasicDBList federatedIdentities = (BasicDBList) user.get("federatedIdentities");
                    if (federatedIdentities == null) {
                        federatedIdentities = new BasicDBList();
                        user.put("federatedIdentities", federatedIdentities);
                    }

                    for (Object socialLinkObj : socialLinks) {
                        BasicDBObject socialLink = (BasicDBObject) socialLinkObj;
                        BasicDBObject idFedLink = new BasicDBObject();
                        idFedLink.put("userName", socialLink.get("socialUsername"));
                        idFedLink.put("userId", socialLink.get("socialUserId"));
                        idFedLink.put("identityProvider", socialLink.get("socialProvider"));

                        federatedIdentities.add(idFedLink);
                    }

                    // Remove obsolete keys and save user
                    user.remove("socialLinks");
                    users.save(user);

                    if (log.isTraceEnabled()) {
                        log.tracev("Social links of user {0} converted to identity links", user.get("_id"));
                    }
                }
            }
        } finally {
            usersCursor.close();
        }

        log.debug("Social links of users converted to identity links");
    }

    private void addAccessCodeLoginTimeout() {
        DBCollection realms = db.getCollection("realms");
        DBCursor realmsCursor = realms.find();

        try {
            while (realmsCursor.hasNext()) {
                BasicDBObject realm = (BasicDBObject) realmsCursor.next();
                realm.put("accessCodeLifespanLogin", 1800);
                realms.save(realm);
            }
        } finally {
            realmsCursor.close();
        }
    }

    private void addNewAdminRoles() {
        DBCollection realms = db.getCollection("realms");
        String adminRealmName = Config.getAdminRealm();

        DBCursor realmsCursor = realms.find();
        try {
            while (realmsCursor.hasNext()) {
                BasicDBObject realm = (BasicDBObject) realmsCursor.next();
                if (adminRealmName.equals(realm.get("name"))) {
                    addNewAdminRolesToMasterRealm(realm);
                } else {
                    addNewAdminRolesToRealm(realm);
                }
            }
        } finally {
            realmsCursor.close();
        }
    }

    private void addNewAdminRolesToMasterRealm(BasicDBObject adminRealm) {
        DBCollection realms = db.getCollection("realms");
        DBCollection applications = db.getCollection("applications");
        DBCollection roles = db.getCollection("roles");

        DBCursor realmsCursor = realms.find();
        try {
            while (realmsCursor.hasNext()) {
                BasicDBObject currentRealm = (BasicDBObject) realmsCursor.next();
                String masterAdminAppName = currentRealm.getString("name") + "-realm";

                BasicDBObject masterAdminApp = (BasicDBObject) applications.findOne(new BasicDBObject().append("realmId", adminRealm.get("_id")).append("name", masterAdminAppName));

                String viewIdProvidersRoleId = insertApplicationRole(roles, AdminRoles.VIEW_IDENTITY_PROVIDERS, masterAdminApp.getString("_id"));
                String manageIdProvidersRoleId = insertApplicationRole(roles, AdminRoles.MANAGE_IDENTITY_PROVIDERS, masterAdminApp.getString("_id"));

                BasicDBObject adminRole = (BasicDBObject) roles.findOne(new BasicDBObject().append("realmId", adminRealm.get("_id")).append("name", AdminRoles.ADMIN));
                BasicDBList adminCompositeRoles = (BasicDBList) adminRole.get("compositeRoleIds");
                adminCompositeRoles.add(viewIdProvidersRoleId);
                adminCompositeRoles.add(manageIdProvidersRoleId);
                roles.save(adminRole);

                log.debugv("Added roles {0} and {1} to application {2}", AdminRoles.VIEW_IDENTITY_PROVIDERS, AdminRoles.MANAGE_IDENTITY_PROVIDERS, masterAdminAppName);
            }
        } finally {
            realmsCursor.close();
        }
    }

    private void addNewAdminRolesToRealm(BasicDBObject currentRealm) {
        DBCollection applications = db.getCollection("applications");
        DBCollection roles = db.getCollection("roles");

        BasicDBObject adminApp = (BasicDBObject) applications.findOne(new BasicDBObject().append("realmId", currentRealm.get("_id")).append("name", "realm-management"));

        String viewIdProvidersRoleId = insertApplicationRole(roles, AdminRoles.VIEW_IDENTITY_PROVIDERS, adminApp.getString("_id"));
        String manageIdProvidersRoleId = insertApplicationRole(roles, AdminRoles.MANAGE_IDENTITY_PROVIDERS, adminApp.getString("_id"));

        BasicDBObject adminRole = (BasicDBObject) roles.findOne(new BasicDBObject().append("applicationId", adminApp.get("_id")).append("name", AdminRoles.REALM_ADMIN));
        BasicDBList adminCompositeRoles = (BasicDBList) adminRole.get("compositeRoleIds");
        adminCompositeRoles.add(viewIdProvidersRoleId);
        adminCompositeRoles.add(manageIdProvidersRoleId);

        roles.save(adminRole);
        log.debugv("Added roles {0} and {1} to application realm-management of realm {2}", AdminRoles.VIEW_IDENTITY_PROVIDERS, AdminRoles.MANAGE_IDENTITY_PROVIDERS, currentRealm.get("name"));
    }

    private void addDefaultProtocolMappers(KeycloakSession session) {
        addDefaultMappers(session, db.getCollection("applications"));
        addDefaultMappers(session, db.getCollection("oauthClients"));
    }

    private void addDefaultMappers(KeycloakSession session, DBCollection clients) {
        DBCursor clientsCursor = clients.find();
        try {
            while (clientsCursor.hasNext()) {
                BasicDBObject currentClient = (BasicDBObject) clientsCursor.next();

                BasicDBList dbProtocolMappers = new BasicDBList();
                currentClient.put("protocolMappers", dbProtocolMappers);

                Object claimMask = currentClient.get("allowedClaimsMask");
                MigrationProvider migrationProvider = session.getProvider(MigrationProvider.class);
                List<ProtocolMapperRepresentation> protocolMappers = migrationProvider.getMappersForClaimMask((Long) claimMask);

                for (ProtocolMapperRepresentation protocolMapper : protocolMappers) {
                    BasicDBObject dbMapper = new BasicDBObject();
                    dbMapper.put("id", KeycloakModelUtils.generateId());
                    dbMapper.put("protocol", protocolMapper.getProtocol());
                    dbMapper.put("name", protocolMapper.getName());
                    dbMapper.put("consentRequired", protocolMapper.isConsentRequired());
                    dbMapper.put("consentText", protocolMapper.getConsentText());
                    dbMapper.put("protocolMapper", protocolMapper.getProtocolMapper());

                    Map<String, String> config = protocolMapper.getConfig();
                    BasicDBObject dbConfig = MapMapper.convertMap(config, null);
                    dbMapper.put("config", dbConfig);

                    dbProtocolMappers.add(dbMapper);
                }

                // Remove obsolete keys from client
                currentClient.remove("allowedClaimsMask");

                log.debugv("Added default mappers to application {1}", currentClient.get("name"));
                clients.save(currentClient);
            }
        } finally {
            clientsCursor.close();
        }
    }

    private String insertApplicationRole(DBCollection roles, String roleName, String applicationId) {
        BasicDBObject role = new BasicDBObject();
        String roleId = KeycloakModelUtils.generateId();
        role.append("_id", roleId);
        role.append("name", roleName);
        role.append("applicationId", applicationId);
        role.append("nameIndex", applicationId + "//" + roleName);
        roles.insert(role);
        return roleId;
    }

}
