/*
 * 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.jpa.updater.liquibase.custom;

import liquibase.exception.CustomChangeException;
import liquibase.exception.DatabaseException;
import liquibase.statement.core.InsertStatement;
import liquibase.statement.core.UpdateStatement;
import liquibase.structure.core.Table;
import org.keycloak.Config;
import org.keycloak.migration.MigrationProvider;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

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

    private String realmTableName;

    @Override
    protected String getTaskId() {
        return "Update 1.2.0.Beta1";
    }

    @Override
    protected void generateStatementsImpl() throws CustomChangeException {
        realmTableName = database.correctObjectName("REALM", Table.class);

        try {
            convertSocialToIdFedRealms();
            convertSocialToIdFedUsers();
            addAccessCodeLoginTimeout();
            addNewAdminRoles();
            addDefaultProtocolMappers();
        } catch (Exception e) {
            throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
        }
    }


    protected void convertSocialToIdFedRealms() throws SQLException, DatabaseException {
        String identityProviderTableName = database.correctObjectName("IDENTITY_PROVIDER", Table.class);
        String idpConfigTableName = database.correctObjectName("IDENTITY_PROVIDER_CONFIG", Table.class);

        String realmSocialConfigTable = getTableName("REALM_SOCIAL_CONFIG");
        String realmTableName = getTableName("REALM");
        PreparedStatement statement = jdbcConnection.prepareStatement("select RSC.NAME, VALUE, REALM_ID, UPDATE_PROFILE_ON_SOC_LOGIN from " + realmSocialConfigTable + " RSC," + realmTableName +
                " REALM where RSC.REALM_ID = REALM.ID ORDER BY RSC.REALM_ID, RSC.NAME");
        try {
            ResultSet resultSet = statement.executeQuery();
            try {
                boolean providerInProgress = false;
                String socialProviderId = null;
                String clientId = null;
                String clientSecret;
                String realmId = null;
                boolean updateProfileOnSocialLogin = false;
                boolean first = true;

                while (resultSet.next()) {
                    if (first) {
                        confirmationMessage.append("Migrating social to identity providers: ");
                        first = false;
                    }

                    if (!providerInProgress) {
                        String key = resultSet.getString("NAME");
                        int keyIndex = key.indexOf(".key");
                        if (keyIndex == -1) {
                            throw new IllegalStateException("Can't parse the provider from column: " + key);
                        }

                        socialProviderId = key.substring(0, keyIndex);
                        clientId = resultSet.getString("VALUE");
                        realmId = resultSet.getString("REALM_ID");
                        updateProfileOnSocialLogin = resultSet.getBoolean("UPDATE_PROFILE_ON_SOC_LOGIN");
                        providerInProgress = true;
                    } else {
                        clientSecret = resultSet.getString("VALUE");

                        String internalId = KeycloakModelUtils.generateId();
                        InsertStatement idpInsert = new InsertStatement(null, null, identityProviderTableName)
                                .addColumnValue("INTERNAL_ID", internalId)
                                .addColumnValue("ENABLED", true)
                                .addColumnValue("PROVIDER_ALIAS", socialProviderId)
                                .addColumnValue("PROVIDER_ID", socialProviderId)
                                .addColumnValue("UPDATE_PROFILE_FIRST_LOGIN", updateProfileOnSocialLogin)
                                .addColumnValue("STORE_TOKEN", false)
                                .addColumnValue("AUTHENTICATE_BY_DEFAULT", false)
                                .addColumnValue("REALM_ID", realmId);
                        InsertStatement clientIdInsert = new InsertStatement(null, null, idpConfigTableName)
                                .addColumnValue("IDENTITY_PROVIDER_ID", internalId)
                                .addColumnValue("NAME", "clientId")
                                .addColumnValue("VALUE", clientId);
                        InsertStatement clientSecretInsert = new InsertStatement(null, null, idpConfigTableName)
                                .addColumnValue("IDENTITY_PROVIDER_ID", internalId)
                                .addColumnValue("NAME", "clientSecret")
                                .addColumnValue("VALUE", clientSecret);

                        statements.add(idpInsert);
                        statements.add(clientIdInsert);
                        statements.add(clientSecretInsert);
                        confirmationMessage.append(socialProviderId + " in realm " + realmId + ", ");

                        providerInProgress = false;
                    }
                }

                // It means that some provider where processed
                if (!first) {
                    confirmationMessage.append(". ");
                }
            } finally {
                resultSet.close();
            }
        } finally {
            statement.close();
        }
    }

    protected void convertSocialToIdFedUsers() throws SQLException, DatabaseException {
        String federatedIdentityTableName = database.correctObjectName("FEDERATED_IDENTITY", Table.class);
        PreparedStatement statement = jdbcConnection.prepareStatement("select REALM_ID, USER_ID, SOCIAL_PROVIDER, SOCIAL_USER_ID, SOCIAL_USERNAME from " + getTableName("USER_SOCIAL_LINK"));
        try {
            ResultSet resultSet = statement.executeQuery();
            try {
                int count = 0;
                while (resultSet.next()) {
                    InsertStatement insert = new InsertStatement(null, null, federatedIdentityTableName)
                            .addColumnValue("REALM_ID", resultSet.getString("REALM_ID"))
                            .addColumnValue("USER_ID", resultSet.getString("USER_ID"))
                            .addColumnValue("IDENTITY_PROVIDER", resultSet.getString("SOCIAL_PROVIDER"))
                            .addColumnValue("FEDERATED_USER_ID", resultSet.getString("SOCIAL_USER_ID"))
                            .addColumnValue("FEDERATED_USERNAME", resultSet.getString("SOCIAL_USERNAME"));
                    count++;
                    statements.add(insert);
                }

                confirmationMessage.append("Updating " + count + " social links to federated identities. ");
            } finally {
                resultSet.close();
            }
        } finally {
            statement.close();
        }
    }

    protected void addAccessCodeLoginTimeout() {
        UpdateStatement statement = new UpdateStatement(null, null, realmTableName)
                .addNewColumnValue("LOGIN_LIFESPAN", 1800)
                .setWhereClause("LOGIN_LIFESPAN IS NULL");
        statements.add(statement);

        confirmationMessage.append("Updated LOGIN_LIFESPAN of all realms to 1800 seconds. ");
    }

    private void addNewAdminRoles() throws SQLException, DatabaseException{
        addNewMasterAdminRoles();
        addNewRealmAdminRoles();

        confirmationMessage.append("Adding new admin roles. ");
    }

    protected void addNewMasterAdminRoles() throws SQLException, DatabaseException {
        // Retrieve ID of admin role of master realm
        String adminRoleId = getAdminRoleId();
        String masterRealmId = Config.getAdminRealm();

        PreparedStatement statement = jdbcConnection.prepareStatement("select NAME from " + getTableName("REALM"));
        try {
            ResultSet resultSet = statement.executeQuery();
            try {
                while (resultSet.next()) {
                    String realmName = resultSet.getString("NAME");
                    String masterAdminAppName = realmName + "-realm";

                    PreparedStatement statement2 = jdbcConnection.prepareStatement("select ID from " + getTableName("CLIENT") + " where REALM_ID = ? AND NAME = ?");
                    statement2.setString(1, masterRealmId);
                    statement2.setString(2, masterAdminAppName);

                    try {
                        ResultSet resultSet2 = statement2.executeQuery();
                        try {
                            if (resultSet2.next()) {
                                String masterAdminAppId = resultSet2.getString("ID");

                                addAdminRole(AdminRoles.VIEW_IDENTITY_PROVIDERS, masterRealmId, masterAdminAppId, adminRoleId);
                                addAdminRole(AdminRoles.MANAGE_IDENTITY_PROVIDERS, masterRealmId, masterAdminAppId, adminRoleId);
                            } else {
                                throw new IllegalStateException("Couldn't find ID of '" + masterAdminAppName + "' application in 'master' realm. ");
                            }
                        } finally {
                            resultSet2.close();
                        }
                    } finally {
                        statement2.close();
                    }
                }
            } finally {
                resultSet.close();
            }
        } finally {
            statement.close();
        }
    }

    private String getAdminRoleId() throws SQLException, DatabaseException {
        PreparedStatement statement = jdbcConnection.prepareStatement("select ID from " + getTableName("KEYCLOAK_ROLE") + " where NAME = ? AND REALM = ?");
        statement.setString(1, AdminRoles.ADMIN);
        statement.setString(2, Config.getAdminRealm());

        try {
            ResultSet resultSet = statement.executeQuery();
            try {
                if (resultSet.next()) {
                    return resultSet.getString("ID");
                } else {
                    throw new IllegalStateException("Couldn't find ID of 'admin' role in 'master' realm");
                }
            } finally {
                resultSet.close();
            }
        } finally {
            statement.close();
        }
    }


    protected void addNewRealmAdminRoles() throws SQLException, DatabaseException {
        PreparedStatement statement = jdbcConnection.prepareStatement("select CLIENT.ID REALM_ADMIN_APP_ID, CLIENT.REALM_ID REALM_ID, KEYCLOAK_ROLE.ID ADMIN_ROLE_ID from " +
                getTableName("CLIENT") + " CLIENT," + getTableName("KEYCLOAK_ROLE") + " KEYCLOAK_ROLE where KEYCLOAK_ROLE.APPLICATION = CLIENT.ID AND CLIENT.NAME = 'realm-management' AND KEYCLOAK_ROLE.NAME = ?");
        statement.setString(1, AdminRoles.REALM_ADMIN);

        try {
            ResultSet resultSet = statement.executeQuery();
            try {

                while (resultSet.next()) {
                    String realmAdminAppId = resultSet.getString("REALM_ADMIN_APP_ID");
                    String realmId = resultSet.getString("REALM_ID");
                    String adminRoleId = resultSet.getString("ADMIN_ROLE_ID");

                    addAdminRole(AdminRoles.VIEW_IDENTITY_PROVIDERS, realmId, realmAdminAppId, adminRoleId);
                    addAdminRole(AdminRoles.MANAGE_IDENTITY_PROVIDERS, realmId, realmAdminAppId, adminRoleId);
                }
            } finally {
                resultSet.close();
            }
        } finally {
            statement.close();
        }
    }

    private void addAdminRole(String roleName, String realmId, String applicationId, String realmAdminAppRoleId) {
        String roleTableName = database.correctObjectName("KEYCLOAK_ROLE", Table.class);
        String compositeRoleTableName = database.correctObjectName("COMPOSITE_ROLE", Table.class);
        String newRoleId = KeycloakModelUtils.generateId();

        InsertStatement insertRole = new InsertStatement(null, null, roleTableName)
                .addColumnValue("ID", newRoleId)
                .addColumnValue("APP_REALM_CONSTRAINT", applicationId)
                .addColumnValue("APPLICATION_ROLE", true)
                .addColumnValue("NAME", roleName)
                .addColumnValue("REALM_ID", realmId)
                .addColumnValue("APPLICATION", applicationId);

        // Add newly created role to the composite roles of 'realm-admin' role
        InsertStatement insertCompRole = new InsertStatement(null, null, compositeRoleTableName)
                .addColumnValue("COMPOSITE", realmAdminAppRoleId)
                .addColumnValue("CHILD_ROLE", newRoleId);

        statements.add(insertRole);
        statements.add(insertCompRole);
    }

    protected void addDefaultProtocolMappers() throws SQLException, DatabaseException {
        String protocolMapperTableName = database.correctObjectName("PROTOCOL_MAPPER", Table.class);
        String protocolMapperCfgTableName = database.correctObjectName("PROTOCOL_MAPPER_CONFIG", Table.class);

        PreparedStatement statement = jdbcConnection.prepareStatement("select ID, NAME, ALLOWED_CLAIMS_MASK from " + getTableName("CLIENT"));

        try {
            ResultSet resultSet = statement.executeQuery();
            try {
                boolean first = true;
                while (resultSet.next()) {
                    if (first) {
                        confirmationMessage.append("Migrating claimsMask to protocol mappers for clients: ");
                        first = false;
                    }

                    Object acmObj = resultSet.getObject("ALLOWED_CLAIMS_MASK");
                    long mask = (acmObj != null) ? ((Number) acmObj).longValue() : ClaimMask.ALL;

                    MigrationProvider migrationProvider = this.kcSession.getProvider(MigrationProvider.class);
                    List<ProtocolMapperRepresentation> protocolMappers = migrationProvider.getMappersForClaimMask(mask);

                    for (ProtocolMapperRepresentation protocolMapper : protocolMappers) {
                        String mapperId = KeycloakModelUtils.generateId();

                        InsertStatement insert = new InsertStatement(null, null, protocolMapperTableName)
                                .addColumnValue("ID", mapperId)
                                .addColumnValue("PROTOCOL", protocolMapper.getProtocol())
                                .addColumnValue("NAME", protocolMapper.getName())
                                .addColumnValue("CONSENT_REQUIRED", false)
                                .addColumnValue("PROTOCOL_MAPPER_NAME", protocolMapper.getProtocolMapper())
                                .addColumnValue("CLIENT_ID", resultSet.getString("ID"));
                        statements.add(insert);

                        for (Map.Entry<String, String> cfgEntry : protocolMapper.getConfig().entrySet()) {
                            InsertStatement cfgInsert = new InsertStatement(null, null, protocolMapperCfgTableName)
                                    .addColumnValue("PROTOCOL_MAPPER_ID", mapperId)
                                    .addColumnValue("NAME", cfgEntry.getKey())
                                    .addColumnValue("VALUE", cfgEntry.getValue());
                            statements.add(cfgInsert);
                        }

                    }

                    confirmationMessage.append(resultSet.getString("NAME") + ", ");
                }

                // It means that some provider where processed
                if (!first) {
                    confirmationMessage.append(". ");
                }
            } finally {
                resultSet.close();
            }
        } finally {
            statement.close();
        }
    }
}
