import pg_promise from "pg-promise";
import StorageInterface from "../interface.js";
import { concat, toArray } from "../../common.js";
const defaults = {
    schema: "flexsearch",
    user: "postgres",
    pass: "postgres",
    name: "postgres",
    host: "localhost",
    port: "5432"
},
      pgp = pg_promise(),
      VERSION = 1,
      MAXIMUM_QUERY_VARS = 16000,
      fields = ["map", "ctx", "reg", "tag", "cfg"],
      types = {
    text: "text",
    char: "text",
    varchar: "text",
    string: "text",
    number: "int",
    numeric: "int",
    integer: "int",
    smallint: "int",
    tinyint: "int",
    mediumint: "int",
    int: "int",
    int8: "int",
    uint8: "int",
    int16: "int",
    uint16: "int",
    int32: "int",
    uint32: "bigint",
    int64: "bigint",
    bigint: "bigint"
};


function sanitize(str) {
    return str.toLowerCase().replace(/[^a-z0-9_]/g, "");
}

let DB, TRX;

/**
 * @constructor
 * @implements StorageInterface
 */

export default function PostgresDB(name, config = {}) {
    if (!this || this.constructor !== PostgresDB) {
        return new PostgresDB(name, config);
    }
    if ("object" == typeof name) {
        config = name;
        name = config.name;
    }
    if (!name) {
        console.info("Default storage space was used, because a name was not passed.");
    }
    this.id = (config.schema ? sanitize(config.schema) : defaults.schema) + (name ? "_" + sanitize(name) : "");
    this.field = config.field ? "_" + sanitize(config.field) : "";
    this.type = config.type ? types[config.type.toLowerCase()] : "text";
    this.support_tag_search = !0;
    if (!this.type) throw new Error("Unknown type of ID '" + config.type + "'");
    this.db = DB || (DB = config.db || null);
    Object.assign(defaults, config);
    this.db && delete defaults.db;
}

PostgresDB.prototype.mount = function (flexsearch) {

    if (flexsearch.index) {
        return flexsearch.mount(this);
    }
    flexsearch.db = this;
    return this.open();
};

PostgresDB.prototype.open = async function () {

    if (!this.db) {
        this.db = DB || (DB = pgp(`postgres://${defaults.user}:${encodeURIComponent(defaults.pass)}@${defaults.host}:${defaults.port}/${defaults.name}`));
    }

    const exist = await this.db.oneOrNone(`
        SELECT EXISTS (
            SELECT 1 
            FROM information_schema.schemata 
            WHERE schema_name = '${this.id}'
        );
    `);
    if (!exist || !exist.exists) {
        await this.db.none(`CREATE SCHEMA IF NOT EXISTS ${this.id};`);
    }

    for (let i = 0; i < fields.length; i++) {
        const exist = await this.db.oneOrNone(`
            SELECT EXISTS (
                SELECT 1 FROM pg_tables
                WHERE schemaname = '${this.id}' AND tablename = '${fields[i] + ("reg" !== fields[i] ? this.field : "")}'
            );
        `);
        if (exist && exist.exists) continue;

        const type = "text" === this.type ? "varchar(128)" : this.type;

        switch (fields[i]) {
            case "map":
                await this.db.none(`
                    CREATE TABLE IF NOT EXISTS ${this.id}.map${this.field}(
                        key varchar(128) NOT NULL,
                        res smallint     NOT NULL,
                        id  ${type} NOT NULL
                    );
                    CREATE INDEX IF NOT EXISTS ${this.id}_map_index${this.field}
                        ON ${this.id}.map${this.field} (key);
                    CREATE INDEX IF NOT EXISTS ${this.id}_map_id${this.field}
                        ON ${this.id}.map${this.field} (id);
                `);
                break;

            case "ctx":
                await this.db.none(`
                    CREATE TABLE IF NOT EXISTS ${this.id}.ctx${this.field}(
                        ctx varchar(128) NOT NULL,
                        key varchar(128) NOT NULL,
                        res smallint     NOT NULL,
                        id  ${type} NOT NULL
                    );
                    CREATE INDEX IF NOT EXISTS ${this.id}_ctx_index${this.field}
                        ON ${this.id}.ctx${this.field} (ctx, key);
                    CREATE INDEX IF NOT EXISTS ${this.id}_ctx_id${this.field}
                        ON ${this.id}.ctx${this.field} (id);
                `);
                break;

            case "tag":
                await this.db.none(`
                    CREATE TABLE IF NOT EXISTS ${this.id}.tag${this.field}(
                        tag varchar(128) NOT NULL,
                        id  ${type} NOT NULL
                    );
                    CREATE INDEX IF NOT EXISTS ${this.id}_tag_index${this.field}
                        ON ${this.id}.tag${this.field} (tag);
                    CREATE INDEX IF NOT EXISTS ${this.id}_tag_id${this.field}
                        ON ${this.id}.tag${this.field} (id);
                `);
                break;

            case "reg":
                await this.db.none(`
                    CREATE TABLE IF NOT EXISTS ${this.id}.reg(
                        id ${type} NOT NULL
                            CONSTRAINT ${this.id}_reg_pk
                                PRIMARY KEY,
                        doc text DEFAULT NULL
                    );
                `).catch(() => {});
                break;

            case "cfg":
                await this.db.none(`
                    CREATE TABLE IF NOT EXISTS ${this.id}.cfg${this.field}(
                        cfg text NOT NULL
                    );
                `);
                break;
        }
    }

    return this.db;
};

PostgresDB.prototype.close = function () {

    this.db = null;
    return this;
};

PostgresDB.prototype.destroy = function () {
    return this.db.none(`
        DROP TABLE IF EXISTS ${this.id}.map${this.field};
        DROP TABLE IF EXISTS ${this.id}.ctx${this.field};
        DROP TABLE IF EXISTS ${this.id}.tag${this.field};
        DROP TABLE IF EXISTS ${this.id}.cfg${this.field};
        DROP TABLE IF EXISTS ${this.id}.reg;
    `);
};

PostgresDB.prototype.clear = function () {
    return this.db.none(`
        TRUNCATE TABLE ${this.id}.map${this.field};
        TRUNCATE TABLE ${this.id}.ctx${this.field};
        TRUNCATE TABLE ${this.id}.tag${this.field};
        TRUNCATE TABLE ${this.id}.cfg${this.field};
        TRUNCATE TABLE ${this.id}.reg;
    `);
};

function create_result(rows, resolve, enrich) {
    if (resolve) {
        for (let i = 0; i < rows.length; i++) {
            if (enrich) {
                if (rows[i].doc) {
                    rows[i].doc = JSON.parse(rows[i].doc);
                }
            } else {
                rows[i] = rows[i].id;
            }
        }
        return rows;
    } else {
        const arr = [];
        for (let i = 0, row; i < rows.length; i++) {
            row = rows[i];
            arr[row.res] || (arr[row.res] = []);
            arr[row.res].push(enrich ? row : row.id);
        }
        return arr;
    }
}

PostgresDB.prototype.get = function (key, ctx, limit = 0, offset = 0, resolve = !0, enrich = !1, tags) {
    let rows,
        stmt = '',
        params = ctx ? [ctx, key] : [key],
        table = this.id + (ctx ? ".ctx" : ".map") + this.field;

    if (tags) {
        for (let i = 0, count = params.length + 1; i < tags.length; i += 2) {
            stmt += ` AND ${table}.id IN (SELECT id FROM ${this.id}.tag_${sanitize(tags[i])} WHERE tag = $${count++})`;
            params.push(tags[i + 1]);
        }
    }
    if (ctx) {
        rows = this.db.any(`
            SELECT ${table}.id 
                   ${resolve ? "" : ", res"}
                   ${enrich ? ", doc" : ""}
            FROM ${table}
            ${enrich ? `
                LEFT JOIN ${this.id}.reg ON ${this.id}.reg.id = ${table}.id
            ` : ""}  
            WHERE ctx = $1 AND key = $2 ${stmt}
            ORDER BY res
            ${limit ? "LIMIT " + limit : ""}
            ${offset ? "OFFSET " + offset : ""}`, params);
    } else {
        rows = this.db.any(`
            SELECT ${table}.id
                   ${resolve ? "" : ", res"}
                   ${enrich ? ", doc" : ""}
            FROM ${table}
            ${enrich ? `
                LEFT JOIN ${this.id}.reg ON ${this.id}.reg.id = ${table}.id
            ` : ""}
            WHERE key = $1 ${stmt}
            ORDER BY res
            ${limit ? "LIMIT " + limit : ""}
            ${offset ? "OFFSET " + offset : ""}`, params);
    }
    return rows.then(function (rows) {
        return create_result(rows, resolve, enrich);
    });
};

PostgresDB.prototype.tag = function (tag, limit = 0, offset = 0, enrich = !1) {
    const table = this.id + ".tag" + this.field,
          promise = this.db.any(`
        SELECT ${table}.id
               ${enrich ? ", doc" : ""}
        FROM ${table}
        ${enrich ? `
            LEFT JOIN ${this.id}.reg ON ${this.id}.reg.id = ${table}.id
        ` : ""}
        WHERE tag = $1
        ${limit ? "LIMIT " + limit : ""}
        ${offset ? "OFFSET " + offset : ""}`, [tag]);

    enrich || promise.then(function (rows) {
        return create_result(rows, !0, !1);
    });
    return promise;
};

PostgresDB.prototype.enrich = async function (ids) {
    let result = [];
    if ("object" != typeof ids) {
        ids = [ids];
    }
    for (let count = 0; count < ids.length;) {
        const chunk = ids.length - count > MAXIMUM_QUERY_VARS ? ids.slice(count, count + MAXIMUM_QUERY_VARS) : count ? ids.slice(count) : ids;
        count += chunk.length;
        let stmt = "";
        for (let i = 1; i <= chunk.length; i++) {
            stmt += (stmt ? "," : "") + "$" + i;
        }
        const res = await this.db.any(`
            SELECT id, doc 
            FROM ${this.id}.reg
            WHERE id IN (${stmt})`, ids);
        if (res && res.length) {
            for (let i = 0, doc; i < res.length; i++) {
                if (doc = res[i].doc) {
                    res[i].doc = JSON.parse(doc);
                }
            }
            result.push(res);
        }
    }
    return 1 === result.length ? result[0] : 1 < result.length ? concat(result) : result;
};

PostgresDB.prototype.has = function (id) {
    return this.db.oneOrNone("SELECT EXISTS(SELECT 1 FROM " + this.id + ".reg WHERE id = $1)", [id]).then(function (result) {
        return !!(result && result.exists);
    });
};

PostgresDB.prototype.search = function (flexsearch, query, limit = 100, offset = 0, suggest = !1, resolve = !0, enrich = !1, tags) {

    let rows;

    if (1 < query.length && flexsearch.depth) {
        let where = "",
            params = [],
            keyword = query[0],
            term,
            count = 1;


        for (let i = 1; i < query.length; i++) {
            term = query[i];
            const swap = flexsearch.bidirectional && term > keyword;
            where += (where ? " OR " : "") + `(ctx = $${count++} AND key = $${count++})`;
            params.push(swap ? term : keyword, swap ? keyword : term);
            keyword = term;
        }

        if (tags) {
            where = "(" + where + ")";
            for (let i = 0; i < tags.length; i += 2) {
                where += ` AND id IN (SELECT id FROM ${this.id}.tag_${sanitize(tags[i])} WHERE tag = $${count++})`;
                params.push(tags[i + 1]);
            }
        }

        rows = this.db.any(`
            SELECT r.id 
                   ${resolve ? "" : ", res"}
                   ${enrich ? ", doc" : ""}
            FROM (
                SELECT id, count(*) as count,
                       ${suggest ? "SUM" : "SUM"}(res) as res
                FROM ${this.id}.ctx${this.field}
                WHERE ${where}
                GROUP BY id
            ) as r
            ${enrich ? `
                LEFT JOIN ${this.id}.reg ON ${this.id}.reg.id = r.id
            ` : ""} 
            ${suggest ? "" : "WHERE count = " + (query.length - 1)}
            ORDER BY ${suggest ? "count DESC, res" : "res"}
            ${limit ? "LIMIT " + limit : ""}
            ${offset ? "OFFSET " + offset : ""}
        `, params);
    } else {
        let where = "",
            count = 1,
            query_length = query.length;

        for (let i = 0; i < query_length; i++) {
            where += (where ? "," : "") + "$" + count++;
        }
        where = "key " + (1 < query_length ? "IN (" + where + ")" : "= " + where);

        if (tags) {
            where = "(" + where + ")";
            for (let i = 0; i < tags.length; i += 2) {
                where += ` AND id IN (SELECT id FROM ${this.id}.tag_${sanitize(tags[i])} WHERE tag = $${count++})`;
                query.push(tags[i + 1]);
            }
        }

        rows = this.db.any(`
            SELECT r.id 
                   ${resolve ? "" : ", res"}   
                   ${enrich ? ", doc" : ""}
            FROM (
                SELECT id, count(*) as count,
                       ${suggest ? "SUM" : "SUM"}(res) as res
                FROM ${this.id}.map${this.field}
                WHERE ${where}
                GROUP BY id
            ) as r
            ${enrich ? `
                LEFT JOIN ${this.id}.reg ON ${this.id}.reg.id = r.id
            ` : ""}   
            ${suggest ? "" : "WHERE count = " + query_length}
            ORDER BY ${suggest ? "count DESC, res" : "res"}
            ${limit ? "LIMIT " + limit : ""}
            ${offset ? "OFFSET " + offset : ""}
        `, query);
    }

    return rows.then(function (rows) {
        return create_result(rows, resolve, enrich);
    });
};

PostgresDB.prototype.info = function () {};

PostgresDB.prototype.transaction = function (task) {
    const self = this;
    return this.db.tx(function (trx) {
        return task.call(self, trx);
    });
};

PostgresDB.prototype.commit = async function (flexsearch) {
    let tasks = flexsearch.commit_task,
        removals = [];

    flexsearch.commit_task = [];

    for (let i = 0, task; i < tasks.length; i++) {
        /** @dict */
        task = tasks[i];
        if (task.del) {
            removals.push(task.del);
        } else if (task.ins) {}
    }

    if (removals.length) {
        await this.remove(removals);
    }

    if (!flexsearch.reg.size) {
        return;
    }

    await this.transaction(function (trx) {

        const batch = [];

        if (flexsearch.store) {
            let data = [],
                stmt = new pgp.helpers.ColumnSet(["id", "doc"], {
                table: this.id + ".reg"
            });

            for (const item of flexsearch.store.entries()) {
                const id = item[0],
                      doc = item[1];


                data.push({ id, doc: doc && JSON.stringify(doc) });
                if (data.length === MAXIMUM_QUERY_VARS) {
                    let insert = pgp.helpers.insert(data, stmt);
                    batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
                    data = [];
                }
            }
            if (data.length) {
                let insert = pgp.helpers.insert(data, stmt);
                batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
            }
        } else if (!flexsearch.bypass) {
            let data = [],
                stmt = new pgp.helpers.ColumnSet(["id"], {
                table: this.id + ".reg"
            });

            for (const id of flexsearch.reg.keys()) {

                data.push({ id });
                if (data.length === MAXIMUM_QUERY_VARS) {
                    let insert = pgp.helpers.insert(data, stmt);
                    batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
                    data = [];
                }
            }
            if (data.length) {
                let insert = pgp.helpers.insert(data, stmt);
                batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
            }
        }

        if (flexsearch.map.size) {
            let data = [],
                stmt = new pgp.helpers.ColumnSet(["key", "res", "id"], {
                table: this.id + ".map" + this.field
            });

            for (const item of flexsearch.map) {
                const key = item[0],
                      arr = item[1];


                for (let i = 0, ids; i < arr.length; i++) {
                    if ((ids = arr[i]) && ids.length) {

                        for (let j = 0; j < ids.length; j++) {

                            data.push({
                                key: key,
                                res: i,
                                id: ids[j]
                            });
                            if (data.length === MAXIMUM_QUERY_VARS) {
                                let insert = pgp.helpers.insert(data, stmt);
                                batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
                                data = [];
                            }
                        }
                    }
                }
            }
            if (data.length) {
                let insert = pgp.helpers.insert(data, stmt);
                batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
            }
        }

        if (flexsearch.ctx.size) {
            let data = [],
                stmt = new pgp.helpers.ColumnSet(["ctx", "key", "res", "id"], {
                table: this.id + ".ctx" + this.field
            });

            for (const ctx of flexsearch.ctx) {
                const ctx_key = ctx[0],
                      ctx_value = ctx[1];


                for (const item of ctx_value) {
                    const key = item[0],
                          arr = item[1];


                    for (let i = 0, ids; i < arr.length; i++) {
                        if ((ids = arr[i]) && ids.length) {

                            for (let j = 0; j < ids.length; j++) {

                                data.push({
                                    ctx: ctx_key,
                                    key: key,
                                    res: i,
                                    id: ids[j]
                                });
                                if (data.length === MAXIMUM_QUERY_VARS) {
                                    let insert = pgp.helpers.insert(data, stmt);
                                    batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
                                    data = [];
                                }
                            }
                        }
                    }
                }
            }
            if (data.length) {
                let insert = pgp.helpers.insert(data, stmt);
                batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
            }
        }

        if (flexsearch.tag) {
            let data = [],
                stmt = new pgp.helpers.ColumnSet(["tag", "id"], {
                table: this.id + ".tag" + this.field
            });

            for (const item of flexsearch.tag) {
                const tag = item[0],
                      ids = item[1];

                if (!ids.length) continue;
                for (let j = 0; j < ids.length; j++) {
                    data.push({ tag, id: ids[j] });
                    if (data.length === MAXIMUM_QUERY_VARS) {
                        let insert = pgp.helpers.insert(data, stmt);
                        batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
                        data = [];
                    }
                }
            }
            if (data.length) {
                let insert = pgp.helpers.insert(data, stmt);
                batch.push(trx.none(insert.replace(/^(insert into )"([^"]+)"/, '$1 $2')));
            }
        }

        if (batch.length) {
            return trx.batch(batch);
        }
    });

    flexsearch.map.clear();
    flexsearch.ctx.clear();
    flexsearch.tag && flexsearch.tag.clear();
    flexsearch.store && flexsearch.store.clear();
    flexsearch.document || flexsearch.reg.clear();
};

PostgresDB.prototype.remove = function (ids) {

    if (!ids && 0 !== ids) {
        return;
    }
    if ("object" != typeof ids) {
        ids = [ids];
    }
    if (!ids.length) {
        return;
    }

    return this.transaction(function (trx) {

        ids = [ids];
        return trx.batch([trx.none({ text: "DELETE FROM " + this.id + ".map" + this.field + " WHERE id = ANY ($1)", rowMode: "array" }, ids), trx.none({ text: "DELETE FROM " + this.id + ".ctx" + this.field + " WHERE id = ANY ($1)", rowMode: "array" }, ids), trx.none({ text: "DELETE FROM " + this.id + ".tag" + this.field + " WHERE id = ANY ($1)", rowMode: "array" }, ids), trx.none({ text: "DELETE FROM " + this.id + ".reg WHERE id = ANY ($1)", rowMode: "array" }, ids)]);
    });
};