// "Row" (key)-level locks for antietcd
// (c) Vitaliy Filippov, 2024+
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1

const { RequestError } = require('./common.js');

// How does it work:
// - Leader sets key-level locks, persists and replicates the change, then removes locks
// - Followers set locks, persist and reply to the leader, wait for the feedback, then remove locks
// - This is done from etctree by calling regular 'replicate watcher' method so etctree is agnostic of it
// - Both leader and followers check for locks with check_txn_locks() and then wait for them
//   using wait_commit() before executing the transaction, and only proceed when check_txn_locks() returns 0
// - A node calls break_locks() if their Raft state changes
class AntiLocker
{
    constructor(cfg)
    {
        cfg = cfg || {};
        this.cfg = cfg;
        this.cfg.lock_wait_interval = Number(this.cfg.lock_wait_interval);
        if (!this.cfg.lock_wait_interval || this.cfg.lock_wait_interval < 50)
            this.cfg.lock_wait_interval = 3000;
        this.locks = [];
        this.on_unlock = [];
        this.txn_locks = {};
        this.lock_timeout_timer = this.cfg.use_locks ? setInterval(() => this._timeout_locks(), this.cfg.lock_wait_interval) : null;
        this.resume_notifications = null;
    }

    destroy()
    {
        if (this.lock_timeout_timer)
        {
            clearInterval(this.lock_timeout_timer);
            this.lock_timeout_timer = null;
        }
    }

    // Put (txn) => etctree.resume_notifications(txn) here
    set_resume_notifications(resume_cb)
    {
        this.resume_notifications = resume_cb;
    }

    _find_lock(key)
    {
        let min = 0, max = this.locks.length;
        while (max-min > 1)
        {
            let mid = Math.floor((min+max)/2);
            if (this.locks[mid].key > key)
                max = mid;
            else
                min = mid;
        }
        return min;
    }

    check_locks(key, range_end)
    {
        const pos = this._find_lock(key);
        if (pos < this.locks.length && this.locks[pos].key <= (key || range_end))
        {
            return this.locks[pos].revision;
        }
        return 0;
    }

    check_txn_locks(txn)
    {
        let lock_txn;
        if (txn.compare)
        {
            for (const req of txn.compare)
            {
                lock_txn = this.check_locks(req.key);
                if (lock_txn)
                    return lock_txn;
            }
        }
        for (const actions of [ 'success', 'failure' ])
        {
            if (!txn[actions])
                continue;
            let r, lock_txn;
            for (const req of txn[actions])
            {
                if ((r = (req.request_range || req.requestRange)))
                {
                    lock_txn = this.check_locks(r.key, r.range_end);
                    if (lock_txn)
                        return lock_txn;
                }
                else if ((r = (req.request_put || req.requestPut)))
                {
                    lock_txn = this.check_locks(r.key);
                    if (lock_txn)
                        return lock_txn;
                }
                else if ((r = (req.request_delete_range || req.requestDeleteRange)))
                {
                    lock_txn = this.check_locks(r.key, r.range_end);
                    if (lock_txn)
                        return lock_txn;
                }
            }
        }
        return 0;
    }

    set_txn_locks(notifications, mod_revision)
    {
        if (this.txn_locks[mod_revision])
        {
            throw new Error('BUG: locks already set for revision '+mod_revision);
        }
        const res = [];
        for (const event of notifications)
        {
            const pos = this._find_lock(event.key);
            const lock = { key: event.key, revision: mod_revision };
            res.push(lock);
            this.locks.splice(pos, 0, lock);
        }
        this.txn_locks[mod_revision] = res;
    }

    wait_commit(mod_revision, expire_ts)
    {
        // Postpone transaction
        return new Promise((ok, fail) => this._set_on_unlock(mod_revision, expire_ts, ok, fail));
    }

    break_locks()
    {
        this.txn_locks = {};
        this.locks = [];
        const waits = this.on_unlock;
        this.on_unlock = [];
        for (const wait of waits)
        {
            const cb = wait.fail;
            cb(new RequestError(408, 'Lock wait aborted, please retry request'));
        }
    }

    _set_on_unlock(lock_rev, expire_ts, ok, fail)
    {
        let min = 0, max = this.on_unlock.length;
        while (max-min > 1)
        {
            let mid = Math.floor((min+max)/2);
            if (this.on_unlock[mid].revision > lock_rev)
                max = mid;
            else
                min = mid;
        }
        this.on_unlock.splice(min, 0, { revision: lock_rev, expire_ts, ok, fail });
    }

    _timeout_locks()
    {
        const now = Date.now();
        const cancel = [];
        for (let i = 0; i < this.on_unlock.length; i++)
        {
            if (now > this.on_unlock[i].expire_ts)
            {
                cancel.push(this.on_unlock[i]);
                this.on_unlock.splice(i, 1);
                i--;
            }
        }
        for (const wait of cancel)
        {
            const cb = wait.fail;
            cb(new RequestError(408, 'Lock wait timeout, please retry request'));
        }
    }

    unlock_txn(lock_rev)
    {
        if (!this.txn_locks[lock_rev])
        {
            throw new Error('BUG: revision '+lock_rev+' not locked');
        }
        const txn_set = new Set();
        for (const lock of this.txn_locks[lock_rev])
        {
            txn_set.add(lock);
        }
        delete this.txn_locks[lock_rev];
        this.locks = this.locks.filter(l => !txn_set.has(l));
        let i = 0;
        while (i < this.on_unlock.length && this.on_unlock[i].revision <= lock_rev)
        {
            i++;
        }
        const waits = this.on_unlock.splice(0, i);
        for (const wait of waits)
        {
            const cb = wait.ok;
            cb();
        }
        if (this.resume_notifications)
        {
            this.resume_notifications(lock_rev);
        }
    }
}

module.exports = AntiLocker;
