package com.atlassian.stash.internal.liquibase;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import liquibase.change.custom.CustomTaskChange;
import liquibase.database.Database;
import liquibase.exception.CustomChangeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

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

/**
 * Remove orphaned memberships where membership_type is equal to 'GROUP_USER'. This _should_ remove all the duplicates.
 * If they have any duplicates left over we'll have to just delete them all. Getting in a state where
 * this will delete anything is very unlikely and probably means the database is badly corrupted.
 *
 * @since 3.7
 */
public class CrowdMembershipOrphanDedupeChange extends AbstractCustomChange implements CustomTaskChange {

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

    private int duplicatesDeleted;
    private int orphansDeleted;

    @Override
    public void execute(Database database) throws CustomChangeException {
        JdbcTemplate template = LiquibaseUtils.getJdbcTemplate(database);

        Collection<Long> orphans = findOrphans(template);
        log.debug("{} orphan cwd_membership rows found", orphans.size());
        if (!orphans.isEmpty()) {
            orphansDeleted = deleteRowsById(template, orphans);
            log.debug("Orphan cwd_membership rows deleted");
        }

        Collection<MembershipIndex> duplicateIndices = findDuplicateIndices(template);
        log.debug("{} duplicate cwd_membership indices found", duplicateIndices.size());
        if (!duplicateIndices.isEmpty()) {
            duplicatesDeleted = deleteRowsByIndices(template, duplicateIndices);
            log.debug("Duplicate cwd_membership rows deleted");
        }
    }

    private int deleteRowsById(JdbcTemplate template, Collection<Long> rowIdsToDelete) throws CustomChangeException {
        int rowsDeleted = 0;
        try {
            Iterable<List<Long>> batches = Iterables.partition(rowIdsToDelete, MAX_IN_CLAUSE_SIZE);
            for (List<Long> idsToRemove : batches) {
                rowsDeleted += template.update("DELETE FROM cwd_membership WHERE id IN ( " + Joiner.on(',').join(idsToRemove) + " )");
            }
        } catch (DataAccessException e) {
            throw new CustomChangeException("Failed to delete some cwd_membership rows", e);
        }
        return rowsDeleted;
    }

    private int deleteRowsByIndices(JdbcTemplate template, Collection<MembershipIndex> indices) throws CustomChangeException {
        int rowsDeleted = 0;
        try {
            Iterable<List<MembershipIndex>> batches = Iterables.partition(indices, MAX_BATCH_SIZE);
            for (final List<MembershipIndex> indicesToRemove : batches) {
                rowsDeleted += template.batchUpdate("DELETE FROM cwd_membership " +
                        "WHERE directory_id = ? AND lower_parent_name = ? AND " +
                        "lower_child_name = ? AND membership_type = ?", new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setLong(1, indicesToRemove.get(i).directoryId);
                        ps.setString(2, indicesToRemove.get(i).lowerParentName);
                        ps.setString(3, indicesToRemove.get(i).lowerChildName);
                        ps.setString(4, indicesToRemove.get(i).membershipType);
                    }

                    @Override
                    public int getBatchSize() {
                        return indicesToRemove.size();
                    }
                })[0];
            }
        } catch (DataAccessException e) {
            log.error("Failed to delete some cwd_membership rows", e);
            throw new CustomChangeException("Failed to delete some cwd_membership rows", e);
        }
        return rowsDeleted;
    }

    private Collection<MembershipIndex> findDuplicateIndices(JdbcTemplate template) throws CustomChangeException {
        try {
            List<MembershipIndex> duplicateIndices = template.query("SELECT directory_id, lower_parent_name, lower_child_name, membership_type, " +
                    "COUNT(*) as cn " +
                    "FROM cwd_membership " +
                    "GROUP BY directory_id, lower_parent_name, lower_child_name, membership_type " +
                    "HAVING COUNT(*) > 1", new MembershipIndexMapper());
            return duplicateIndices;
        } catch (DataAccessException e) {
            log.error("Failed to query cwd_membership for orphans", e);
            throw new CustomChangeException("Failed to query cwd_membership for orphans", e);
        }
    }

    private Collection<Long> findOrphans(JdbcTemplate template) throws CustomChangeException {
        try {
            List<Long> orphans = template.query("SELECT cwd_membership.id " +
                            "FROM cwd_membership " +
                            "LEFT JOIN cwd_user ON cwd_membership.child_id = cwd_user.id " +
                            "LEFT JOIN cwd_group ON cwd_membership.parent_id = cwd_group.id " +
                            "WHERE cwd_membership.membership_type = 'GROUP_USER' AND (cwd_user.id IS NULL OR cwd_group.id IS NULL)",
                    new IdMapper());
            return orphans;
        } catch (DataAccessException e) {
            log.error("Failed to query cwd_membership for orphans", e);
            throw new CustomChangeException("Failed to query cwd_membership for orphans", e);
        }
    }

    @Override
    public String getConfirmationMessage() {
        return "Orphans and duplicate entries in cwd_membership removed." +
                " Orphans = " + orphansDeleted +
                " Duplicates = " + duplicatesDeleted + ".";
    }

    private static class IdMapper implements RowMapper<Long> {
        @Override
        public Long mapRow(ResultSet rs, int rowNum) throws SQLException {
            return rs.getLong("id");
        }
    }

    private static class MembershipIndex {
        private final long directoryId;
        private final String lowerParentName;
        private final String lowerChildName;

        private final String membershipType;
        private MembershipIndex(long directoryId, String lowerParentName, String lowerChildName, String membershipType) {
            this.directoryId = directoryId;
            this.lowerParentName = lowerParentName;
            this.lowerChildName = lowerChildName;
            this.membershipType = membershipType;
        }
    }

    private static class MembershipIndexMapper implements RowMapper<MembershipIndex> {
        @Override
        public MembershipIndex mapRow(ResultSet rs, int rowNum) throws SQLException {
            return new MembershipIndex(rs.getLong("directory_id"),
                    rs.getString("lower_parent_name"),
                    rs.getString("lower_child_name"),
                    rs.getString("membership_type"));
        }
    }
}
