import { ID } from '@vendure/common/lib/shared-types';

import { InjectableStrategy } from '../../common/types/injectable-strategy';
import { UserChannelPermissions } from '../../service/helpers/utils/get-user-channels-permissions';

/**
 * @description
 * A simplified representation of the User associated with the
 * current Session.
 *
 * @docsCategory auth
 * @docsPage SessionCacheStrategy
 */
export type CachedSessionUser = {
    id: ID;
    identifier: string;
    verified: boolean;
    channelPermissions: UserChannelPermissions[];
};

/**
 * @description
 * A simplified representation of a Session which is easy to
 * store.
 *
 * @docsCategory auth
 * @docsPage SessionCacheStrategy
 */
export type CachedSession = {
    /**
     * @description
     * The timestamp after which this cache entry is considered stale and
     * a fresh copy of the data will be set. Based on the `sessionCacheTTL`
     * option.
     */
    cacheExpiry: number;
    id: ID;
    token: string;
    expires: Date;
    activeOrderId?: ID;
    authenticationStrategy?: string;
    user?: CachedSessionUser;
    activeChannelId?: ID;
};

/**
 * @description
 * This strategy defines how sessions get cached. Since most requests will need the Session
 * object for permissions data, it can become a bottleneck to go to the database and do a multi-join
 * SQL query each time. Therefore, we cache the session data only perform the SQL query once and upon
 * invalidation of the cache.
 *
 * The Vendure default is to use a the {@link InMemorySessionCacheStrategy}, which is fast and suitable for
 * single-instance deployments. However, for multi-instance deployments (horizontally scaled, serverless etc.),
 * you will need to define a custom strategy that stores the session cache in a shared data store, such as in the
 * DB or in Redis.
 *
 * :::info
 *
 * This is configured via the `authOptions.sessionCacheStrategy` property of
 * your VendureConfig.
 *
 * :::
 *
 * Here's an example implementation using Redis. To use this, you need to add the
 * [ioredis package](https://www.npmjs.com/package/ioredis) as a dependency.
 *
 * @example
 * ```ts
 * import { CachedSession, Logger, SessionCacheStrategy, VendurePlugin } from '\@vendure/core';
 * import { Redis, RedisOptions } from 'ioredis';
 *
 * export interface RedisSessionCachePluginOptions {
 *   namespace?: string;
 *   redisOptions?: RedisOptions;
 * }
 * const loggerCtx = 'RedisSessionCacheStrategy';
 * const DEFAULT_NAMESPACE = 'vendure-session-cache';
 * const DEFAULT_TTL = 86400;
 *
 * export class RedisSessionCacheStrategy implements SessionCacheStrategy {
 *   private client: Redis;
 *   constructor(private options: RedisSessionCachePluginOptions) {}
 *
 *   init() {
 *     this.client = new Redis(this.options.redisOptions as RedisOptions);
 *     this.client.on('error', err => Logger.error(err.message, loggerCtx, err.stack));
 *   }
 *
 *   async destroy() {
 *     await this.client.quit();
 *   }
 *
 *   async get(sessionToken: string): Promise<CachedSession | undefined> {
 *     try {
 *       const retrieved = await this.client.get(this.namespace(sessionToken));
 *       if (retrieved) {
 *         try {
 *           return JSON.parse(retrieved);
 *         } catch (e: any) {
 *           Logger.error(`Could not parse cached session data: ${e.message}`, loggerCtx);
 *         }
 *       }
 *     } catch (e: any) {
 *       Logger.error(`Could not get cached session: ${e.message}`, loggerCtx);
 *     }
 *   }
 *
 *   async set(session: CachedSession) {
 *     try {
 *       await this.client.set(this.namespace(session.token), JSON.stringify(session), 'EX', DEFAULT_TTL);
 *     } catch (e: any) {
 *       Logger.error(`Could not set cached session: ${e.message}`, loggerCtx);
 *     }
 *   }
 *
 *   async delete(sessionToken: string) {
 *     try {
 *       await this.client.del(this.namespace(sessionToken));
 *     } catch (e: any) {
 *       Logger.error(`Could not delete cached session: ${e.message}`, loggerCtx);
 *     }
 *   }
 *
 *   clear() {
 *     // not implemented
 *   }
 *
 *   private namespace(key: string) {
 *     return `${this.options.namespace ?? DEFAULT_NAMESPACE}:${key}`;
 *   }
 * }
 *
 * \@VendurePlugin({
 *   configuration: config => {
 *     config.authOptions.sessionCacheStrategy = new RedisSessionCacheStrategy(
 *       RedisSessionCachePlugin.options,
 *     );
 *     return config;
 *   },
 * })
 * export class RedisSessionCachePlugin {
 *   static options: RedisSessionCachePluginOptions;
 *   static init(options: RedisSessionCachePluginOptions) {
 *     this.options = options;
 *     return this;
 *   }
 * }
 * ```
 *
 * @docsCategory auth
 * @docsPage SessionCacheStrategy
 * @docsWeight 0
 */
export interface SessionCacheStrategy extends InjectableStrategy {
    /**
     * @description
     * Store the session in the cache. When caching a session, the data
     * should not be modified apart from performing any transforms needed to
     * get it into a state to be stored, e.g. JSON.stringify().
     */
    set(session: CachedSession): void | Promise<void>;

    /**
     * @description
     * Retrieve the session from the cache
     */
    get(sessionToken: string): CachedSession | undefined | Promise<CachedSession | undefined>;

    /**
     * @description
     * Delete a session from the cache
     */
    delete(sessionToken: string): void | Promise<void>;

    /**
     * @description
     * Clear the entire cache
     */
    clear(): void | Promise<void>;
}
