/*
 * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com).
 *
 * WSO2 LLC. 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.wso2.carbon.is.migration.service.v600.migrator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.identity.application.common.model.ServiceProvider;
import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementServiceImpl;
import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException;
import org.wso2.carbon.identity.claim.metadata.mgt.model.ExternalClaim;
import org.wso2.carbon.identity.core.migrate.MigrationClientException;
import org.wso2.carbon.identity.oauth.dto.ScopeDTO;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.openidconnect.dao.ScopeClaimMappingDAOImpl;
import org.wso2.carbon.is.migration.service.Migrator;
import org.wso2.carbon.is.migration.service.v600.dao.ApplicationDAO;
import org.wso2.carbon.is.migration.service.v600.dao.IdentityProviderDAO;
import org.wso2.carbon.is.migration.service.v700.constant.MigratorConstants;
import org.wso2.carbon.is.migration.util.Constant;
import org.wso2.carbon.is.migration.util.ReportUtil;
import org.wso2.carbon.is.migration.util.UserStoreOperationsUtil;
import org.wso2.carbon.user.api.Tenant;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import static org.wso2.carbon.is.migration.util.Constant.REPORT_PATH;

public class RoleClaimMigrator extends Migrator {

    private static final Logger LOG = LoggerFactory.getLogger(RoleClaimMigrator.class);
    private static final String OIDC_CLAIM_DIALECT_URI = "http://wso2.org/oidc/claim";
    public static final String ROLES_CLAIM_NAME = "roles";
    public static final String OPENID = "openid";
    public static final String ROLES_CLAIM = "http://wso2.org/claims/roles";
    public static final String GROUPS_CLAIM = "http://wso2.org/claims/groups";

    private ApplicationDAO applicationDAO = new ApplicationDAO();
    private IdentityProviderDAO identityProviderDAO = new IdentityProviderDAO();

    @Override
    public void dryRun() throws MigrationClientException {

        try {
            LOG.info(Constant.MIGRATION_LOG + "Executing dry run for {}", this.getClass().getName());
            Properties migrationProperties = getMigratorConfig().getParameters();
            String reportPath = (String) migrationProperties.get(REPORT_PATH);
            ReportUtil reportUtil = new ReportUtil(reportPath);
            boolean appsExistWithRoleClaim = false;

            LOG.info(Constant.MIGRATION_LOG + "Started the dry run for {} migration.", this.getClass().getName());
            LOG.info(Constant.MIGRATION_LOG + "Checking for applications configured with the role claim.");

            Tenant[] tenants = UserStoreOperationsUtil.getAllTenants();

            reportUtil.writeMessage("\n--- Summary of the report ---\n");
            reportUtil.writeMessage(String.format("%40s | %40s", "Application", "Tenant Domain"));

            for (Tenant tenant : tenants) {
                List<ServiceProvider> apps = applicationDAO.getApplicationsConfiguredWithRoleClaim(tenant.getId());
                if (!apps.isEmpty()) {
                    appsExistWithRoleClaim = true;
                    apps.forEach(app -> reportUtil.writeMessage(String.format("%40s | %40s", app.getApplicationName(),
                            tenant.getDomain())));
                }
            }

            if (appsExistWithRoleClaim) {
                reportUtil.writeMessage(String.format("%40s | %40s", "---------", "---------"));
                reportUtil.writeMessage("\nApplications with the role claim configured as a requested claim exist" +
                        " in the system.");
                reportUtil.writeMessage("If the group role separation is enabled, execute the role claim migrator." +
                        "To handle the role claim being empty in tokens after the migration," +
                        " update the application logic to fallback on the roles and groups claims to retrieve" +
                        " the role and group values.");
            }

            reportUtil.commit();
        } catch (IOException e) {
            throw new MigrationClientException("Error while writing the dry run report.", e);
        }
    }

    @Override
    public void migrate() throws MigrationClientException {

        if(!(Boolean) getMigratorConfig().getParameters().get(MigratorConstants.MIGRATE_ROLE_CLAIM)) {
            return;
        }

        Set<Tenant> tenants = new HashSet<>(Arrays.asList(UserStoreOperationsUtil.getAllTenants()));
        migrateServiceProviderRoleClaim(tenants);
        migrateOIDCOpenIDScope(tenants);
        MigrateIdentityProviderRoleClaim();
    }

    private void MigrateIdentityProviderRoleClaim() throws MigrationClientException {

        LOG.info("Identity provider role claim migration started.");
        identityProviderDAO.migrateRoleClaimMapping();
        identityProviderDAO.migrateRoleClaim();
        LOG.info("Identity provider role claim migration completed.");
    }

    private void migrateServiceProviderRoleClaim(Set<Tenant> tenants) throws MigrationClientException {

        LOG.info("Service provider role claim migration started.");
        for (Tenant tenant : tenants) {

            List<ServiceProvider> applications = applicationDAO.getApplicationsConfiguredWithRoleClaim(tenant.getId());
            if (applications.isEmpty()) {
                LOG.info("Service provider role claim migration is not required for the tenant: " + tenant.getDomain());
                continue;
            }
            LOG.info(Constant.MIGRATION_LOG + "Migrating service provider role claim for the tenant: "
                    + tenant.getDomain());
            for (ServiceProvider application : applications) {
                List<String> existingClaims = applicationDAO.getExistingClaims(application.getApplicationID());
                if (!existingClaims.contains(ROLES_CLAIM)) {
                    applicationDAO.updateClaim(tenant.getId(), application.getApplicationID(), ROLES_CLAIM);
                }
                if (!existingClaims.contains(GROUPS_CLAIM)) {
                    applicationDAO.updateClaim(tenant.getId(), application.getApplicationID(), GROUPS_CLAIM);
                }
            }
        }
        LOG.info("Service provider role claim migration completed.");
    }

    private void migrateOIDCOpenIDScope(Set<Tenant> tenants) throws MigrationClientException {

        LOG.info(Constant.MIGRATION_LOG + "Started migrating OIDC roles scope in the super tenant.");
        try {
            for (Tenant tenant : tenants) {
                ClaimMetadataManagementServiceImpl claimMetadataManagementService =
                        new ClaimMetadataManagementServiceImpl();
                List<ExternalClaim> oidcClaims = claimMetadataManagementService
                        .getExternalClaims(OIDC_CLAIM_DIALECT_URI, tenant.getDomain());
                ExternalClaim roleClaim = oidcClaims.stream().filter(claim ->
                                ROLES_CLAIM_NAME.equalsIgnoreCase(claim.getClaimURI()))
                        .findAny().orElse(null);
                if (roleClaim == null) {
                    throw new MigrationClientException("Roles OIDC claim is not available.");
                }
                ScopeClaimMappingDAOImpl scopeClaimMappingDAO = new ScopeClaimMappingDAOImpl();
                ScopeDTO openidScopeDTO = scopeClaimMappingDAO.getScope(OPENID, tenant.getId());
                List<String> scopeClaimsList = new ArrayList<>(Arrays.asList(openidScopeDTO.getClaim()));
                if (!scopeClaimsList.contains(ROLES_CLAIM_NAME)) {
                    scopeClaimsList.add(ROLES_CLAIM_NAME);
                    openidScopeDTO.setClaim(scopeClaimsList.toArray(new String[0]));
                    scopeClaimMappingDAO.updateScope(openidScopeDTO, tenant.getId());
                }
            }
        } catch (IdentityOAuth2Exception | ClaimMetadataException | MigrationClientException e) {
            String message = Constant.MIGRATION_LOG + "Error while migrating oidc scopes";
            throw new MigrationClientException(message, e);
        }
    }
}
