'use strict';

const EventEmitter = require('events');
const JSONB = require('json-buffer');

const loadStore = options => {
    const adapters = {
        redis: '@keyv/redis',
        rediss: '@keyv/redis',
        mongodb: '@keyv/mongo',
        mongo: '@keyv/mongo',
        sqlite: '@keyv/sqlite',
        postgresql: '@keyv/postgres',
        postgres: '@keyv/postgres',
        mysql: '@keyv/mysql',
        etcd: '@keyv/etcd',
        offline: '@keyv/offline',
        tiered: '@keyv/tiered',
    };
    if (options.adapter || options.uri) {
        const adapter = options.adapter || /^[^:+]*/.exec(options.uri)[0];
        return new (require(adapters[adapter]))(options);
    }

    return new Map();
};

const iterableAdapters = [
    'sqlite',
    'postgres',
    'mysql',
    'mongo',
    'redis',
    'tiered',
];

class Keyv extends EventEmitter {
    constructor(uri, {emitErrors = true, ...options} = {}) {
        super();
        this.opts = {
            namespace: 'keyv',
            serialize: JSONB.stringify,
            deserialize: JSONB.parse,
            ...((typeof uri === 'string') ? {uri} : uri),
            ...options,
        };

        if (!this.opts.store) {
            const adapterOptions = {...this.opts};
            this.opts.store = loadStore(adapterOptions);
        }

        if (this.opts.compression) {
            const compression = this.opts.compression;
            this.opts.serialize = compression.serialize.bind(compression);
            this.opts.deserialize = compression.deserialize.bind(compression);
        }

        if (typeof this.opts.store.on === 'function' && emitErrors) {
            this.opts.store.on('error', error => this.emit('error', error));
        }

        this.opts.store.namespace = this.opts.namespace;

        const generateIterator = iterator => async function * () {
            for await (const [key, raw] of typeof iterator === 'function'
                ? iterator(this.opts.store.namespace)
                : iterator) {
                const data = await this.opts.deserialize(raw);
                if (this.opts.store.namespace && !key.includes(this.opts.store.namespace)) {
                    continue;
                }

                if (typeof data.expires === 'number' && Date.now() > data.expires) {
                    this.delete(key);
                    continue;
                }

                yield [this._getKeyUnprefix(key), data.value];
            }
        };

        // Attach iterators
        if (typeof this.opts.store[Symbol.iterator] === 'function' && this.opts.store instanceof Map) {
            this.iterator = generateIterator(this.opts.store);
        } else if (typeof this.opts.store.iterator === 'function' && this.opts.store.opts
            && this._checkIterableAdaptar()) {
            this.iterator = generateIterator(this.opts.store.iterator.bind(this.opts.store));
        }
    }

    _checkIterableAdaptar() {
        return iterableAdapters.includes(this.opts.store.opts.dialect)
            || iterableAdapters.findIndex(element => this.opts.store.opts.url.includes(element)) >= 0;
    }

    _getKeyPrefix(key) {
        return `${this.opts.namespace}:${key}`;
    }

    _getKeyPrefixArray(keys) {
        return keys.map(key => `${this.opts.namespace}:${key}`);
    }

    _getKeyUnprefix(key) {
        return key
            .split(':')
            .splice(1)
            .join(':');
    }

    get(key, options) {
        const {store} = this.opts;
        const isArray = Array.isArray(key);
        const keyPrefixed = isArray ? this._getKeyPrefixArray(key) : this._getKeyPrefix(key);
        if (isArray && store.getMany === undefined) {
            const promises = [];
            for (const key of keyPrefixed) {
                promises.push(Promise.resolve()
                    .then(() => store.get(key))
                    .then(data => (typeof data === 'string') ? this.opts.deserialize(data) : (this.opts.compression ? this.opts.deserialize(data) : data))
                    .then(data => {
                        if (data === undefined || data === null) {
                            return undefined;
                        }

                        if (typeof data.expires === 'number' && Date.now() > data.expires) {
                            return this.delete(key).then(() => undefined);
                        }

                        return (options && options.raw) ? data : data.value;
                    }),
                );
            }

            return Promise.allSettled(promises)
                .then(values => {
                    const data = [];
                    for (const value of values) {
                        data.push(value.value);
                    }

                    return data;
                });
        }

        return Promise.resolve()
            .then(() => isArray ? store.getMany(keyPrefixed) : store.get(keyPrefixed))
            .then(data => (typeof data === 'string') ? this.opts.deserialize(data) : (this.opts.compression ? this.opts.deserialize(data) : data))
            .then(data => {
                if (data === undefined || data === null) {
                    return undefined;
                }

                if (isArray) {
                    const result = [];

                    for (let row of data) {
                        if ((typeof row === 'string')) {
                            row = this.opts.deserialize(row);
                        }

                        if (row === undefined || row === null) {
                            result.push(undefined);
                            continue;
                        }

                        if (typeof row.expires === 'number' && Date.now() > row.expires) {
                            this.delete(key).then(() => undefined);
                            result.push(undefined);
                        } else {
                            result.push((options && options.raw) ? row : row.value);
                        }
                    }

                    return result;
                }

                if (typeof data.expires === 'number' && Date.now() > data.expires) {
                    return this.delete(key).then(() => undefined);
                }

                return (options && options.raw) ? data : data.value;
            });
    }

    set(key, value, ttl) {
        const keyPrefixed = this._getKeyPrefix(key);
        if (typeof ttl === 'undefined') {
            ttl = this.opts.ttl;
        }

        if (ttl === 0) {
            ttl = undefined;
        }

        const {store} = this.opts;

        return Promise.resolve()
            .then(() => {
                const expires = (typeof ttl === 'number') ? (Date.now() + ttl) : null;
                if (typeof value === 'symbol') {
                    this.emit('error', 'symbol cannot be serialized');
                }

                value = {value, expires};
                return this.opts.serialize(value);
            })
            .then(value => store.set(keyPrefixed, value, ttl))
            .then(() => true);
    }

    delete(key) {
        const {store} = this.opts;
        if (Array.isArray(key)) {
            const keyPrefixed = this._getKeyPrefixArray(key);
            if (store.deleteMany === undefined) {
                const promises = [];
                for (const key of keyPrefixed) {
                    promises.push(store.delete(key));
                }

                return Promise.allSettled(promises)
                    .then(values => values.every(x => x.value === true));
            }

            return Promise.resolve()
                .then(() => store.deleteMany(keyPrefixed));
        }

        const keyPrefixed = this._getKeyPrefix(key);
        return Promise.resolve()
            .then(() => store.delete(keyPrefixed));
    }

    clear() {
        const {store} = this.opts;
        return Promise.resolve()
            .then(() => store.clear());
    }

    has(key) {
        const keyPrefixed = this._getKeyPrefix(key);
        const {store} = this.opts;
        return Promise.resolve()
            .then(async () => {
                if (typeof store.has === 'function') {
                    return store.has(keyPrefixed);
                }

                const value = await store.get(keyPrefixed);
                return value !== undefined;
            });
    }

    disconnect() {
        const {store} = this.opts;
        if (typeof store.disconnect === 'function') {
            return store.disconnect();
        }
    }
}

module.exports = Keyv;