/*
 * Copyright (c) 2022-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.dao;

import org.apache.commons.lang.StringUtils;
import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException;
import org.wso2.carbon.identity.application.common.model.InboundAuthenticationConfig;
import org.wso2.carbon.identity.application.common.model.InboundAuthenticationRequestConfig;
import org.wso2.carbon.identity.application.common.model.Property;
import org.wso2.carbon.identity.application.common.model.ServiceProvider;
import org.wso2.carbon.identity.application.mgt.AbstractInboundAuthenticatorConfig;
import org.wso2.carbon.identity.application.mgt.ApplicationMgtUtil;
import org.wso2.carbon.identity.core.migrate.MigrationClientException;
import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.is.migration.internal.ISMigrationServiceDataHolder;
import org.wso2.carbon.is.migration.service.v530.SQLConstants;
import org.wso2.carbon.is.migration.service.v530.util.JDBCPersistenceUtil;
import org.wso2.carbon.utils.DBUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ApplicationDAO {

    private static final String ADD_ACCESS_URL = "UPDATE SP_APP " +
            "SET ACCESS_URL = ? " +
            "WHERE APP_NAME= ? AND TENANT_ID= ? ";

    private static final String LOAD_APPLICATION_NAME_BY_CLIENT_ID_AND_TYPE = "SELECT APP_NAME, ACCESS_URL "
            + "FROM SP_APP INNER JOIN SP_INBOUND_AUTH "
            + "ON SP_APP.ID = SP_INBOUND_AUTH.APP_ID "
            + "WHERE INBOUND_AUTH_KEY = ? AND INBOUND_AUTH_TYPE = ? AND SP_APP.TENANT_ID = ? "
            + "AND SP_INBOUND_AUTH.TENANT_ID=?";

    private static final String LOAD_BASIC_APP_INFO_BY_APP_ID = "SELECT ID, TENANT_ID, APP_NAME, USER_STORE, " +
            "USERNAME, DESCRIPTION, ROLE_CLAIM, AUTH_TYPE, PROVISIONING_USERSTORE_DOMAIN, IS_LOCAL_CLAIM_DIALECT," +
            "IS_SEND_LOCAL_SUBJECT_ID, IS_SEND_AUTH_LIST_OF_IDPS, IS_USE_TENANT_DOMAIN_SUBJECT, " +
            "IS_USE_USER_DOMAIN_SUBJECT, ENABLE_AUTHORIZATION, " +
            "SUBJECT_CLAIM_URI, IS_SAAS_APP, UUID, IMAGE_URL, " +
            "ACCESS_URL, IS_DISCOVERABLE " +
            "FROM SP_APP WHERE ID = ?";

    private static final String LOAD_CLIENTS_INFO_BY_APP_ID = "SELECT INBOUND_AUTH_KEY, INBOUND_AUTH_TYPE, PROP_NAME, " +
            "PROP_VALUE,INBOUND_CONFIG_TYPE FROM  SP_INBOUND_AUTH WHERE APP_ID = ? AND TENANT_ID = ?";

    private static final String LOAD_APP_ID_BY_APP_NAME = "SELECT ID FROM SP_APP WHERE APP_NAME = ? AND TENANT_ID = ?";

    private static final String GET_APPLICATIONS_CONFIGURED_WITH_ROLE_CLAIM = "SELECT SP_APP.ID, SP_APP.APP_NAME " +
            "FROM SP_APP INNER JOIN SP_CLAIM_MAPPING ON SP_APP.ID = SP_CLAIM_MAPPING.APP_ID WHERE " +
            "SP_CLAIM_MAPPING.SP_CLAIM = 'http://wso2.org/claims/role' AND SP_APP.TENANT_ID = ?";

    private static final String INSERT_CLAIM = "INSERT INTO SP_CLAIM_MAPPING (TENANT_ID, IDP_CLAIM, SP_CLAIM, " +
            "APP_ID, IS_REQUESTED, IS_MANDATORY) VALUES (?, ?, ?, ?, 1, 0)";

    private static final String GET_EXISTING_CLAIMS = "SELECT IDP_CLAIM FROM SP_CLAIM_MAPPING WHERE APP_ID = ?";

    public static final String APP_ID = "ID";
    public static final String APP_NAME = "APP_NAME";

    public void updateAccessURL(Connection connection, String application, String accessURL, int
            tenantID)
            throws MigrationClientException {

        PreparedStatement prepStmt = null;
        try {
            boolean autoCommitStatus = connection.getAutoCommit();
            connection.setAutoCommit(false);
            String dbProductName = connection.getMetaData().getDatabaseProductName();
            prepStmt = connection.prepareStatement(ADD_ACCESS_URL,
                    new String[]{DBUtils.getConvertedAutoGeneratedColumnName
                            (dbProductName, SQLConstants.ID_COLUMN)});
            prepStmt.setString(1, accessURL);
            prepStmt.setString(2, application);
            prepStmt.setInt(3, tenantID);
            prepStmt.executeUpdate();
            JDBCPersistenceUtil.commitTransaction(connection);
            connection.setAutoCommit(autoCommitStatus);
        } catch (SQLException e) {
            JDBCPersistenceUtil.rollbackTransaction(connection);
            throw new MigrationClientException("Error while inserting AccessURL value.", e);
        } finally {
            IdentityDatabaseUtil.closeStatement(prepStmt);
        }
    }

    public ServiceProvider getServiceProviderByClientId(String clientId, String clientType,
                                                        String tenantDomain) throws IdentityApplicationManagementException {

        String applicationName = null;
        String accessURL = null;

        if (StringUtils.isEmpty(clientId)) {
            return null;
        }

        // client id can contain the @ to identify the tenant domain.
        if (clientId.contains("@")) {
            clientId = clientId.split("@")[0];
        }

        ServiceProvider serviceProvider = null;

        if (tenantDomain != null) {
            int tenantID = IdentityTenantUtil.getTenantId(tenantDomain);
            try (Connection connection = IdentityDatabaseUtil.getDBConnection(false);
                 PreparedStatement storeAppPrepStmt = connection.
                         prepareStatement(LOAD_APPLICATION_NAME_BY_CLIENT_ID_AND_TYPE)) {
                storeAppPrepStmt.setString(1, clientId);
                storeAppPrepStmt.setString(2, clientType);
                storeAppPrepStmt.setInt(3, tenantID);
                storeAppPrepStmt.setInt(4, tenantID);
                ResultSet appNameResult = storeAppPrepStmt.executeQuery();
                if (appNameResult.next()) {
                    applicationName = appNameResult.getString(1);
                    accessURL = appNameResult.getString(2);
                    int applicationID = getApplicationIDByName(applicationName, tenantID, connection);
                    InboundAuthenticationConfig inboundAuthenticationConfig = getInboundAuthenticationConfig(
                            applicationID, connection, tenantID);
                    serviceProvider = new ServiceProvider();
                    serviceProvider.setApplicationName(applicationName);
                    serviceProvider.setAccessUrl(accessURL);
                    serviceProvider.setInboundAuthenticationConfig(inboundAuthenticationConfig);
                }
            } catch (SQLException e) {
                throw new IdentityApplicationManagementException("Error while reading application", e);
            }
        }
        return serviceProvider;
    }

    private InboundAuthenticationConfig getInboundAuthenticationConfig(int applicationId, Connection connection, int
            tenantID) throws SQLException {

        Map<String, InboundAuthenticationRequestConfig> inboundAuthenticationRequestConfigMap =
                new HashMap<String, InboundAuthenticationRequestConfig>();
        try (PreparedStatement getClientInfo = connection.prepareStatement(LOAD_CLIENTS_INFO_BY_APP_ID)) {
            getClientInfo.setInt(1, applicationId);
            getClientInfo.setInt(2, tenantID);
            ResultSet resultSet = getClientInfo.executeQuery();
            while (resultSet.next()) {
                String authKey = resultSet.getString(1);
                //this is done to handle empty string added to oracle database as null.
                if (authKey == null) {
                    authKey = new String();
                }
                String authType = resultSet.getString(2);
                String propName = resultSet.getString(3);
                String propValue = resultSet.getString(4);
                String configType = resultSet.getString(5);

                String mapKey = authType + ":" + authKey;

                InboundAuthenticationRequestConfig inboundAuthRequest = null;
                if ((inboundAuthRequest = inboundAuthenticationRequestConfigMap.get(mapKey)) == null) {
                    inboundAuthRequest = new InboundAuthenticationRequestConfig();
                }
                inboundAuthRequest.setInboundAuthKey(authKey);
                inboundAuthRequest.setInboundAuthType(authType);
                inboundAuthRequest.setInboundConfigType(configType);

                boolean isCustomAuthenticator = isCustomInboundAuthType(authType);

                AbstractInboundAuthenticatorConfig customAuthenticator = ISMigrationServiceDataHolder
                        .getInboundAuthenticatorConfig(authType + ":" + configType);
                if (isCustomAuthenticator && customAuthenticator != null) {
                    inboundAuthRequest.setFriendlyName(customAuthenticator.getFriendlyName());
                }
                if (propName != null) {
                    Property prop = new Property();
                    prop.setName(propName);
                    prop.setValue(propValue);
                    if (isCustomAuthenticator && customAuthenticator != null) {
                        Property mappedProperty = getMappedProperty(customAuthenticator, propName);
                        if (mappedProperty != null) {
                            prop.setDisplayName(mappedProperty.getDisplayName());
                        }
                    }
                    inboundAuthRequest.setProperties((ApplicationMgtUtil.concatArrays(new Property[]{prop},
                            inboundAuthRequest.getProperties())));
                }
                inboundAuthenticationRequestConfigMap.put(mapKey, inboundAuthRequest);
            }
        }
        Map<String, AbstractInboundAuthenticatorConfig> allCustomAuthenticators = new HashMap<>
                (ISMigrationServiceDataHolder.getAllInboundAuthenticatorConfig());
        for (Map.Entry<String, InboundAuthenticationRequestConfig> entry : inboundAuthenticationRequestConfigMap
                .entrySet()) {
            InboundAuthenticationRequestConfig inboundAuthenticationRequestConfig = entry.getValue();
            AbstractInboundAuthenticatorConfig inboundAuthenticatorConfig = allCustomAuthenticators.remove
                    (inboundAuthenticationRequestConfig.getInboundAuthType() + ":" +
                            inboundAuthenticationRequestConfig.getInboundConfigType());
            if (inboundAuthenticatorConfig != null && inboundAuthenticationRequestConfig != null) {
                Property[] sources = inboundAuthenticatorConfig.getConfigurationProperties();
                Property[] destinations = inboundAuthenticationRequestConfig.getProperties();
                Map<String, Property> destinationMap = new HashMap<>();
                for (Property destination : destinations) {
                    destinationMap.put(destination.getName(), destination);
                }
                for (Property source : sources) {
                    Property property = destinationMap.get(source.getName());
                    if (property == null) {
                        if (isCustomInboundAuthType(inboundAuthenticationRequestConfig.getInboundAuthType())) {
                            if (inboundAuthenticatorConfig.isRelyingPartyKeyConfigured()) {
                                if (StringUtils.equals(inboundAuthenticatorConfig.getRelyingPartyKey(), source
                                        .getName())) {
                                    source.setValue(inboundAuthenticationRequestConfig.getInboundAuthKey());
                                }
                            }
                        }
                        destinationMap.put(source.getName(), source);
                    } else {
                        property.setConfidential(source.isConfidential());
                        property.setDefaultValue(source.getDefaultValue());
                        property.setAdvanced(source.isAdvanced());
                        property.setDescription(source.getDescription());
                        property.setDisplayOrder(source.getDisplayOrder());
                        property.setRequired(source.isRequired());
                        property.setType(source.getType());
                    }
                }

                inboundAuthenticationRequestConfig
                        .setProperties(destinationMap.values().toArray(new Property[destinationMap.size()]));
            }
        }
        List<InboundAuthenticationRequestConfig> returnList = new ArrayList<>(inboundAuthenticationRequestConfigMap
                .values());
        InboundAuthenticationConfig inboundAuthenticationConfig = new InboundAuthenticationConfig();
        inboundAuthenticationConfig.setInboundAuthenticationRequestConfigs(returnList.toArray(new
                InboundAuthenticationRequestConfig[returnList.size()]));
        return inboundAuthenticationConfig;
    }

    private Property getMappedProperty(AbstractInboundAuthenticatorConfig customAuthenticator, String propertyName) {

        Property property = null;
        if (customAuthenticator != null) {
            Property[] confProps = customAuthenticator.getConfigurationProperties();
            if (confProps != null) {
                for (Property confProp : confProps) {
                    if (propertyName != null && propertyName.equals(confProp.getName())) {
                        property = confProp;
                    }
                }
            }
        }
        return property;
    }

    private boolean isCustomInboundAuthType(String authType) {

        List<String> standardInboundAuthTypes = new ArrayList<String>();
        standardInboundAuthTypes.add("oauth2");
        standardInboundAuthTypes.add("wstrust");
        standardInboundAuthTypes.add("samlsso");
        standardInboundAuthTypes.add("openid");
        standardInboundAuthTypes.add("passivests");
        standardInboundAuthTypes.add("kerberos");

        return !standardInboundAuthTypes.contains(authType);
    }

    private int getApplicationIDByName(String applicationName, int tenantID,
                                       Connection connection) throws SQLException {

        int applicationId = 0;
        try (PreparedStatement getAppIDPrepStmt = connection.prepareStatement(LOAD_APP_ID_BY_APP_NAME)) {
            getAppIDPrepStmt.setString(1, applicationName);
            getAppIDPrepStmt.setInt(2, tenantID);
            ResultSet appidResult = getAppIDPrepStmt.executeQuery();
            if (!connection.getAutoCommit()) {
                connection.commit();
            }
            if (appidResult.next()) {
                applicationId = appidResult.getInt(1);
            }
        }
        return applicationId;
    }

    public List<ServiceProvider> getApplicationsConfiguredWithRoleClaim(int tenantId) throws MigrationClientException {

        try (Connection connection = IdentityDatabaseUtil.getDBConnection(false);
             PreparedStatement prepStmt = connection.prepareStatement(GET_APPLICATIONS_CONFIGURED_WITH_ROLE_CLAIM)) {

            prepStmt.setInt(1, tenantId);
            ResultSet resultSet = prepStmt.executeQuery();
            List<ServiceProvider> serviceProviders = new ArrayList<>();
            while (resultSet.next()) {
                ServiceProvider serviceProvider = new ServiceProvider();
                serviceProvider.setApplicationID(resultSet.getInt(APP_ID));
                serviceProvider.setApplicationName(resultSet.getString(APP_NAME));
                serviceProviders.add(serviceProvider);
            }
            return serviceProviders;
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving apps.", e);
        }
    }

    public void updateClaim(int tenantId, int appId, String claim) throws MigrationClientException {

        try (Connection connection = IdentityDatabaseUtil.getDBConnection(false)) {
            try (PreparedStatement preparedStatement = connection.prepareStatement(INSERT_CLAIM)) {
                preparedStatement.setInt(1, tenantId);
                preparedStatement.setString(2, claim);
                preparedStatement.setString(3, claim);
                preparedStatement.setInt(4, appId);
                preparedStatement.execute();
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while migrating role claim.", e);
        }
    }

    public List<String> getExistingClaims(int appId) throws MigrationClientException {

        try (Connection connection = IdentityDatabaseUtil.getDBConnection(false);
             PreparedStatement preparedStatement = connection.prepareStatement(GET_EXISTING_CLAIMS)) {

            preparedStatement.setInt(1, appId);
            ResultSet resultSet = preparedStatement.executeQuery();
            List<String> claims = new ArrayList<>();
            while (resultSet.next()) {
                claims.add(resultSet.getString(1));
            }
            return claims;
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving existing claims.", e);
        }
    }
}
