/// <reference types='node' />

import type * as Fastify from 'fastify'
import { FastifyPluginCallback } from 'fastify'
import { CookieSerializeOptions } from '@fastify/cookie'

declare module 'fastify' {
  interface FastifyInstance {
    decryptSession<Request extends Record<string, any> = FastifyRequest>(sessionId: string, request: Request, cookieOpts: fastifySession.CookieOptions, callback: Callback): void;
    decryptSession<Request extends Record<string, any> = FastifyRequest>(sessionId: string, request: Request, callback: Callback): void;
  }

  interface FastifyRequest {
    /** Allows to access or modify the session data. */
    session: fastifySession.FastifySessionObject;

    /** A session store. */
    sessionStore: Readonly<fastifySession.SessionStore>;
  }

  interface Session extends ExpressSessionData { }
}

type FastifySession = FastifyPluginCallback<fastifySession.FastifySessionOptions> & {
  Store: fastifySession.MemoryStore,
  MemoryStore: fastifySession.MemoryStore,
}

type Callback = (err?: any) => void
type CallbackSession = (err: any, result?: Fastify.Session | null) => void

interface ExpressSessionData {
  /** The cookie properties as defined by express-session */
  cookie: {
    originalMaxAge: number | null;
    maxAge?: number;
    signed?: boolean;
    expires?: Date | null;
    httpOnly?: boolean;
    path?: string;
    domain?: string;
    secure?: boolean | 'auto';
    sameSite?: boolean | 'lax' | 'strict' | 'none';
  }
}

interface UnsignResult {
  valid: boolean;
  renew: boolean;
  value: string | null;
}

interface Signer {
  sign: (value: string) => string;
  unsign: (input: string) => UnsignResult;
}

declare namespace fastifySession {

  export interface FastifySessionObject extends Fastify.Session {
    sessionId: string;

    /** The stable identifier used as the session store key. */
    sessionStoreId: string;

    encryptedSessionId: string;

    /** Updates the `expires` property of the session's cookie. */
    touch(): void;

    /**
     * Regenerates the session by generating a new `sessionId`.
     *
     * ignoreFields specifies which fields should be kept in the new session object.
     */
    regenerate(callback: Callback): void;
    regenerate(ignoreFields: string[], callback: Callback): void;
    regenerate(): Promise<void>;
    regenerate(ignoreFields: string[]): Promise<void>;

    /** Set the session cookie options for this request handler. */
    options(opts: Partial<CookieOptions>): void

    /** Allows to destroy the session in the store. */
    destroy(callback: Callback): void;
    destroy(): Promise<void>;

    /** Reloads the session data from the store and re-populates the request.session object. */
    reload(callback: Callback): void;
    reload(): Promise<void>;

    /** Save the session back to the store, replacing the contents on the store with the contents in memory. */
    save(callback: Callback): void;
    save(): Promise<void>;

    /** sets values in the session. */
    set<K extends keyof Fastify.Session>(key: K, value: Fastify.Session[K]): void;

    /** gets values from the session. */
    get<K extends keyof Fastify.Session>(key: K): Fastify.Session[K] | undefined;

    /** checks if session has been modified since it was generated or loaded from the store. */
    isModified(): boolean;

    /** checks if the session has been saved to the store */
    isSaved(): boolean;

    /** checks if the session is backed by an existing store entry */
    isPersisted(): boolean;
  }

  export interface SessionStore {
    set(
      sessionStoreId: string,
      session: Fastify.Session,
      callback: Callback
    ): void;
    get(
      sessionStoreId: string,
      callback: CallbackSession
    ): void;
    compareAndSet(
      sessionStoreId: string,
      expectedGeneration: string,
      session: Fastify.Session,
      callback: Callback
    ): void;
    destroy(sessionStoreId: string, callback: Callback): void;
  }

  export interface FastifySessionOptions {
    /**
     * The secret used to sign the cookie.
     *
     * Must be an array of strings, or a string with length 32 or greater. If an array, the first secret is used to
     * sign new cookies, and is the first one to be checked for incoming cookies.
     * Further secrets in the array are used to check incoming cookies, in the order specified.
     *
     * Note that the array may be manipulated by the rest of the application during its life cycle.
     * This can be done by storing the array in a separate variable that is later manipulated with mutating methods
     * like unshift(), pop(), splice(), etc.
     * This can be used to rotate the signing secret at regular intervals.
     * A secret should remain somewhere in the array as long as there are active sessions with cookies signed by it.
     * Secrets management is left up to the rest of the application.
     */
    secret: string | string[] | Signer;

    /**
     * The algorithm used to sign the cookie.
     *
     * @default 'sha256'
     */
    algorithm?: string;

    /** The name of the session cookie. Defaults to `sessionId`. */
    cookieName?: string;

    /**
     * The options object used to generate the `Set-Cookie` header of the session cookie.
     *
     * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie
     */
    cookie?: CookieOptions;

    /**
     * A session store.
     * Express-session stores need an adapter that implements atomic `compareAndSet`.
     * Defaults to a simple in memory store.
     * Note: The default store should not be used in a production environment because it will leak memory.
     */
    store?: fastifySession.SessionStore;

    /**
     * Save sessions to the store, even when they are new and not modified.
     * Defaults to true. Setting this to false can be useful to save storage space and to comply with the EU cookie law.
     */
    saveUninitialized?: boolean;

    /**
     * Force the session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown.
     * Defaults to true. This is typically used in conjuction with short, non-session-length maxAge values to provide a quick timeout of the session data with reduced potential of it occurring during on going server interactions.
     */
    rolling?: boolean;

    /** Function used to generate new session IDs. */
    idGenerator?(request?: Fastify.FastifyRequest): string;

    /**
     * Prefixes all cookie values. Run with "s:" to be be compatible with express-session.
     * Defaults to ""
     */
    cookiePrefix?: string;
  }

  export interface CookieOptions extends Omit<CookieSerializeOptions, 'signed' | 'maxAge' | 'httpOnly' | 'secure'> {
    /** A `number` in milliseconds that specifies the `Expires` attribute by adding the specified milliseconds to the current date. If both `expires` and `maxAge` are set, then `expires` is used. */
    maxAge?: number;
    /**
     * The `boolean` value of the `HttpOnly` attribute.
     * @default true
     */
    httpOnly?: boolean;
    /**
     * The `boolean` value of the `Secure` attribute. Set this option to false when communicating over an unencrypted (HTTP) connection.
     * Value can be set to `auto`; in that case the `Secure` attribute is false for HTTP and true for HTTPS.
     * @default true
     */
    secure?: boolean | 'auto';
  }

  export class MemoryStore implements fastifySession.SessionStore {
    constructor (map?: Map<string, Fastify.Session>)
    set (
      sessionStoreId: string,
      session: Fastify.Session,
      callback: Callback
    ): void
    get (
      sessionStoreId: string,
      callback: CallbackSession
    ): void
    compareAndSet (
      sessionStoreId: string,
      expectedGeneration: string,
      session: Fastify.Session,
      callback: Callback
    ): void
    destroy (sessionStoreId: string, callback: Callback): void
  }

  export const Store: MemoryStore

  export const fastifySession: FastifySession
  export { fastifySession as default }
}

declare function fastifySession (...params: Parameters<FastifySession>): ReturnType<FastifySession>
export = fastifySession
