/*
 * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). All Rights Reserved.
 *
 * This software is the property of WSO2 LLC. and its suppliers, if any.
 * Dissemination of any information or reproduction of any material contained
 * herein in any form is strictly forbidden, unless permitted by WSO2 expressly.
 * You may not alter or remove any copyright or other notice from copies of this content.
 */

package org.wso2.carbon.is.migration.service.v700.service;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.identity.api.resource.mgt.constant.SQLConstants;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants;
import org.wso2.carbon.identity.core.migrate.MigrationClientException;
import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil;
import org.wso2.carbon.is.migration.internal.ISMigrationServiceDataHolder;
import org.wso2.carbon.is.migration.service.v700.dao.ApplicationDAO;
import org.wso2.carbon.is.migration.service.v700.model.Application;
import org.wso2.carbon.is.migration.service.v700.util.Utils;
import org.wso2.carbon.is.migration.util.Constant;
import org.wso2.carbon.is.migration.util.ReportUtil;
import org.wso2.carbon.user.api.AuthorizationManager;
import org.wso2.carbon.user.api.Tenant;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.user.core.util.UserCoreUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static org.wso2.carbon.is.migration.service.v700.constant.MigratorConstants.AUDIENCE_FOR_ASSOCIATED_ROLES;
import static org.wso2.carbon.is.migration.service.v700.constant.MigratorConstants.CLIENT_CREDENTIALS;
import static org.wso2.carbon.is.migration.service.v700.constant.MigratorConstants.IS_LEGACY_APP;
import static org.wso2.carbon.is.migration.service.v700.constant.MigratorConstants.ORGANIZATION;
import static org.wso2.carbon.is.migration.service.v700.constant.MigratorConstants.SUBSCRIBE_ALL;

public class ManagementAppAPIAuthorizationService {

    private static final Logger LOG = LoggerFactory.getLogger(ManagementAppAPIAuthorizationService.class);

    private final ApplicationDAO applicationDAO = new ApplicationDAO();
    private static final String GET_APP_DETAILS_OF_MANAGEMENT_APPS = "SELECT APP_NAME, USERNAME, USER_STORE, ID, UUID " +
            "FROM SP_APP WHERE SP_APP.ID NOT IN (SELECT SP_APP.ID AS ID FROM SP_APP " +
            "INNER JOIN SP_METADATA ON SP_APP.ID = SP_METADATA.SP_ID WHERE NAME = 'isManagementApp' AND " +
            "VALUE = 'false' AND SP_APP.TENANT_ID = ?) AND SP_APP.TENANT_ID = ?";
    private static final String GET_APP_DETAILS_OF_MANAGEMENT_APPS_H2 = "SELECT APP_NAME, USERNAME, USER_STORE, ID, UUID " +
            "FROM SP_APP WHERE SP_APP.ID NOT IN (SELECT SP_APP.ID AS ID FROM SP_APP " +
            "INNER JOIN SP_METADATA ON SP_APP.ID = SP_METADATA.SP_ID WHERE NAME = 'isManagementApp' AND " +
            "\"VALUE\" = 'false' AND SP_APP.TENANT_ID = ?) AND SP_APP.TENANT_ID = ?";
    private static final String GET_APP_ALLOWED_GRANT_TYPES = "SELECT GRANT_TYPES FROM IDN_OAUTH_CONSUMER_APPS WHERE " +
            "TENANT_ID = ? AND APP_NAME = ?";

    private static final String GET_MANAGEMENT_APPS_WITH_CC_AND_MORE_GRANTS = "SELECT SP_APP.APP_NAME, " +
            "SP_APP.TENANT_ID FROM SP_APP INNER JOIN IDN_OAUTH_CONSUMER_APPS " +
            "ON SP_APP.APP_NAME = IDN_OAUTH_CONSUMER_APPS.APP_NAME " +
            "WHERE SP_APP.TENANT_ID = IDN_OAUTH_CONSUMER_APPS.TENANT_ID AND " +
            "IDN_OAUTH_CONSUMER_APPS.GRANT_TYPES LIKE '%client_credentials%' AND " +
            "IDN_OAUTH_CONSUMER_APPS.GRANT_TYPES  != 'client_credentials' AND SP_APP.ID " +
            "NOT IN (SELECT SP_APP.ID AS ID FROM SP_APP INNER JOIN SP_METADATA ON SP_APP.ID = SP_METADATA.SP_ID " +
            "WHERE NAME = 'isManagementApp' AND VALUE = 'false')";
    private static final String GET_MANAGEMENT_APPS_WITH_CC_AND_MORE_GRANTS_H2 = "SELECT SP_APP.APP_NAME AS APP_NAME, " +
            "SP_APP.TENANT_ID AS TENANT_ID" +
            "FROM SP_APP INNER JOIN IDN_OAUTH_CONSUMER_APPS ON SP_APP.APP_NAME = IDN_OAUTH_CONSUMER_APPS.APP_NAME " +
            "WHERE SP_APP.TENANT_ID = IDN_OAUTH_CONSUMER_APPS.TENANT_ID AND " +
            "IDN_OAUTH_CONSUMER_APPS.GRANT_TYPES LIKE '%client_credentials%' AND " +
            "IDN_OAUTH_CONSUMER_APPS.GRANT_TYPES  != 'client_credentials' AND SP_APP.ID " +
            "NOT IN (SELECT SP_APP.ID AS ID FROM SP_APP INNER JOIN SP_METADATA ON SP_APP.ID = SP_METADATA.SP_ID " +
            "WHERE NAME = 'isManagementApp' AND \"VALUE\" = 'false')";

    public void dryRun(ReportUtil reportUtil) throws MigrationClientException {

        reportUtil.writeMessage("Checking for management applications with client_credentials and other grant " +
                "types enabled.");
        LOG.info(Constant.MIGRATION_LOG + "Checking for management applications with client_credentials and other " +
                "grant types enabled.");

        List<String> appList = getGetManagementAppsWithCcAndMoreGrants();
        if (!appList.isEmpty()) {
            reportUtil.writeMessage("The following management applications have enabled client_credentials and " +
                    "some other grant types");
            for (String appName : appList) {
                reportUtil.writeMessage(appName.split(":")[0] + " in tenant " + appName.split(":")[1]);
            }
            reportUtil.writeMessage("Take the following actions to resolve the issues");

            reportUtil.writeMessage("Option 1: Separate out the applications as one app for M2M use cases and another " +
                    "app to issue user tokens.");
            reportUtil.writeMessage("Option 2: Authorize all system APIs to the app. (This will cause a privilege " +
                    "escalation for the client_credentials grant type issuing tokens.)");
            reportUtil.writeMessage("Option 3: Authorize only app owner allowed system APIs to the app. (This might " +
                    "break existing flows of users who request tokens from this app for accessing system resource which " +
                    "were not subscribed to the app.");
            reportUtil.writeMessage("It is recommended to follow option 1 for the conflicting apps as much as possible." +
                    " For the remaining apps, option 2 or 3 has to be taken. By default the migration option 2 is " +
                    "considered by having subscribeAllAPIs: true in the migration-config.yml file. " +
                    "Set subscribeAllAPIs: false if required to take the option 3.");
        }
    }

    public void execute(boolean subscribeAllAPIs) throws MigrationClientException {

        try {
            Tenant[] tenants = ISMigrationServiceDataHolder.getRealmService().getTenantManager().getAllTenants();
            List<Tenant> tenantList = Arrays.stream(tenants).collect(Collectors.toList());
            Tenant superTenant = new Tenant();
            superTenant.setId(-1234);
            tenantList.add(superTenant);
            for (Tenant tenant: tenantList) {
                AuthorizationManager authorizationManager = ISMigrationServiceDataHolder.getRealmService()
                        .getTenantUserRealm(tenant.getId()).getAuthorizationManager();
                List<Application> managementApplicationList = getManagementApplicationDetails(tenant.getId());
                managementApplicationList = managementApplicationList.stream().filter(app ->
                        !FrameworkConstants.Application.MY_ACCOUNT_APP.equals(app.getAppName()) &&
                                !FrameworkConstants.Application.CONSOLE_APP.equals(app.getAppName()))
                        .collect(Collectors.toList());
                for (Application application : managementApplicationList) {
                    if (applicationDAO.isAppAuthorizedForAPIs(application.getUUID())) {
                        continue;
                    }

                    String[] allowedUIResourcesForUser =
                            authorizationManager.getAllowedUIResourcesForUser(application.getAppOwner(), "/");
                    List<String> allowedUIResourceList = Arrays.stream(allowedUIResourcesForUser).collect(Collectors.toList());
                    List<String> allowedGrants = Arrays.asList(application.getAllowedGrantTypes().split(" "));
                    // If CC grant is not enabled, no problem of authorizing all the APIs
                    if (!allowedGrants.contains(CLIENT_CREDENTIALS)) {
                        applicationDAO.authorizeAllSystemAPIsWithScopes(application.getUUID());
                        if (tenant.getId() == -1234) {
                            applicationDAO.authorizeSuperTenantSystemAPIsWithScopes(application.getUUID());
                        }
                    } else {
                        // If only CC grant is enabled, the only app owner allowed APIs can be authorized.
                        if (allowedGrants.size() == 1) {
                            if (allowedUIResourceList.contains("/permission/admin")) {
                                applicationDAO.authorizeAllSystemAPIsWithScopes(application.getUUID());
                                if (tenant.getId() == -1234) {
                                    applicationDAO.authorizeSuperTenantSystemAPIsWithScopes(application.getUUID());
                                }
                            } else {
                                authorizeAppOwnerAllowedAPIs(application.getUUID(), allowedUIResourceList);
                            }
                        } else {
                            // Here UX issue vs privilege escalation concern has to be handled as user consented.
                            if (subscribeAllAPIs) {
                                applicationDAO.authorizeAllSystemAPIsWithScopes(application.getUUID());
                                if (tenant.getId() == -1234) {
                                    applicationDAO.authorizeSuperTenantSystemAPIsWithScopes(application.getUUID());
                                }
                            } else {
                                if (allowedUIResourceList.contains("/permission/admin")) {
                                    applicationDAO.authorizeAllSystemAPIsWithScopes(application.getUUID());
                                    if (tenant.getId() == -1234) {
                                        applicationDAO.authorizeSuperTenantSystemAPIsWithScopes(application.getUUID());
                                    }
                                } else {
                                    authorizeAppOwnerAllowedAPIs(application.getUUID(), allowedUIResourceList);
                                }
                            }
                        }
                    }

                    if (!FrameworkConstants.Application.MY_ACCOUNT_APP.equals(application.getAppName()) &&
                            !FrameworkConstants.Application.CONSOLE_APP.equals(application.getAppName()) &&
                            applicationDAO.getApplicationProperty(application.getAppID(), "isLegacyApp") == null) {
                        applicationDAO.insertApplicationProperty(application.getAppID(), tenant.getId(), "isLegacyApp",
                                "true", null);
                    }
                }

                List<Application> userApps = applicationDAO.getApplicationsExceptSystemApps(tenant.getId());

                // Set organization audience for the apps without an audience.
                for (Application userApp : userApps) {
                    if (applicationDAO.getApplicationProperty(userApp.getAppID(), AUDIENCE_FOR_ASSOCIATED_ROLES) == null) {
                        applicationDAO.insertApplicationProperty(userApp.getAppID(), tenant.getId(), AUDIENCE_FOR_ASSOCIATED_ROLES, ORGANIZATION, null);
                    }
                    // Set is legacy app property for old applications.
                    if (applicationDAO.getApplicationProperty(userApp.getAppID(), IS_LEGACY_APP) == null) {
                        applicationDAO.insertApplicationProperty(userApp.getAppID(), tenant.getId(), IS_LEGACY_APP, "true", null);
                    }
                }
            }
        } catch (UserStoreException e) {
            throw new MigrationClientException("Error while migrating the management applications.");
        }
    }

    private void authorizeAppOwnerAllowedAPIs(String appUUID, List<String> allowedUIResourceList) throws MigrationClientException {

        Map<String, String> scopeBindingMap = Utils.loadOauthScopeBinding();
        if (scopeBindingMap == null) {
            throw new MigrationClientException("Error while loading scope binding configurations.");
        }
        List<String> scopesList = new ArrayList<>();
        for (Map.Entry<String, String> entry : scopeBindingMap.entrySet()) {
            for (String permissionString : allowedUIResourceList) {
                if (StringUtils.containsIgnoreCase(entry.getKey(), permissionString)) {
                    if (!scopesList.contains(entry.getValue())) {
                        scopesList.add(entry.getValue());
                    }
                }
            }
        }
        if (scopesList.isEmpty()) {
            return;
        }
        applicationDAO.authorizeSetOfSystemAPIsWithScopes(appUUID, scopesList);
    }

    private List<Application> getManagementApplicationDetails(int tenantId) throws MigrationClientException {

        List<Application> applicationList = new ArrayList<>();
        try (Connection connection = IdentityDatabaseUtil.getDBConnection(false)) {
            String databaseName = connection.getMetaData().getDatabaseProductName();
            String sql = databaseName.contains(SQLConstants.H2) ? GET_APP_DETAILS_OF_MANAGEMENT_APPS_H2 :
                    GET_APP_DETAILS_OF_MANAGEMENT_APPS;
            try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
                preparedStatement.setInt(1, tenantId);
                preparedStatement.setInt(2, tenantId);
                try (ResultSet resultSet = preparedStatement.executeQuery()) {
                    while (resultSet.next()) {
                        String appName = resultSet.getString("APP_NAME");
                        String userName = resultSet.getString("USERNAME");
                        String userStoreDomain = resultSet.getString("USER_STORE");
                        int appID = resultSet.getInt("ID");
                        String appUUID = resultSet.getString("UUID");
                        String allowedGrants = getAllowedGrantTypesOfApp(tenantId, appName);
                        Application application = new Application();
                        application.setAppName(appName);
                        application.setAppID(appID);
                        application.setUUID(appUUID);
                        application.setAppOwner(UserCoreUtil.addDomainToName(userName, userStoreDomain));
                        application.setAllowedGrantTypes(allowedGrants);
                        applicationList.add(application);
                    }
                }
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving service provider details.", e);
        }
        return applicationList;
    }

    private String getAllowedGrantTypesOfApp(int tenantID, String appName) throws MigrationClientException {

        try (Connection connection = IdentityDatabaseUtil.getDBConnection(false)) {
            try (PreparedStatement preparedStatement = connection.prepareStatement(GET_APP_ALLOWED_GRANT_TYPES)) {
                preparedStatement.setInt(1, tenantID);
                preparedStatement.setString(2, appName);
                try (ResultSet resultSet = preparedStatement.executeQuery()) {
                    if (resultSet.next()) {
                        return resultSet.getString("GRANT_TYPES");
                    }
                }
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving allowed grant types of the application.", e);
        }
        return StringUtils.EMPTY;
    }

    private List<String> getGetManagementAppsWithCcAndMoreGrants() throws MigrationClientException {

        List<String> appList = new ArrayList<>();
        try (Connection connection = IdentityDatabaseUtil.getDBConnection(false)) {
            String databaseName = connection.getMetaData().getDatabaseProductName();
            String sql =  databaseName.contains(SQLConstants.H2) ? GET_MANAGEMENT_APPS_WITH_CC_AND_MORE_GRANTS_H2 :
                    GET_MANAGEMENT_APPS_WITH_CC_AND_MORE_GRANTS;
            try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = preparedStatement.executeQuery()) {
                    while (resultSet.next()) {
                        appList.add(resultSet.getString("APP_NAME") + ":" + resultSet.getString("TENANT_ID"));
                    }
                }
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving allowed grant types of the application.", e);
        }
        return appList;
    }

}
