'use strict';

const fs = require('fs');
const path = require('path');
const lockfile = require('proper-lockfile');
const DB = require("./database");

var _I = ""; //Instance support
for (let arg of process.argv)
    if (/^-I=/.test(arg)) {
        _I = arg.split(/=(.*)/s)[1];
        break;
    }

const {
    SHARES_PER_NODE,
    SHARE_THRESHOLD,
    SHUFFLE_INTERVAL
} = require("./_constants")["keys"];

const PRIV_EKEY_MIN = 32,
    PRIV_EKEY_MAX = 48,
    PRIME_FILE_TYPE = 'binary',
    INDEX_FILE_TYPE = 'utf-8',
    UNSIGNED_INT_MIN = 0,
    UNSIGNED_INT_MAX = 4294967295,
    INDEX_FILE_NAME_LENGTH = 16,
    INDEX_FILE_EXT = '.txt',
    MIN_DUMMY_FILES = 16,
    MAX_DUMMY_FILES = 24,
    MIN_DUMMY_SIZE_MUL = 0.5,
    MAX_DUMMY_SIZE_MUL = 1.5,
    SIZE_FACTOR = 100,
    LOCK_RETRY_MIN_TIME = 1 * 1000,
    LOCK_RETRY_MAX_TIME = 2 * 1000;

var node_priv, e_key, node_id, node_pub; //containers for node-key wrapper
const _x = {
    get node_priv() {
        if (!node_priv || !e_key)
            throw Error("keys not set");
        return Crypto.AES.decrypt(node_priv, e_key);
    },
    set node_priv(key) {
        node_pub = floCrypto.getPubKeyHex(key);
        node_id = floCrypto.getFloID(node_pub);
        if (!key || !node_pub || !node_id)
            throw Error("Invalid Keys");
        let n = floCrypto.randInt(PRIV_EKEY_MIN, PRIV_EKEY_MAX)
        e_key = floCrypto.randString(n);
        node_priv = Crypto.AES.encrypt(key, e_key);
    },
    args_dir: path.resolve(__dirname, '..', 'args'),
    get index_dir() {
        return path.join(this.args_dir, `indexes${_I}`)
    },
    get prime_file() {
        return path.join(this.args_dir, `prime_index${_I}.b`)
    },
    get index_file() {
        try {
            let data = fs.readFileSync(this.prime_file, PRIME_FILE_TYPE),
                fname = Crypto.AES.decrypt(data, this.node_priv);
            return path.join(this.index_dir, fname + INDEX_FILE_EXT);
        } catch (error) {
            console.debug(error);
            throw Error("Prime-Index Missing/Corrupted");
        }
    }
}

function initialize() {
    return new Promise((resolve, reject) => {
        fs.readFile(_x.prime_file, PRIME_FILE_TYPE, (err, res) => {
            var data, cur_filename, new_filename, priv_key;
            try {
                priv_key = _x.node_priv;
            } catch (error) {
                return reject(error);
            }
            if (!err) {
                if (res.length) { //prime file not empty
                    try {
                        cur_filename = Crypto.AES.decrypt(res, priv_key);
                    } catch (error) {
                        console.debug(error);
                        return reject("Prime file corrupted");
                    } try { //read data from index file
                        let tmp = fs.readFileSync(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), INDEX_FILE_TYPE);
                        tmp = Crypto.AES.decrypt(tmp, priv_key);
                        JSON.parse(tmp); //check if data is JSON parse-able
                        data = tmp;
                    } catch (error) {
                        console.debug(error);
                        return reject("Index file corrupted");
                    }
                }
            }
            try {
                if (!fs.existsSync(_x.index_dir)) {
                    fs.mkdirSync(_x.index_dir);
                }
            } catch (error) {
                console.debug(error);
                return reject("Index directory creation failed");
            }
            try { //delete all old dummy files
                let files = fs.readdirSync(_x.index_dir);
                for (const file of files)
                    if (!cur_filename || file !== cur_filename + INDEX_FILE_EXT)    //check if file is current file
                        fs.unlinkSync(path.join(_x.index_dir, file));
            } catch (error) {
                console.debug(error);
                return reject("Clear index directory failed");
            } try { //create files (dummy and new index file)
                let N = floCrypto.randInt(MIN_DUMMY_FILES, MAX_DUMMY_FILES),
                    k = floCrypto.randInt(0, N);
                if (typeof data === 'undefined' || data.length == 0)  //no existing data, initialize
                    data = JSON.stringify({});
                let data_size = data.length;
                for (let i = 0; i <= N; i++) {
                    let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
                    if (i == k) {
                        new_filename = f_name;
                        f_data = data;
                    } else {
                        let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
                        f_data = floCrypto.randString(d_size, false);
                    }
                    f_data = Crypto.AES.encrypt(f_data, priv_key);
                    fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
                }
            } catch (error) {
                console.debug(error);
                return reject("Index file creation failed");
            } try { //update prime file
                let en_filename = Crypto.AES.encrypt(new_filename, priv_key);
                fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
            } catch (error) {
                console.debug(error);
                return reject("Update prime file failed");
            }
            if (cur_filename)
                fs.unlink(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), err => err ? console.debug(err) : null);
            shuffle.interval = setInterval(shuffle, SHUFFLE_INTERVAL);
            resolve("Key management initiated");
        })
    })
}

function shuffle() {
    readIndexFile().then(data => {
        let new_filename, cur_filename = Crypto.AES.decrypt(fs.readFileSync(_x.prime_file, PRIME_FILE_TYPE), _x.node_priv);
        fs.readdir(_x.index_dir, (err, files) => {
            if (err)
                return console.error(err);
            data = JSON.stringify(data);
            let data_size = data.length;
            for (let file of files) {
                let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
                if (file === cur_filename + INDEX_FILE_EXT) {
                    new_filename = f_name;
                    f_data = data;
                } else {
                    let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
                    f_data = floCrypto.randString(d_size, false);
                }
                f_data = Crypto.AES.encrypt(f_data, _x.node_priv);
                //rename and rewrite the file
                try {
                    fs.renameSync(path.join(_x.index_dir, file), path.join(_x.index_dir, f_name + INDEX_FILE_EXT));
                    fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
                } catch (error) {
                    console.error(error)
                }
            }
            //update prime file
            if (!new_filename)
                return console.error("Index file has not been renamed");
            let en_filename = Crypto.AES.encrypt(new_filename, _x.node_priv);
            try {
                fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
            } catch (error) {
                console.error(error);
            }
        })
    }).catch(error => console.error(error))
}

function readIndexFile() {
    return new Promise((resolve, reject) => {
        fs.readFile(_x.index_file, INDEX_FILE_TYPE, (err, data) => {
            if (err) {
                console.debug(err);
                return reject('Unable to read Index file');
            }
            try {
                data = JSON.parse(Crypto.AES.decrypt(data, _x.node_priv));
                resolve(data);
            } catch {
                reject("Index file corrupted");
            }
        })
    })
}

function writeIndexFile(data) {
    return new Promise((resolve, reject) => {
        let en_data = Crypto.AES.encrypt(JSON.stringify(data), _x.node_priv);
        fs.writeFile(_x.index_file, en_data, INDEX_FILE_TYPE, (err) => {
            if (err) {
                console.debug(err);
                return reject('Unable to write Index file');
            } else resolve("Updated Index file");
        })
    })
}

function getShares(group, id, ignoreDiscarded = true) {
    return new Promise((resolve, reject) => {
        checkIfDiscarded(id).then(result => {
            if (ignoreDiscarded && result != false)
                return reject("Trying to get share for discarded ID");
            readIndexFile().then(data => {
                if (!(group in data))
                    reject("Group not found in Index file");
                else if (!(id in data[group]))
                    reject("ID not found in Index file");
                else {
                    let ref = data[group][id].shift();
                    DB.query("SELECT share FROM sinkShares WHERE num IN (?)", [data[group][id]])
                        .then(result => resolve({ ref, shares: result.map(r => Crypto.AES.decrypt(r.share, _x.node_priv)) }))
                        .catch(error => reject(error))
                }
            }).catch(error => reject(error))
        }).catch(error => reject(error))
    })
}

function storeShareAtRandom(share) {
    return new Promise((resolve, reject) => {
        let rand = floCrypto.randInt(UNSIGNED_INT_MIN, UNSIGNED_INT_MAX);
        DB.query("INSERT INTO sinkShares(num, share) VALUE (?)", [[rand, share]])
            .then(result => resolve(result.insertId)).catch(error => {
                if (error.code === "ER_DUP_ENTRY")
                    storeShareAtRandom(share) //try again (with diff rand_num)
                        .then(result => resolve(result))
                        .catch(error => reject(error))
                else
                    reject(error);
            })
    })
}

function addShare(group, id, ref, share) {
    return new Promise((resolve, reject) => {
        checkIfDiscarded(id).then(result => {
            if (result != false)
                return reject("Trying to store share for discarded ID");
            lockfile.lock(_x.index_file, { retries: { forever: true, minTimeout: LOCK_RETRY_MIN_TIME, maxTimeout: LOCK_RETRY_MAX_TIME } }).then(release => {
                const releaseAndReject = err => {
                    release().then(_ => null).catch(error => console.error(error));
                    reject(err);
                }
                readIndexFile().then(data => {
                    if (!(group in data))
                        data[group] = {};
                    if (!(id in data[group]))
                        data[group][id] = [ref];
                    else if (ref < data[group][id][0])
                        return reject("reference is lower than current");
                    else if (ref > data[group][id][0]) {
                        let old_shares = data[group][id];
                        data[group][id] = [ref];
                        old_shares.shift();
                        DB.query("DELETE FROM sinkShares WHERE num in (?)", [old_shares])//delete old shares
                            .then(_ => null).catch(error => console.error(error));
                    }
                    let encrypted_share = Crypto.AES.encrypt(share, _x.node_priv);
                    console.debug(ref, '|sinkID:', id, '|EnShare:', encrypted_share);
                    storeShareAtRandom(encrypted_share).then(i => {
                        data[group][id].push(i);
                        writeIndexFile(data).then(_ => resolve(i)).catch(error => reject(error))
                            .finally(_ => release().then(_ => null).catch(error => console.error(error)));
                    }).catch(error => releaseAndReject(error))
                }).catch(error => releaseAndReject(error))
            }).catch(error => reject(error))
        }).catch(error => reject(error))
    })
}

function generateShares(sinkKey, total_n, min_n) {
    let shares = floCrypto.createShamirsSecretShares(sinkKey, total_n * SHARES_PER_NODE, min_n * SHARES_PER_NODE * SHARE_THRESHOLD);
    let node_shares = Array(total_n);
    for (let i = 0; i < total_n; i++)
        node_shares[i] = shares.splice(0, SHARES_PER_NODE);
    return node_shares;
}

function getStoredList(group = null) {
    return new Promise((resolve, reject) => {
        readIndexFile().then(data => {
            if (group !== null) {
                if (group in data)
                    resolve(Object.keys(data.group));
                else
                    reject("Group not found in Index file");
            } else {
                let ids = {};
                for (let group in data)
                    ids[group] = Object.keys(data[group]);
                resolve(ids);
            }
        }).catch(error => reject(error))
    })
}

function getDiscardedList() {
    return new Promise((resolve, reject) => {
        DB.query("SELECT floID, discard_time FROM discardedSinks")
            .then(result => resolve(Object.fromEntries(result.map(r => [r.floID, r.discard_time]))))
            .catch(error => reject(error))
    })
}

function checkIfDiscarded(id) {
    return new Promise((resolve, reject) => {
        DB.query("SELECT discard_time FROM discardedSinks WHERE floID=?", [id])
            .then(result => resolve(result.length ? result[0].discard_time : false))
            .catch(error => reject(error))
    })
}

function discardSink(id) {
    return new Promise((resolve, reject) => {
        DB.query("INSERT INTO discardedSinks(floID) VALUE (?)", [id])
            .then(result => resolve(`Discarded ${id}`))
            .catch(error => reject(error))
    })
}

//Sink groups and chest
const sink_groups = {
    get EXCHANGE() { return "exchange" },
    get CONVERT() { return "convert" },
    get BLOCKCHAIN_BONDS() { return "blockchain_bonds" },
    get BOBS_FUND() { return "bobs_fund" },
    get list() { //total list
        return [this.EXCHANGE, this.CONVERT, this.BLOCKCHAIN_BONDS, this.BOBS_FUND]
    },
    get initial_list() { //list to generate when starting exchange
        return [this.EXCHANGE, this.CONVERT]
    },
    get generate_list() { //list allowed to generate 
        return [this.EXCHANGE, this.CONVERT, this.BLOCKCHAIN_BONDS, this.BOBS_FUND]
    }
};

const sink_ids = {}, sink_chest = {
    reset() {
        for (let i in sink_ids)
            delete sink_ids[i];
    },
    set_id(group, id, value) {
        if (!(group in sink_ids))
            sink_ids[group] = {};
        sink_ids[group][id] = value;
    },
    rm_id(group, id) {
        return delete sink_ids[group][id];
    },
    get_id(group, id) {
        return sink_ids[group][id];
    },
    list(group) {
        return Object.keys(sink_ids[group] || {});
    },
    active_list(group) {
        let ids = [];
        if (group in sink_ids)
            for (let id in sink_ids[group])
                if (sink_ids[group][id])
                    ids.push(id);
        return ids;
    },
    includes(group, id) {
        return group in sink_ids ? (id in sink_ids[group]) : null;
    },
    isActive(group, id) {
        return group in sink_ids ? (id in sink_ids && sink_ids[id]) : null;
    },
    pick(group) {
        let ids = this.list(group),
            i = floCrypto.randInt(0, ids.length - 1);
        return ids[i];
    },
    active_pick(group) {
        let ids = this.active_list(group),
            i = floCrypto.randInt(0, ids.length - 1);
        return ids[i];
    },
    find_group(id) {
        let group = null;
        for (let g in sink_ids)
            if (id in sink_ids[g]) {
                group = g; break;
            }
        return group;
    },
    get_all() {
        let ids = {};
        for (let g in sink_ids)
            ids[g] = Object.keys(sink_ids[g])
        return ids;
    }
};

module.exports = {
    init: initialize,
    getShares,
    addShare,
    generateShares,
    getStoredList,
    getDiscardedList,
    checkIfDiscarded,
    discardSink,
    set node_priv(key) {
        _x.node_priv = key;
    },
    get node_priv() {
        return _x.node_priv;
    },
    get node_id() {
        return node_id;
    },
    get node_pub() {
        return node_pub;
    },
    get sink_groups() {
        return sink_groups;
    },
    get sink_chest() {
        return sink_chest;
    }
}