/* ************************************************************************************************************************ *\
 * SDK for JavaScript!                                                                                                      *
 *                                                                                                                          *
 * COPYRIGHT © 2021 BEIJING JOINGO INFORMATION TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.                                    *
 * LICENSED UNDER THE MIT LICENSE. SEE LICENSE FILE IN THE PROJECT ROOT FOR FULL LICENSE INFORMATION.                       *
 *                                                                                                                          *
 * AUTHOR        : WANG YUCAI                                                                                               *
 * E-MAIL ADDRESS: WANGYUCAI@JOINGO.VIP                                                                                     *
 * DATE TIME     : 2021-12-31 12:27                                                                                         *
\* ************************************************************************************************************************ */

// CODEFILE: cache.ts
// FEATURE: 提供了缓存相关的方法。
// FILE-VERSION: v2021.12.31-build.1227

import { createException, Nullable, sealed, queryable, isNull } from "@joingo/sdk-core";
import { computeHash, AES } from "@joingo/sdk-security";
import { Dayjs } from "dayjs";
import Store2, { StoreBase } from "store2";

/**
 * 缓存过期模式。
 *
 * @export
 * @enum {number}
 */
export enum ExpirationMode {
    /**
     * 永不过期。
     */
    neverExpire = 0,

    /**
     * 绝对时间过期。
     */
    absoluteTimeExpire = 1,

    /**
     * 滑动过期。
     */
    slidingTimeExpire = 2,

    /**
     * 基于会话的。
     */
    session = 3,
}

/**
 * 定义了过期策略的接口。
 *
 * @export
 * @interface IExpirationPolicy
 */
export interface IExpirationPolicy {
    /**
     * 获取一个值，用于表示过期模式。
     *
     * @type {ExpirationMode}
     * @memberof IExpirationPolicy
     * @readonly
     */
    readonly mode: ExpirationMode;

    /**
     * 是否已经失效。
     *
     * @returns {boolean}
     * @memberof IExpirationPolicy
     */
    invalid(): boolean;
}

/**
 * 提供了缓存过期策略相关的基本方法。
 *
 * @export
 * @abstract
 * @class ExpirationPolicy
 * @implements {IExpirationPolicy}
 */
export abstract class ExpirationPolicy implements IExpirationPolicy {
    /**
     * 用于初始化一个 ExpirationPolicy 类型的对象实例。
     * @param {ExpirationMode} [mode=ExpirationMode.neverExpire] 过期模式。
     * @memberof ExpirationPolicy
     */
    constructor(mode: ExpirationMode = ExpirationMode.neverExpire) {
        this.mode = mode;
    }

    mode: ExpirationMode;
    abstract invalid(): boolean;
}

/**
 * 提供了基于回话的超时策略相关的方法。
 *
 * @export
 * @class SessionExpirationPolicy
 * @extends {ExpirationPolicy}
 * @implements {IExpirationPolicy}
 */
export class SessionExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
    /**
     * 用于初始化一个 SessionExpirationPolicy 类型的对象实例。
     *
     * @memberof SessionExpirationPolicy
     */
    constructor() {
        super(ExpirationMode.session);
    }

    invalid(): boolean {
        return true;
    }
}

/**
 * 提供了永不过期相关的方法。密闭的，不可以从此类型派生。
 *
 * @export
 * @class NeverExpirationPolicy
 * @extends {ExpirationPolicy}
 * @implements {IExpirationPolicy}
 */
@sealed
export class NeverExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
    /**
     * 用于初始化一个 NeverExpirationPolicy 类型的对象实例。
     * @memberof NeverExpirationPolicy
     */
    constructor() {
        super(ExpirationMode.neverExpire);
    }

    invalid(): boolean {
        return false;
    }
}

/**
 * 提供了绝对时间过期策略相关的方法。密闭的，不可以从此类型派生。
 *
 * @export
 * @class AbsoluteTimeExpirationPolicy
 * @extends {ExpirationPolicy}
 * @implements {IExpirationPolicy}
 */
@sealed
export class AbsoluteTimeExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
    /**
     * 获取一个值，用于表示绝对超时时间。
     *
     * @type {Date}
     * @memberof AbsoluteTimeExpirationPolicy
     * @readonly
     */
    readonly absoluteExpire: Date;

    private readonly _absoluteTime: Dayjs;

    private static readonly _DEFAULT_EXPIRE_MILLISECONDS: number = 1000 * 60 * 30;

    /**
     * 用于初始化一个 AbsoluteTimeExpirationPolicy 类型的对象实例。
     * @param {Date} absoluteTime
     * @memberof AbsoluteTimeExpirationPolicy
     */
    constructor(absoluteTime: Date) {
        super(ExpirationMode.absoluteTimeExpire);
        this.absoluteExpire = absoluteTime;
        this._absoluteTime = new Dayjs(absoluteTime);
    }

    invalid(): boolean {
        return this._absoluteTime.isBefore(new Date());
    }

    /**
     * 创建绝对超时策略。
     *
     * @static
     * @param {number} [milliSeconds=AbsoluteTimeExpirationPolicy._DEFAULT_EXPIRE_MILLISECONDS] 毫秒值。用于表示多少毫秒后失效。默认为 30 分钟。
     * @returns {IExpirationPolicy}
     * @memberof AbsoluteTimeExpirationPolicy
     */
    static createAbsoluteExpirationPolicy(milliSeconds: number = AbsoluteTimeExpirationPolicy._DEFAULT_EXPIRE_MILLISECONDS): IExpirationPolicy {
        return new AbsoluteTimeExpirationPolicy(new Dayjs().add(milliSeconds).toDate());
    }
}

/**
 * 提供了滑动时间超时策略相关的方法。密闭的，不可以从此类型派生。
 *
 * @export
 * @class SlidingTimeExpirationPolicy
 * @extends {ExpirationPolicy}
 * @implements {IExpirationPolicy}
 */
@sealed
export class SlidingTimeExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
    private static readonly _DEFAULT_SLIDING_MILLISECONDS: number = 5 * 1000 * 60;
    private readonly _slidingMilliSeconds: number;
    private _executeTime: Date;

    invalid(): boolean {
        return new Dayjs(this._executeTime).add(this._slidingMilliSeconds).isBefore(new Date());
    }

    /**
     * 用于初始化一个 SlidingTimeExpirationPolicy 类型的对象实例。
     *
     * @param {number} [slidingMilliSeconds=SlidingTimeExpirationPolicy._DEFAULT_SLIDING_MILLISECONDS] 滑动时间毫秒值。默认 5 分钟。
     * @memberof SlidingTimeExpirationPolicy
     */
    constructor(slidingMilliSeconds: number = SlidingTimeExpirationPolicy._DEFAULT_SLIDING_MILLISECONDS) {
        super(ExpirationMode.slidingTimeExpire);
        this._slidingMilliSeconds = slidingMilliSeconds;
        this._executeTime = new Date();
    }

    /**
     * 刷新时间。
     *
     * @memberof SlidingTimeExpirationPolicy
     */
    refresh(): void {
        console.debug(`[DEBUG]: 滑动更新策略刷新。`);
        this._executeTime = new Date();
    }
}

/**
 * 定义了缓存项目类型。
 */
export type CacheItemConstructor = {
    /**
     * 设置或获取一个字符串，用于表示缓存标识。
     *
     * @type {string}
     */
    key: string;

    /**
     * 设置或获取 * 类型的对象实例或值，用于表示需要缓存的数据。
     *
     * @type {*}
     */
    value?: any;

    /**
     * 设置或获取一个值，用于表示是否加密存储。
     *
     * @type {boolean}
     */
    secure?: boolean;

    /**
     * 设置或获取 IExpirationPolicy 类型的对象实例，用于表示缓存时效策略。
     *
     * @type {IExpirationPolicy}
     */
    expire?: IExpirationPolicy;
};

const DEFAULT_CACHEITEM: CacheItemConstructor = {
    key: "",
    value: undefined,
    secure: true,
    expire: new SessionExpirationPolicy(),
};

/**
 * 定义了内部缓存项类型的接口。
 */
type InternalCacheItemConstructor = {
    /**
     * 设置或获取一个字符串，用于表示缓存标识。
     *
     * @type {string}
     */
    key: string;

    /**
     * 设置或获取一个值，用于表示是否加密存储。
     *
     * @type {boolean}
     */
    secure: boolean;

    /**
     * 设置或获取一个值，用于表示缓存超时策略。
     *
     * @type {ExpirationMode}
     */
    expire: ExpirationMode;
};

const DEFAULT_INTERNAL_CACHEITEM_KEY: string = "vip.joingo.sdk.caching.internalTable";

let _DEFAULT_CACHE_SECUREKEY: string = "dIg40xfb6mnCEOpGh8c5YsQvNPAwBZJX";

/**
 * 配置全局缓存加密安全密钥。
 *
 * @export
 * @param {string} key 密钥。
 */
export function configureCacheSecureKey(key: string) {
    console.debug(`[DEBUG]: 尝试更新全局缓存加密密钥。`);
    _DEFAULT_CACHE_SECUREKEY = key;
}

/**
 * (异步的方法) 创建内部缓存标识。
 *
 * @returns {Promise<string>}
 * @async
 */
function createInternalCacheKeyAsync(): Promise<string> {
    return new Promise<string>((resolve, reject) => {
        try {
            resolve(computeHash(DEFAULT_INTERNAL_CACHEITEM_KEY));
        } catch (error) {
            console.error(`[ERROR]: 创建内部缓存标识名称失败。详情参见：%o`, error);
            reject(error);
        }
    });
}

/**
 * (异步的方法) 获取内部缓存数据表。
 *
 * @returns {Promise<InternalCacheItemConstructor[]>}
 * @async
 */
function getInternalCacheItemAsync(): Promise<InternalCacheItemConstructor[]> {
    return new Promise<InternalCacheItemConstructor[]>((resolve) => {
        createInternalCacheKeyAsync()
            .then((key) => {
                if (Store2.local.has(key)) {
                    const table: InternalCacheItemConstructor[] = (JSON.parse(AES.decrypt(Store2.local.get(key) as string, _DEFAULT_CACHE_SECUREKEY)) as InternalCacheItemConstructor[]) ?? [];
                    resolve(table);
                } else resolve([]);
            })
            .catch((error) => {
                resolve([]);
            });
    });
}

/**
 * 定义了数据缓存的接口。
 *
 * @export
 * @interface ICache
 */
export interface ICache {
    /**
     * (异步的方法) 用于校验指定标识名称的缓存是否存在。
     *
     * @param {string} key 缓存标识名称。
     * @returns {Promise<boolean>}
     * @memberof ICache
     * @async
     */
    existsAsync(key: string): Promise<boolean>;

    /**
     * (异步的方法) 获取指定标识名称的缓存数据。
     *
     * @template T
     * @param {string} key 缓存标识名称。
     * @returns {Promise<Nullable<T>>}
     * @memberof ICache
     * @async
     */
    getAsync<T>(key: string): Promise<Nullable<T>>;

    /**
     * (异步的方法) 设置缓存数据。
     *
     * @param {CacheItemConstructor} item 需要缓存的数据。
     * @returns {Promise<void>}
     * @memberof ICache
     * @async
     */
    setAsync(item: CacheItemConstructor): Promise<void>;

    /**
     * (异步的方法) 删除指定标识名称的缓存数据。
     *
     * @param {string} key 缓存标识名称。
     * @returns {Promise<void>}
     * @memberof ICache
     * @async
     */
    removeAsync(key: string): Promise<void>;

    /**
     * (异步的方法) 清空缓存数据。
     *
     * @returns {Promise<void>}
     * @memberof ICache
     * @async
     */
    clearAsync(): Promise<void>;
}

/**
 * 提供了数据缓存相关的方法。密闭的，不可以从此类型派生。
 *
 * @export
 * @class Cache
 * @implements {ICache}
 */
@sealed
export class Cache implements ICache {
    /**
     * (异步的方法) 用于校验指定标识名称的缓存是否存在。
     *
     * @private
     * @param {string} key 缓存标识。
     * @returns {(Promise<{ exists: boolean, internalItem: InternalCacheItemConstructor | undefined; }>)}
     * @memberof Cache
     */
    private internalExistsAsync(key: string): Promise<{ exists: boolean, internalItem: InternalCacheItemConstructor | undefined; }> {
        return new Promise<{ exists: boolean, internalItem: InternalCacheItemConstructor | undefined; }>((resolve) => {
            getInternalCacheItemAsync().then((table) => {
                const internalItem = queryable<InternalCacheItemConstructor>(table).singleOrDefault((item) => item.key === key);
                if (!internalItem) {
                    console.debug(`[DEBUG]: "${key}" 未收录在 SDK 内部缓存表中。`);
                    resolve({ exists: Store2.local.has(key) || Store2.session.has(key), internalItem: undefined });
                } else {
                    if (internalItem.secure) key = computeHash(key);
                    const cacheProvider: StoreBase = internalItem.expire === ExpirationMode.session ? Store2.session : Store2.local;
                    resolve({ exists: cacheProvider.has(key), internalItem });
                }
            });
        });
    }

    existsAsync(key: string): Promise<boolean> {
        const context = this;
        return new Promise<boolean>((resolve) => {
            context.internalExistsAsync(key).then(value => {
                resolve(value.exists);
            });
        });
    }
    getAsync<T>(key: string): Promise<Nullable<T>> {
        const context = this;
        return new Promise<Nullable<T>>((resolve) => {
            context.internalExistsAsync(key).then(value => {
                if (!value.exists) resolve(new Nullable<T>());
                else if (!value.internalItem) {
                    resolve(new Nullable<T>((Store2.local.get(key) ?? Store2.session.get(key)) as T));
                }
                else {
                    let _key = key;
                    if (value.internalItem.secure) _key = computeHash(key);
                    const storeBase: StoreBase = value.internalItem.expire === ExpirationMode.session ? Store2.session : Store2.local;
                    let cachedStr: string = storeBase.get(_key);
                    if (value.internalItem.secure)
                        cachedStr = AES.decrypt(cachedStr, _DEFAULT_CACHE_SECUREKEY);
                    const cachedItem: CacheItemConstructor = JSON.parse(cachedStr) as CacheItemConstructor;
                    if (cachedItem.expire?.invalid) {
                        console.warn(`[WARN]: 缓存数据 "${key}" 已经失效，因此返回 undefined。`);
                        return new Nullable<T>();
                    }
                    else {
                        if (cachedItem.expire?.mode === ExpirationMode.slidingTimeExpire) {
                            (cachedItem.expire as SlidingTimeExpirationPolicy).refresh();
                            cachedStr = JSON.stringify(cachedItem);
                            if (value.internalItem.secure) cachedStr = AES.encrypt(cachedStr, _DEFAULT_CACHE_SECUREKEY);
                            storeBase.set(_key, cachedStr, true);
                        }
                        return new Nullable<T>(cachedItem.value);
                    }
                }
            });
        });
    }
    setAsync(item: CacheItemConstructor): Promise<void> {
        return new Promise((resolve) => {
            if (!item.value) resolve();
            else {
                getInternalCacheItemAsync().then(table => {
                    if (!item.expire) item.expire = new SessionExpirationPolicy();
                    let key: string = item.key;
                    let jsonStr: string = JSON.stringify(item);
                    if (item.secure) {
                        key = computeHash(key);
                        jsonStr = AES.encrypt(jsonStr, _DEFAULT_CACHE_SECUREKEY);
                    }
                    if (queryable(table).any(i => i.key === item.key)) {
                        console.debug(`[DEBUG]: 尝试更新内部缓存表中标识名称为 ${item.key} 的缓存项信息。`);
                        table.forEach(i => {
                            if (i.key === item.key) {
                                i.secure = item.secure ?? false;
                                i.expire = item.expire?.mode ?? ExpirationMode.session;
                                return;
                            }
                        });
                    }
                    else {
                        table.push({ key: item.key, secure: item.secure ?? false, expire: item.expire?.mode ?? ExpirationMode.session });
                    }
                    ((item.expire?.mode ?? ExpirationMode.session) === ExpirationMode.session ? Store2.session : Store2.local).set(key, jsonStr, true);
                    Store2.local.set(computeHash(DEFAULT_INTERNAL_CACHEITEM_KEY), AES.encrypt(JSON.stringify(table), _DEFAULT_CACHE_SECUREKEY), true);
                    resolve();
                });
            }
        });
    }
    removeAsync(key: string): Promise<void> {
        const context = this;
        return new Promise(resolve => {
            context.internalExistsAsync(key).then(value => {
                if (value.exists) {
                    if (!value.internalItem) {
                        Store2.local.remove(key);
                        Store2.session.remove(key);
                        resolve();
                    }
                    else {
                        let _key: string = key;
                        if (value.internalItem.secure) _key = computeHash(key);
                        Store2.local.remove(_key);
                        Store2.session.remove(_key);
                        getInternalCacheItemAsync().then(table => {
                            const _table = queryable(table).where(item => item.key != key).toArray();
                            Store2.local.set(computeHash(DEFAULT_INTERNAL_CACHEITEM_KEY), AES.encrypt(JSON.stringify(_table), _DEFAULT_CACHE_SECUREKEY), true);
                            resolve();
                        });
                    }
                }
            });
        });
    }
    clearAsync(): Promise<void> {
        return new Promise((resolve) => {
            Store2.local.clear();
            Store2.session.clear();
            resolve();
        });
    }
}
