package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.config.ReplicationConfig;
import com.atlassian.bitbucket.mesh.transaction.CoordinatingReferenceTransactionListener;
import com.atlassian.bitbucket.mesh.transaction.Transaction;
import com.atlassian.bitbucket.mesh.transaction.TransactionManager;

import static com.google.common.hash.Hashing.sha256;
import static com.google.common.math.DoubleMath.fuzzyCompare;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Helper class used by {@link CoordinatingReferenceTransactionListener} during content hash updates.
 * <p>
 * This class is used to determine how a content hash should be calculated when committing a transaction. It has been
 * implemented in a helper class to allow easier unit testing of this logic.
 *
 * @since 2.0.9, 2.1.3, 2.2
 */
class ContentHashHelper {

    private static final double EPSILON = 0.000001d;

    private final ReplicationConfig config;
    private final TransactionManager transactionManager;

    ContentHashHelper(ReplicationConfig config, TransactionManager transactionManager) {
        this.config = config;
        this.transactionManager = transactionManager;
    }

    /**
     * Determine whether content hash recalculation should be forced for the current transaction.
     * <p>
     * This method uses the {@link Transaction#getId() transaction ID} as a source of randomness as it is
     * implicitly synchronised between all nodes in a transaction.
     *
     * @return {@code true} if the content hash recalculation should be forced, otherwise {@code false}
     * @see ReplicationConfig#getContentHashForcedRecalculationFrequency()
     */
    boolean shouldForceRecalculate() {
        double frequency = config.getContentHashForcedRecalculationFrequency();
        //Avoid retrieving the current transaction if the frequency is set to 0 (never force recalculate) since the
        //return value will always be false

        if (fuzzyCompare(frequency, 0d, EPSILON) <= 0) {
            return false;
        }

        return transactionManager.getCurrentTransaction()
                .map(transaction -> {
                    //Only return true when the frequency is set to 1 (always) when there is also a current transaction
                    if (fuzzyCompare(frequency, 1d, EPSILON) >= 0) {
                        return true;
                    }
                    //Get the hash code for the transaction ID, which will be between Long.MIN_VALUE and
                    //Long.MAX_VALUE - 1 inclusive
                    long txHashCode = sha256().hashString(transaction.getId(), UTF_8).asLong();
                    //Divide the hash code by Long.MAX_VALUE to get a value between -1 and 1 inclusive
                    double d = (double) txHashCode / Long.MAX_VALUE;
                    //Divide this value by 2 then add 0.5 to shrink and shift the range to 0 to 1 inclusive
                    d = d / 2d + 0.5d;
                    //Compare this value to the configured frequency which is also between 0 to 1 inclusive, which
                    //results in this returning true frequency% of the time
                    return frequency > d;
                })
                //There should always be a transaction in the context when this method is called, but in case there
                //isn't default to returning false
                .orElse(false);
    }
}
