/*
 * Copyright (c) 2022.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import {EventEmitter} from "events";
import {Job, scheduleJob} from "node-schedule";
import {Redis} from "ioredis";

export interface RedisExpireCacheOptions<T> {
    prefix: string,
    suffix?: string,
    seconds: number,
    getKey?: (key: T) => string
}

export class RedisCacheEventEmitter<T extends string | number | Record<string, any>> extends EventEmitter {
    protected expireHandler: Job | undefined;
    protected expireMap: Record<string, any>;

    constructor(protected redisDatabase: Redis, public readonly options: RedisExpireCacheOptions<T>) {
        super();

        this.expireMap = {};
        this.registerExpireHandler();
    }

    protected registerExpireHandler() {
        if (this.expireHandlerIsRunning()) {
            return;
        }

        this.expireHandler = scheduleJob('* * * * * *', async () => {
            const timeNow = new Date().getTime();
            const ttlPipeline = this.redisDatabase.pipeline();

            for (let key in this.expireMap) {
                if (!this.expireMap.hasOwnProperty(key)) continue;
                ttlPipeline.ttl(key);
            }

            const result = await ttlPipeline.exec();

            const dropPipeline = this.redisDatabase.pipeline();

            let iteration: number = 0;

            for (let key in this.expireMap) {
                if (!this.expireMap.hasOwnProperty(key)) continue;

                if (this.expireMap[key] < timeNow) {
                    const redisTime = result[iteration] ? result[iteration + 1] : -1;

                    if (typeof redisTime !== 'number' || redisTime <= 0) {
                        dropPipeline.del(key);
                        delete this.expireMap[key];
                        this.emit('expired', key);
                    } else {
                        this.expireMap[key] = timeNow + redisTime;
                    }
                }

                iteration = iteration + 2;
            }

            await dropPipeline.exec();
        });
    }

    public expireHandlerIsRunning(): boolean {
        return typeof this.expireHandler !== 'undefined' && this.expireHandler.nextInvocation() !== null;
    }

    public async isExpired(id: T): Promise<boolean> {
        const key : string = this.getKey(id);

        if (key in this.expireMap) {
            return false;
        }

        const ttl = await this.redisDatabase.ttl(key);

        return ttl <= 0;
    }

    public async get(id: T): Promise<any> {
        const key : string = this.getKey(id);
        try {
            let entry = await this.redisDatabase.get(key);
            if (entry === null) {
                return undefined;
            }

            return JSON.parse(entry);
        } catch (e) {
            return undefined;
        }
    }

    public async set(id: T, value?: any, seconds?: number) {
        const key : string = this.getKey(id);

        seconds = seconds ?? this.options.seconds;

        if(typeof value === 'undefined') {
            const expireSet: number = await this.redisDatabase.expire(key, seconds);

            if (expireSet === 0) {
                await this.redisDatabase.set(key, seconds * 1000, 'EX', seconds)
            }
        } else {
            await this.redisDatabase.set(key, JSON.stringify(value), 'EX', seconds)
        }

        this.expireMap[key] = new Date().getTime() + (seconds * 1000);
    }

    public async drop(id: T): Promise<boolean> {
        const key : string = this.getKey(id);

        if (key in this.expireMap) {
            delete this.expireMap[key];
        }

        return await this.redisDatabase.del(key) === 1;
    }

    public getKey(id?: T) {
        let prefix: string = this.options.prefix + '.cache';

        if (typeof this.options.suffix !== 'undefined') {
            prefix += '.' + this.options.suffix;
        }

        if (typeof id === 'undefined') {
            return prefix;
        }

        if (typeof this.options.getKey !== 'undefined') {
            return prefix + ':' + this.options.getKey(id);
        }

        if (typeof id === 'string' || typeof id === 'number') {
            return prefix + ':' + id;
        }

        return prefix;
    }
}
