"use strict";

var Redlock = require('redlock');
var { getRedisClient } = require("../redis/RedisClient");

// the redlock instance
var redlock;

var DEFAULT_LOCK_DURATION_MILLISECONDS = 10000;

// the time that the lock will be held for
var lockDuration;

var documentLockPrefix = "locks:document:";

// initialise this to true if the lock isn't configured, so an attempt won't be made to load the lock on every use 
var lockDisabled = false;

// initialise the lock
function initialiseLock() {
    var redisConfig = config.get('redis');

    if (redisConfig && redisConfig.port && redisConfig.host && redisConfig.lock && redisConfig.lock.enabled) {
        var client1 = getRedisClient();

        lockDuration = redisConfig.lock.durationMilliseconds ? 
                   redisConfig.lock.durationMilliseconds : 
                   DEFAULT_LOCK_DURATION_MILLISECONDS;

        logger.info("Redlock locking is enabled with lock duration of: " + lockDuration);

        // configuration values for redlock

        // the expected clock drift; for more details 
        // see http://redis.io/topics/distlock 
        var driftFactor = redisConfig.lock.driftFactor ? redisConfig.lock.driftFactor : 0.01;

        // the max number of times Redlock will attempt 
        // to lock a resource before erroring 
        var retryCount = redisConfig.lock.retryCount ? redisConfig.lock.retryCount : 1000;

        // the time in ms between attempts 
        var retryDelay = redisConfig.lock.retryDelay ? redisConfig.lock.retryDelay : 200;

        // the max time in ms randomly added to retries 
        // to improve performance under high contention 
        // see https://www.awsarchitectureblog.com/2015/03/backoff.html 
        var retryJitter = redisConfig.lock.retryJitter ? redisConfig.lock.retryJitter : 200;

        redlock = new Redlock(
            // there should be one client for each redis node 
            // in the cluster. We will need to make the nodes configurable if we move to using more than one node
            [client1],
            {
                driftFactor: driftFactor, // time in ms 
        
                retryCount:  retryCount,
        
                retryDelay:  retryDelay, // time in ms 
        
                retryJitter:  retryJitter // time in ms 
            }
        );

        logger.info("Using redlock distributed lock in data service operations");
    } else {
        logger.info("Not using redlock distributed lock in data service operations because it isn't configured");
        lockDisabled = true;
    }
}

/**
 * lock a document so other operations can't update it at the same time. This prevents problems of the same document being updated 
 * in quick succession. Even within the same process there can be concurrency problems because when we update we insert the previous
 * version and we can't have duplicates. If an update has already started on a document then a second update may look for the 
 * current version and find it but by the time the current version has been inserted as a previous version the first update has 
 * already inserted a previous version with the same versionId. We can't allow duplicate versionIds because versionId is our only 
 * unique key on the documents. documentId is not unique because the previous versions have the same documentId.
 * 
 * This returns a promise and the process that locks the document needs to take a copy of the lock variable returned as a parameter
 * so that it can then call the unlock function with that lock passed as a parameter. Eg
 * 
 * lockDocument(document.documentId).then(function documentLocked(returnedLock) {
 *      // store the lock so it can be unlocked
 *      var lock = returnedLock;
 * 
 *      // perform processing of document that needs locking
 *      ...
 * 
 *      // call the unlock function with the lock variable to release the lock
 *      DistributedLock.unlock(lock);
 */
function lockDocument(documentId) {
    if (!redlock && !lockDisabled) {
        initialiseLock();
    }

    // check lockDisabled again because the value may have been changed by the initialiseLock function      
    if (lockDisabled || !documentId) {
        // resolve the promise with no documentId
        // resolve the promise with not lock, so that the app will work with no locking configured
        return Promise.resolve();
    } else {
        var resource = documentLockPrefix + documentId;

        // the maximum amount of time you want the resource locked,
        // keeping in mind that you can extend the lock up until
        // the point when it expires
        logger.info("Locking documentId: " + documentId);

        return redlock.lock(resource, lockDuration);
    }
}

/**
 * unlock the document. This needs to be called at the end of the operation that needs the lock. If it doesn't the lock will
 * timeout but it's better to always unlock it when the lock is no longer needed
 * @param {*} lock - the lock that was returned in the promise result of lockDocument 
 */
function unlockDocument(lock) {
    if (lock && lock.unlock) {
        return lock.unlock().catch(function unlockError(err) {
            logger.error("Error unlocking document: " + err.message);
        });
    }
}

module.exports = {
    initialiseLock: initialiseLock,

    lockDocument: lockDocument, 

    unlockDocument: unlockDocument 
};
