import { ObjectId, MongoClient } from 'mongodb';

let db;
let dbo;

/**
 * Manages database operations, управление на операциите към базата от данни
 */
class Db {
    name = 'db';

    /**
     * Initializes the database plugin, инициализация
     * @param {App} app The application instance, апликация
     */
    init(app){
        
    }

    /**
     * Starts the database plugin, стартиране
     * @param {App} app The application instance, апликация
     */
    async start(app){
        db = await MongoClient.connect(app.config.db.url, {maxPoolSize: 256});
        try {
            dbo = db.db(app.config.db.name);
            this.instance = dbo;
            for (let c of ['users', 'user_sessions', 'history', 'log', 'assets', 'scenarios', 'games', 'config']){
                try {
                    await dbo.createCollection(c);
                }catch(err){}
            }
            await dbo.collection('assets').createIndex({ name: 'text', 'description': 'text', tags: 'text' }, {name:'fts', default_language: "none" });
        }finally{
        }
    }

    /**
     * Inserts a record in a db collection, добавяне на запис в базата от данни
     * @param {string} collection The name of the collection, име на колекцията, в която да бъде добавен записа
     * @param {Object} value The object to insert, стойност на записа
     * @returns {ObjectId} Inserted Id, идентификатор на новия запис
     */
    async create(collection, value){
        try {
            delete value._id;
            return await dbo.collection(collection).insertOne(value);
        }finally{
        }
    }

    /**
     * Loads a record from db collection, извличане на запис от базата от данни
     * @param {string} collection The name of the collection, име на колекцията, съдържаща записа
     * @param {Object} key Record identifier, идентификатор на записа
     * @param {Object} projection What data to take from the object, проекция на очаквания резултат
     * @returns {Object} A record, запис
     */
    async get(collection, key, projection){
        try {
            let res = await dbo.collection(collection).findOne(key, projection ? {projection} : undefined)
            return res;
        }finally{
        }
    }

    /**
     * Performs a database query, проста заявка за търсене в базата от данни
     * @param {string} collection Collection name, име на колекция
     * @param {Object} query A mongo db query, заявка
     * @returns {Object[]} Array of records, масив от записи
     */
    async list(collection, query){
        try {
            let cursor = dbo.collection(collection).find(query.query, query.project ? {projection: query.project} : undefined);
            query.sort && cursor.sort(query.sort);
            query.skip && cursor.skip(query.skip);
            query.limit && cursor.limit(query.limit);
            let count = await dbo.collection(collection).countDocuments(query.query);
            let result = await cursor.toArray();
            return {data: result, count:count};
        }finally{
        }
    }

    /**
     * Performs a database aggregation according to a given pipeline, сложна заявка (агрегираща) към базата от данни
     * @param {string} collection Database collection name, име на колекция
     * @param {Object} specs aggregation definition (the pipeline), дефиниция на заявката
     * @returns {Object[]} Array of records, списък от записи
     */
    async aggregate(collection, specs){
        try {
            let cursor = dbo.collection(collection);
            let aggCursor = cursor.aggregate(specs);
            let result = await aggCursor.toArray();
            return result.length == 1 ? result[0] : result;
        }finally{
        }
    }

    /**
     * Finds the distinct values for a specified field across a single collection, извличане на списък от уникални записи в колекция по зададени критерии
     * @param {string} collection Database collection name, име на колекцията
     * @param {Object} key The target field for the distinction, целеви атрибут, по който се търси уникалност
     * @param {Object} query filter to be applied, филтър на записите в колекцията
     * @returns {Object[]} Array of records, списък от записи
     */
    async distinct(collection, key, query){
        try {
            return await dbo.collection(collection).distinct(key, query);
        }finally{
        }
    }
    
    /**
     * Updates a record in database by given key and value, обновяване на запис в базата данни по дадени ключ и стойност
     * @param {Object} collection DB collection, име на колекцията
     * @param {Object} key The key/query which identifies the record to be updated, ключ или заявка за идентификация на съществуващия запис
     * @param {Object} value The new value for the record, нова стойност на записа
     * @returns {Object} The result from the update operation, резултат от операцията
     */
    async update(collection, key, value){
        let r;
        try {
            delete value._id;
            r = await dbo.collection(collection).replaceOne(key, value, {upsert:true});
        }finally{
            return r;
        }
    }

    /**
     * Performs partial update on a record by given key and partial value, частично обновяване на запис в базата от данни
     * @param {Object} collection Database collection, име на колекция
     * @param {Object} key The key/query which identifies the record to be updated, ключ или заявка за идентификация на целевите обекти
     * @param {Object} value The partial value to be updated, дефиниция на частичното обновяване
     * @returns {Object} The result from the update operation, резултат от операцията
     */
    async updateSet(collection, key, value){
        let r;
        try {
            r = await dbo.collection(collection).updateMany(key, value);
        }finally{
            return r;
        }
    }
    
    /**
     * Removes a record from the database by given key, изтриване на запис от базата от данни по зададен критерий
     * @param {Object} collection Database collection, име на колекцията
     * @param {Object} key The key/query which identifies the record to be updated, ключ/заявка за идентификация на целевите записи
     */
    async remove(collection, key){
        try {
            await dbo.collection(collection).deleteMany(key);
        }finally{
        }
    }
    

    // convertToObjectId(object, key, recursive, result){
    //     if (object && object[key]){
    //         if (Array.isArray(object[key])){
    //             object[key].forEach((v, i, a)=>{
    //                 a[i] = this.ObjectId(v);
    //                 result && result.push(a[i]);
    //             })
    //         }else{
    //             let oid = this.ObjectId(object[key])
    //             object[key] = oid;
    //             result && result.push(oid);
    //         }
    //     }
    //     if (recursive){
    //         for (var k in object){
    //             if (typeof(object[k]) == 'object'){
    //                 this.convertToObjectId(object[k], key, recursive, result);
    //             }
    //         }
    //     }
    // }

    sanitizeQuery(q){
        if (!q) return;
        Object.getOwnPropertyNames(q).forEach(n=>{
            if (n.startsWith('$')){
                //sanitize $
                console.warn('Deleting suspicious query key', n)
                delete q[n];
            }
            if (typeof(q[n]) == 'object'){
                this.sanitizeQuery(q[n]);
            }
            //prepare for DB
            if (n.startsWith('*')){
                let n1 = '$' + n.slice(1);
                q[n1] = q[n];
                delete q[n];
                n = n1;
            }
            if (['$where', '$group', '$merge', '$lookup', '$accumulator', '$function'].includes(n)){
                delete q[n];
                console.warn('Deleting suspicious query key', n);
            }
        })
        if (q.$oid){
            q.$oid.forEach(o=>{
              q[o] = this.ObjectId(q[o]);
            })
            delete q.$oid;
        }
        return q;
    }

    sanitizeProjection(p){
        if (!p) return { _id: 1 };
        let props = Object.getOwnPropertyNames(p);

        props.forEach(n=>{
            if (typeof p[n] == 'object'){
                console.warn('Deleting suspicious projection key, object', n)
                delete p[n]
            }else if (!n.match(/^[0-9a-zA-Z\._\$\#\-\:]*$/)){
                console.warn('Deleting suspicious projection key', n)
                delete p[n]
            }else if (typeof p[n] !== 'number' && !p[n].match(/^[0-9a-zA-Z\._\$\#\-]*$/)){
                console.warn('Deleting suspicious projection value', n, p[n])
                delete p[n]
            }
        })
        
        props = Object.getOwnPropertyNames(p);
        if (props.length == 0){
            return { _id: 1 };
        }
    }

    checkLimit(q){
        if (!q) return;
        if (!q.limit) q.limit = 100;
        if (q.limit == 'all') delete q.limit;
    }

    ObjectId(id){
        return new ObjectId(id);
    }

    /**
     * Gets last asset Id from database, намира последния пореден идентификатор на обект в базата от данни
     * @returns {Number} Last Asset Id, последен (най-голям) идентификатор
     */
    async getId(collection){
        let newId = (await dbo.collection('config').findOneAndUpdate(
            { 'counter': { $exists: true } },
            { $inc: { 'counter.value': 1 } },
            { upsert: true, returnDocument: 'after' }
        )).counter.value;
        return newId;
    }

    async stop(){
        await db.close();
    }
}

export { Db };