/*
 * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * 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.wso2.carbon.is.migration.service.v530.dao;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.identity.claim.metadata.mgt.dao.ClaimDialectDAO;
import org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimConstants;
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.identity.core.util.IdentityUtil;
import org.wso2.carbon.is.migration.service.v530.SQLConstants;
import org.wso2.carbon.is.migration.service.v530.bean.Claim;
import org.wso2.carbon.is.migration.service.v530.bean.MappedAttribute;
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;
import java.util.Random;

/**
 * Claim DAO.
 */
public class ClaimDAO {

    private static Logger log = LoggerFactory.getLogger(ClaimDialectDAO.class);

    private static ClaimDAO claimDAO = new ClaimDAO();

    private ClaimDAO() {

    }

    public static ClaimDAO getInstance() {

        return claimDAO;
    }

    /**
     * Add claim dialect.
     *
     * @param claimDialectURI
     * @param tenantId
     * @throws MigrationClientException
     */
    public void addClaimDialect(String claimDialectURI, int tenantId) throws MigrationClientException {

        Connection connection = null;
        PreparedStatement prepStmt = null;

        String query = SQLConstants.ADD_CLAIM_DIALECT;
        try {
            connection = IdentityDatabaseUtil.getDBConnection();
            connection.setAutoCommit(false);
            prepStmt = connection.prepareStatement(query);
            prepStmt.setString(1, claimDialectURI);
            prepStmt.setInt(2, tenantId);
            prepStmt.executeUpdate();
            JDBCPersistenceUtil.commitTransaction(connection);
        } catch (SQLException e) {
            JDBCPersistenceUtil.rollbackTransaction(connection);
            throw new MigrationClientException("Error while adding claim dialect " + claimDialectURI, e);
        } finally {
            IdentityDatabaseUtil.closeStatement(prepStmt);
            IdentityDatabaseUtil.closeConnection(connection);
        }
    }

    /**
     * Get claim dialect.
     *
     * @param claimDialectURI
     * @param tenantId
     * @return
     * @throws MigrationClientException
     */
    public int getClaimDialect(String claimDialectURI, int tenantId) throws MigrationClientException {

        Connection connection = IdentityDatabaseUtil.getDBConnection();
        PreparedStatement prepStmt = null;

        String getQuery = SQLConstants.GET_CLAIM_DIALECT;
        ResultSet rs;
        int id = 0;

        try {
            prepStmt = connection.prepareStatement(getQuery);
            prepStmt.setString(1, claimDialectURI);
            prepStmt.setInt(2, tenantId);
            rs = prepStmt.executeQuery();

            if (rs.next()) {
                id = rs.getInt("ID");
            }

        } catch (SQLException e) {
            throw new MigrationClientException("Error while adding claim dialect " + claimDialectURI, e);
        } finally {
            IdentityDatabaseUtil.closeStatement(prepStmt);
            IdentityDatabaseUtil.closeConnection(connection);
        }
        return id;
    }

    /**
     * Add local claim.
     *
     * @param claim
     * @throws MigrationClientException
     */
    public void addLocalClaim(Claim claim) throws MigrationClientException {

        Connection connection = IdentityDatabaseUtil.getDBConnection();
        String localClaimURI = claim.getClaimURI();
        int localClaimId = 0;
        try {
            // Start transaction
            connection.setAutoCommit(false);
            localClaimId = getClaimId(connection, ClaimConstants.LOCAL_CLAIM_DIALECT_URI, localClaimURI,
                    claim.getTenantId());
            if (localClaimId == 0) {
                //Claim record is not in the table
                //Add record
                localClaimId = addClaim(connection, claim.getDialectURI(), localClaimURI, claim.getTenantId());

                // Some JDBC Drivers returns this in the result, some don't
                if (localClaimId == 0) {
                    if (log.isDebugEnabled()) {
                        log.debug("JDBC Driver did not return the claimId, executing Select operation");
                    }
                    localClaimId = getClaimId(connection, ClaimConstants.LOCAL_CLAIM_DIALECT_URI, localClaimURI,
                            claim.getTenantId());
                }

                Map<String, String> claimProperties = new HashMap<>();
                //Reason for null check, Oracle db not allows to insert null values
                if (StringUtils.isNotBlank(claim.getDescription())) {
                    claimProperties.put("Description", claim.getDescription());
                }

                if (StringUtils.isNotBlank(claim.getDisplayTag())) {
                    claimProperties.put("DisplayName", claim.getDisplayTag());
                }

                if (StringUtils.isNotBlank(claim.getRegEx())) {
                    claimProperties.put("RegEx", claim.getRegEx());
                }

                if (StringUtils.isNotBlank(String.valueOf(claim.getDisplayOrder()))) {
                    claimProperties.put("DisplayOrder", String.valueOf(claim.getDisplayOrder()));
                }

                if (StringUtils.isNotBlank(String.valueOf(claim.isReadOnly()))) {
                    claimProperties.put("ReadOnly", String.valueOf(claim.isReadOnly()));
                }

                if (StringUtils.isNotBlank(String.valueOf(claim.isRequired()))) {
                    claimProperties.put("Required", String.valueOf(claim.isRequired()));
                }

                if (StringUtils.isNotBlank(String.valueOf(claim.isSupportedByDefault()))) {
                    claimProperties.put("SupportedByDefault", String.valueOf(claim.isSupportedByDefault()));
                }

                addClaimProperties(connection, localClaimId, claimProperties, claim.getTenantId());

                for (MappedAttribute mappedAttribute : claim.getAttributes()) {
                    addClaimAttributeMappings(connection, localClaimId, mappedAttribute.getAttribute(),
                            mappedAttribute.getDomain(), claim.getTenantId());
                }
            }

            // End transaction
            JDBCPersistenceUtil.commitTransaction(connection);
        } catch (SQLException e) {
            JDBCPersistenceUtil.rollbackTransaction(connection);
            throw new MigrationClientException("Error while adding local claim " + localClaimURI, e);
        } finally {
            IdentityDatabaseUtil.closeConnection(connection);
        }
    }

    /**
     * Add claim.
     *
     * @param connection
     * @param claimDialectURI
     * @param claimURI
     * @param tenantId
     * @return
     * @throws MigrationClientException
     */
    public int addClaim(Connection connection, String claimDialectURI, String claimURI, int tenantId)
            throws MigrationClientException {

        PreparedStatement prepStmt = null;
        ResultSet rs = null;

        int claimId = 0;
        String query = SQLConstants.ADD_CLAIM;
        try {
            String dbProductName = connection.getMetaData().getDatabaseProductName();
            prepStmt = connection.prepareStatement(query, new String[]{
                    DBUtils.getConvertedAutoGeneratedColumnName(dbProductName, SQLConstants.ID_COLUMN)});

            prepStmt.setString(1, claimDialectURI);
            prepStmt.setInt(2, tenantId);
            prepStmt.setString(3, claimURI);
            prepStmt.setInt(4, tenantId);
            prepStmt.executeUpdate();

            rs = prepStmt.getGeneratedKeys();

            if (rs.next()) {
                claimId = rs.getInt(1);
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while adding claim " + claimURI + " to dialect " +
                    claimDialectURI, e);
        } finally {
            IdentityDatabaseUtil.closeResultSet(rs);
            IdentityDatabaseUtil.closeStatement(prepStmt);
        }

        return claimId;
    }

    /**
     * Add claim attribute mapping.
     *
     * @param connection
     * @param localClaimId
     * @param mappedAttribute
     * @param mappedAttributeDomain
     * @param tenantId
     * @throws MigrationClientException
     */
    private void addClaimAttributeMappings(Connection connection, int localClaimId, String mappedAttribute,
                                           String mappedAttributeDomain, int tenantId) throws MigrationClientException {

        PreparedStatement prepStmt = null;
        if (localClaimId > 0 && mappedAttribute != null) {
            try {
                if (mappedAttributeDomain == null) {
                    mappedAttributeDomain = IdentityUtil.getPrimaryDomainName();
                }
                String query = SQLConstants.ADD_CLAIM_MAPPED_ATTRIBUTE;
                prepStmt = connection.prepareStatement(query);

                prepStmt.setInt(1, localClaimId);
                prepStmt.setString(2, mappedAttributeDomain);
                prepStmt.setString(3, mappedAttribute);
                prepStmt.setInt(4, tenantId);

                prepStmt.execute();
            } catch (SQLException e) {
                throw new MigrationClientException("Error while adding attribute mappings", e);
            } finally {
                IdentityDatabaseUtil.closeStatement(prepStmt);
            }
        }
    }

    /**
     * Add claim properties.
     *
     * @param connection
     * @param localClaimId
     * @param claimProperties
     * @param tenantId
     * @throws MigrationClientException
     */
    private void addClaimProperties(Connection connection, int localClaimId, Map<String, String> claimProperties,
                                    int tenantId) throws MigrationClientException {

        PreparedStatement prepStmt = null;
        if (localClaimId > 0 && claimProperties != null) {
            try {
                String query = SQLConstants.ADD_CLAIM_PROPERTY;
                prepStmt = connection.prepareStatement(query);

                for (Map.Entry<String, String> property : claimProperties.entrySet()) {
                    prepStmt.setInt(1, localClaimId);
                    prepStmt.setString(2, property.getKey());
                    prepStmt.setString(3, property.getValue() != null ? property.getValue() : "");
                    prepStmt.setInt(4, tenantId);
                    prepStmt.addBatch();
                }

                prepStmt.executeBatch();
            } catch (SQLException e) {
                throw new MigrationClientException("Error while adding claim properties", e);
            } finally {
                IdentityDatabaseUtil.closeStatement(prepStmt);
            }
        }
    }

    /**
     * Get claim Id.
     *
     * @param connection
     * @param claimDialectURI
     * @param claimURI
     * @param tenantId
     * @return
     * @throws MigrationClientException
     */
    public int getClaimId(Connection connection, String claimDialectURI, String claimURI, int tenantId)
            throws MigrationClientException {

        PreparedStatement prepStmt = null;
        ResultSet rs = null;

        int claimId = 0;
        String query = SQLConstants.GET_CLAIM_ID;
        try {
            prepStmt = connection.prepareStatement(query);
            prepStmt.setString(1, claimDialectURI);
            prepStmt.setInt(2, tenantId);
            prepStmt.setString(3, claimURI);
            prepStmt.setInt(4, tenantId);
            rs = prepStmt.executeQuery();

            while (rs.next()) {
                claimId = rs.getInt(SQLConstants.ID_COLUMN);
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving ID for claim " + claimURI + " in dialect "
                    + claimDialectURI, e);
        } finally {
            IdentityDatabaseUtil.closeResultSet(rs);
            IdentityDatabaseUtil.closeStatement(prepStmt);
        }
        return claimId;
    }

    public String getClaimURI(Connection connection, String claimDialectURI, int claimId, int tenantId)
            throws MigrationClientException {

        PreparedStatement prepStmt = null;
        ResultSet rs = null;

        String claimURI = null;
        String query = SQLConstants.GET_CLAIM_URI;
        try {
            prepStmt = connection.prepareStatement(query);
            prepStmt.setString(1, claimDialectURI);
            prepStmt.setInt(2, tenantId);
            prepStmt.setInt(3, claimId);
            prepStmt.setInt(4, tenantId);
            rs = prepStmt.executeQuery();

            while (rs.next()) {
                claimURI = rs.getString("CLAIM_URI");
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving URI for claim " + claimURI + " in dialect "
                    + claimDialectURI, e);
        } finally {
            IdentityDatabaseUtil.closeResultSet(rs);
            IdentityDatabaseUtil.closeStatement(prepStmt);
        }
        return claimURI;
    }

    /**
     * Get claim Id from mapped Attribute.
     *
     * @param connection
     * @param mappedAttribute
     * @param mappedAttributeDomain
     * @param tenantId
     * @return
     * @throws MigrationClientException
     */
    public List<Integer> getClaimIdFromMappedAttributes(Connection connection, String mappedAttribute,
                                                        String mappedAttributeDomain, int tenantId)
            throws MigrationClientException {

        PreparedStatement prepStmt = null;
        ResultSet rs = null;

        List<Integer> localIds = new ArrayList<>();
        String query = SQLConstants.GET_CLAIM_FROM_MAPPED_ATTRIBUTE;
        try {
            if (mappedAttributeDomain == null) {
                mappedAttributeDomain = IdentityUtil.getPrimaryDomainName();
            }
            prepStmt = connection.prepareStatement(query);
            prepStmt.setString(1, mappedAttributeDomain);
            prepStmt.setString(2, mappedAttribute);
            prepStmt.setInt(3, tenantId);
            rs = prepStmt.executeQuery();

            while (rs.next()) {
                localIds.add(rs.getInt("LOCAL_CLAIM_ID"));
            }
        } catch (SQLException e) {
            throw new MigrationClientException("Error while retrieving LOCAL_CLAIM_ID for claim " + mappedAttribute +
                    " " + "in tenant : " + IdentityTenantUtil.getTenantDomain(tenantId), e);
        } finally {
            IdentityDatabaseUtil.closeResultSet(rs);
            IdentityDatabaseUtil.closeStatement(prepStmt);
        }
        return localIds;
    }

    /**
     * Add External claim.
     *
     * @param claim
     */
    public void addExternalClaim(Claim claim, StringBuilder report) throws MigrationClientException {

        Connection connection = IdentityDatabaseUtil.getDBConnection();

        String externalClaimURI = claim.getClaimURI();
        String externalClaimDialectURI = claim.getDialectURI();
        int externalClaimId = 0;
        try {
            // Start transaction
            connection.setAutoCommit(false);
            externalClaimId = getClaimId(connection, externalClaimDialectURI, externalClaimURI, claim.getTenantId());

            //Nothing to do if the claim is already exists
            if (externalClaimId == 0) {
                externalClaimId = addClaim(connection, externalClaimDialectURI, externalClaimURI, claim.getTenantId());

                // Some JDBC Drivers returns this in the result, some don't
                if (externalClaimId == 0) {
                    if (log.isDebugEnabled()) {
                        log.debug("JDBC Driver did not return the claimId, executing Select operation");
                    }
                    externalClaimId = getClaimId(connection, externalClaimDialectURI, externalClaimURI,
                            claim.getTenantId());
                }

                //Ge the relevant local claim from mapped attributes.If all mapped attributes are matching to the
                // same local claim, add that association. If the multiple local claims are matching to remote claim,
                // we can't add multiple mapping to one remote claim. So, create a new local claim and add mapping to
                // that claim with mapped attributes

                List<Integer> commonLocalClaimIds = null;

                for (MappedAttribute mappedAttribute : claim.getAttributes()) {
                    List<Integer> localClaimIds = getClaimIdFromMappedAttributes(connection, mappedAttribute
                            .getAttribute(), mappedAttribute.getDomain(), claim.getTenantId());
                    if (commonLocalClaimIds == null) {
                        commonLocalClaimIds = localClaimIds;
                    } else {
                        commonLocalClaimIds = intersection(commonLocalClaimIds, localClaimIds);
                    }
                }

                if (commonLocalClaimIds.size() > 0) {
                    addClaimMapping(connection, externalClaimId, commonLocalClaimIds.get(0), claim.getTenantId());
                    String localClaimURI = getClaimURI(connection, ClaimConstants.LOCAL_CLAIM_DIALECT_URI,
                            commonLocalClaimIds.get(0), claim.getTenantId());

                    report.append("\n\n Added Remote Claim  :" + claim.getDialectURI() + " in Dialect : " + claim
                            .getDialectURI()
                            + " in  tenant domain :" + IdentityTenantUtil.getTenantDomain(claim.getTenantId())
                            + " , " + "Mapped Local claim :" + localClaimURI);
                    if (log.isDebugEnabled()) {
                        log.debug("\n Added Remote Claim  :" + claim.getDialectURI() + " in Dialect : "
                                + claim.getDialectURI() + " in  tenant domain :"
                                + IdentityTenantUtil.getTenantDomain(claim.getTenantId()) + " , "
                                + "Mapped Local claim :" + localClaimURI);
                    }

                } else {
                    //Create a new Local claim for matching the remote claim
                    int newClaimId;
                    Random random = new Random();
                    int randomNumber = random.nextInt(100000);

                    if (StringUtils.isNotBlank(claim.getDisplayTag())) {
                        newClaimId = addClaim(connection, ClaimConstants.LOCAL_CLAIM_DIALECT_URI,
                                ClaimConstants.LOCAL_CLAIM_DIALECT_URI + "/migration__" +
                                        claim.getDisplayTag().toLowerCase() + "__" + randomNumber,
                                claim.getTenantId());

                        logReport(report, "\n\n No matching local claim found for external claim :"
                                + claim.getClaimURI() + " in claim dialect :" + claim.getDialectURI()
                                + " in tenant domain :"
                                + IdentityTenantUtil.getTenantDomain(claim.getTenantId())
                                + ". So create a new local " + "claim named : "
                                + ClaimConstants.LOCAL_CLAIM_DIALECT_URI + "/migration__"
                                + claim.getDisplayTag().toLowerCase() + "__" + randomNumber);
                    } else {
                        newClaimId = addClaim(connection, ClaimConstants.LOCAL_CLAIM_DIALECT_URI,
                                ClaimConstants.LOCAL_CLAIM_DIALECT_URI + "/migration__" + randomNumber,
                                claim.getTenantId());

                        logReport(report, "\n\n No matching local claim found for external claim :"
                                + claim.getClaimURI() + " in claim dialect :" + claim.getDialectURI()
                                + " in tenant domain :"
                                + IdentityTenantUtil.getTenantDomain(claim.getTenantId())
                                + ". So create a new local " + "claim named : "
                                + ClaimConstants.LOCAL_CLAIM_DIALECT_URI + "/migration__" + randomNumber);
                    }

                    Map<String, String> claimProperties = new HashMap<>();
                    claimProperties.put("Description", claim.getDescription());
                    claimProperties.put("DisplayName", "Migration__" + claim.getDisplayTag());
                    addClaimProperties(connection, newClaimId, claimProperties, claim.getTenantId());

                    for (MappedAttribute mappedAttribute : claim.getAttributes()) {
                        addClaimAttributeMappings(connection, newClaimId, mappedAttribute.getAttribute(),
                                mappedAttribute.getDomain(), claim.getTenantId());
                    }
                    addClaimMapping(connection, externalClaimId, newClaimId, claim.getTenantId());
                }
            }
            // End transaction
            JDBCPersistenceUtil.commitTransaction(connection);
        } catch (SQLException e) {
            JDBCPersistenceUtil.rollbackTransaction(connection);
            throw new MigrationClientException("Error while adding external claim " + externalClaimURI + " to " +
                    "dialect " + externalClaimDialectURI, e);
        } finally {
            IdentityDatabaseUtil.closeConnection(connection);
        }
    }

    private void logReport(StringBuilder report, String str) {

        report.append(str);

        if (log.isDebugEnabled()) {
            log.debug(str);
        }
    }

    /**
     * Add claim mappings.
     *
     * @param connection
     * @param externalClaimId
     * @param localClaimId
     * @param tenantId
     * @throws MigrationClientException
     */
    private void addClaimMapping(Connection connection, int externalClaimId, int localClaimId, int tenantId)
            throws MigrationClientException {

        PreparedStatement prepStmt = null;

        String query = SQLConstants.ADD_CLAIM_MAPPING;
        try {
            prepStmt = connection.prepareStatement(query);
            prepStmt.setInt(1, localClaimId);
            prepStmt.setInt(2, externalClaimId);
            prepStmt.setInt(3, tenantId);
            prepStmt.executeUpdate();
        } catch (SQLException e) {
            throw new MigrationClientException("Error while adding claim mapping", e);
        } finally {
            IdentityDatabaseUtil.closeStatement(prepStmt);
        }
    }

    private <T> List<T> intersection(List<T> list1, List<T> list2) {

        List<T> list = new ArrayList<T>();

        for (T t : list1) {
            if (list2.contains(t)) {
                list.add(t);
            }
        }

        return list;
    }

}

