/// <reference types="node" />

import { Minipass } from "minipass";
import * as fs from "node:fs";
import { EventEmitter } from "node:stream";

export interface CacheObject {
    /** Subresource Integrity hash for the content this entry refers to. */
    integrity: string;
    /** Key the entry was looked up under. Matches the key argument. */
    key: string;
    /** User-assigned metadata associated with the entry/content. */
    metadata?: any;
    /** Filesystem path where content is stored, joined with cache argument. */
    path: string;
    /** Timestamp the entry was first added on. */
    time: number;
    /** Size of the data */
    size?: number;
}

export interface GetCacheObject {
    metadata?: any;
    integrity: string;
    data: Buffer;
    size?: number;
}

export interface Memoizer {
    get: (key: string) => object | undefined;
    set: (key: string, value: object) => void;
}

export namespace get {
    interface HasContentObject {
        size: number;
        sri: {
            algorithm: string;
            digest: string;
            options: any[];
            source: string;
        };
        stat: fs.Stats;
    }

    interface Options {
        /**
         * If present, the pre-calculated digest for the inserted content. If
         * this option is provided and does not match the post-insertion digest,
         * insertion will fail with an `EINTEGRITY` error.
         *
         * `algorithms` has no effect if this option is present.
         */
        integrity?: string | undefined;

        /**
         * Default: `null`
         *
         * If provided, cacache will memoize the given cache insertion in
         * memory, bypassing any filesystem checks for that key or digest in
         * future cache fetches. Nothing will be written to the in-memory cache
         * unless this option is explicitly truthy.
         *
         * If `opts.memoize` is an object or a `Map`-like (that is, an object
         * with `get` and `set` methods), it will be written to instead of the
         * global memoization cache.
         *
         * Reading from disk data can be forced by explicitly passing
         * `memoize: false` to the reader functions, but their default will be
         * to read from memory.
         */
        memoize?: null | boolean | Memoizer | undefined;

        /**
         * If provided, the data stream will be verified to check that enough
         * data was passed through. If there's more or less data than expected,
         * insertion will fail with an `EBADSIZE` error.
         */
        size?: number | undefined;
    }

    type InfoOptions = Pick<Options, "memoize">;

    interface GetStreamEvents extends Minipass.Events<Buffer> {
        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        integrity: [integrity: string];

        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        size: [size: number];

        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        metadata: [metadata: any];
    }

    type CopyResultObject = Pick<CacheObject, "metadata" | "integrity" | "size">;

    namespace copy {
        function byDigest(cachePath: string, hash: string, dest: string, opts?: Options): Promise<string>;
    }

    namespace stream {
        function byDigest(cachePath: string, hash: string, opts?: Options): Minipass<Buffer, never>;
    }

    function byDigest(cachePath: string, hash: string, opts?: Options): Promise<Buffer>;
    function copy(cachePath: string, key: string, dest: string, opts?: Options): Promise<CopyResultObject>;

    /**
     * Looks up a Subresource Integrity hash in the cache. If content exists
     * for this `integrity`, it will return an object, with the specific single
     * integrity hash that was found in sri key, and the size of the found
     * content as size. If no content exists for this integrity, it will return
     * `false`.
     */
    function hasContent(cachePath: string, hash: string): Promise<HasContentObject | false>;

    /**
     * Looks up `key` in the cache index, returning information about the entry
     * if one exists.
     */
    function info(cachePath: string, key: string, opts?: InfoOptions): Promise<CacheObject | null>;

    /**
     * Returns a Readable Stream of the cached data identified by `key`.
     *
     * If there is no content identified by `key`, or if the locally-stored data
     * does not pass the validity checksum, an error will be emitted.
     *
     * `metadata` and `integrity` events will be emitted before the stream
     * closes, if you need to collect that extra data about the cached entry.
     *
     * A sub-function, `get.stream.byDigest` may be used for identical behavior,
     * except lookup will happen by integrity hash, bypassing the index
     * entirely. This version does not emit the `metadata` and `integrity`
     * events at all.
     */
    function stream(cachePath: string, key: string, opts?: Options): Minipass<Buffer, never, GetStreamEvents>;
}

export namespace ls {
    type Cache = Record<string, CacheObject & { size: number }>;

    /**
     * Lists info for all entries currently in the cache as a single large
     * object.
     *
     * This works just like `ls`, except `get.info` entries are returned as
     * `'data'` events on the returned stream.
     */
    function stream(cachePath: string): Minipass<Cache, never>;
}

export namespace put {
    interface IntegrityEmitterEvents {
        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        integrity: [integrity: string];

        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        size: [size: number];

        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        error: [err: unknown];
    }

    interface Options {
        /**
         * Default: `['sha512']`
         *
         * Hashing algorithms to use when calculating the subresource integrity
         * digest for inserted data. Can use any algorithm listed in
         * `crypto.getHashes()` or `'omakase'`/`'お任せします'` to pick a random
         * hash algorithm on each insertion. You may also use any anagram of
         * `'modnar'` to use this feature.
         *
         * Currently only supports one algorithm at a time (i.e., an array
         * length of exactly `1`). Has no effect if `opts.integrity` is present.
         */
        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        algorithms?: [string] | undefined;

        /**
         * If present, the pre-calculated digest for the inserted content. If
         * this option is provided and does not match the post-insertion digest,
         * insertion will fail with an `EINTEGRITY` error.
         *
         * `algorithms` has no effect if this option is present.
         */
        integrity?: string | undefined;

        /** Arbitrary metadata to be attached to the inserted key. */
        metadata?: any;

        /**
         * Default: `null`
         *
         * If provided, cacache will memoize the given cache insertion in
         * memory, bypassing any filesystem checks for that key or digest in
         * future cache fetches. Nothing will be written to the in-memory cache
         * unless this option is explicitly truthy.
         *
         * If `opts.memoize` is an object or a `Map`-like (that is, an object
         * with `get` and `set` methods), it will be written to instead of the
         * global memoization cache.
         *
         * Reading from disk data can be forced by explicitly passing
         * `memoize: false` to the reader functions, but their default will be
         * to read from memory.
         */
        memoize?: null | boolean | Memoizer | undefined;

        /**
         * If provided, the data stream will be verified to check that enough
         * data was passed through. If there's more or less data than expected,
         * insertion will fail with an `EBADSIZE` error.
         */
        size?: number | undefined;

        /**
         * Default: `null`
         *
         * Prefix to append on the temporary directory name inside the cache's tmp dir.
         */
        tmpPrefix?: null | string | undefined;

        /**
         * Default: `undefined`
         *
         * (Streaming only) If present, uses the provided event emitter as a
         * source of truth for both integrity and size. This allows use cases
         * where integrity is already being calculated outside of cacache to
         * reuse that data instead of calculating it a second time.
         *
         * The emitter must emit both the 'integrity' and 'size' events.
         *
         * NOTE: If this option is provided, you must verify that you receive
         * the correct integrity value yourself and emit an 'error' event if
         * there is a mismatch. ssri Integrity Streams do this for you when
         * given an expected integrity.
         */
        integrityEmitter?: EventEmitter<IntegrityEmitterEvents> | undefined;
    }

    interface PutStreamEvents extends Minipass.Events<never> {
        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        integrity: [integrity: string];

        // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
        size: [size: number];
    }

    /**
     * Returns a Writable Stream that inserts data written to it into the cache.
     * Emits an `integrity` event with the digest of written contents when it
     * succeeds.
     */
    function stream(
        cachePath: string,
        key: string,
        opts?: Options,
    ): Minipass<never, Minipass.ContiguousData, PutStreamEvents>;
}

export namespace rm {
    /**
     * Clears the entire cache. Mainly by blowing away the cache directory
     * itself.
     */
    function all(cachePath: string): Promise<void>;

    /**
     * Removes the index entry for `key`. Content will still be accessible if
     * requested directly by content address (`get.stream.byDigest`).
     *
     * To remove the content itself (which might still be used by other
     * entries), use `rm.content`. Or, to safely vacuum any unused content,
     * use `verify`.
     */
    function entry(cachePath: string, key: string): Promise<CacheObject>;

    /**
     * Removes the content identified by `integrity`. Any index entries
     * referring to it will not be usable again until the content is re-added
     * to the cache with an identical digest.
     */
    function content(cachePath: string, hash: string): Promise<boolean>;
}

export namespace tmp {
    type Callback = (dir: string) => void;

    interface Options {
        /**
         * Default: 20
         *
         * Number of concurrently read files in the filesystem while doing clean up.
         */
        concurrency?: number | undefined;

        /**
         * Receives a formatted entry. Return `false` to remove it.
         *
         * Note: might be called more than once on the same entry.
         */
        filter?: string | false | undefined;

        /**
         * Custom logger function:
         * ```
         *   log: { silly () {} }
         *   log.silly('verify', 'verifying cache at', cache)
         * ```
         */
        log?: Record<string, (...args: any[]) => any> | undefined;

        /**
         * Default: `null`
         *
         * Prefix to append on the temporary directory name inside the cache's tmp dir.
         */
        tmpPrefix?: null | string | undefined;
    }

    /**
     * Sets the `uid` and `gid` properties on all files and folders within the
     * tmp folder to match the rest of the cache.
     *
     * Use this after manually writing files into `tmp.mkdir` or `tmp.withTmp`.
     */
    function fix(cachePath: string): Promise<void>;

    /**
     * Returns a unique temporary directory inside the cache's `tmp` dir. This
     * directory will use the same safe user assignment that all the other stuff
     * use.
     *
     * Once the directory is made, it's the user's responsibility that all files
     * within are given the appropriate `gid`/`uid` ownership settings to match
     * the rest of the cache. If not, you can ask cacache to do it for you by
     * calling `tmp.fix()`, which will fix all tmp directory permissions.
     *
     * If you want automatic cleanup of this directory, use `tmp.withTmp()`
     */
    function mkdir(cachePath: string, opts?: Options): Promise<string>;

    /**
     * Creates a temporary directory with `tmp.mkdir()` and calls `cb` with it.
     * The created temporary directory will be removed when the return value of
     * `cb()` resolves, the tmp directory will be automatically deleted once that
     * promise completes.
     *
     * The same caveats apply when it comes to managing permissions for the tmp dir's contents.
     */
    function withTmp(cachePath: string, opts: Options, cb: Callback): void;
    function withTmp(cachePath: string, cb: Callback): void;
}

export namespace verify {
    interface Options {
        /**
         * Default: 20
         *
         * Number of concurrently read files in the filesystem while doing clean up.
         */
        concurrency?: number | undefined;

        /**
         * Receives a formatted entry. Return `false` to remove it.
         *
         * Note: might be called more than once on the same entry.
         */
        filter?: string | false | undefined;

        /**
         * Custom logger function:
         * ```
         *   log: { silly () {} }
         *   log.silly('verify', 'verifying cache at', cache)
         * ```
         */
        log?: Record<string, (...args: any[]) => any> | undefined;
    }

    /**
     * Returns a Date representing the last time `cacache.verify` was run on
     * `cache`.
     */
    function lastRun(cachePath: string): Promise<Date>;
}

export namespace index {
    interface InsertOptions {
        /** Arbitrary metadata to be attached to the inserted key. */
        metadata?: any;

        size?: number;
    }

    /**
     * Writes an index entry to the cache for the given key without writing content.
     *
     * It is assumed if you are using this method, you have already stored the
     * content some other way and you only wish to add a new index to that content.
     * The metadata and size properties are read from opts and used as part of the
     * index entry.
     *
     * Returns a Promise resolving to the newly added entry.
     */
    function insert(
        cache: string,
        key: string,
        integrity: string,
        opts?: InsertOptions,
    ): Promise<CacheObject>;

    interface CompactOptions {
        validateEntry?: (entry: CacheObject) => boolean;
    }

    /**
     * Uses matchFn, which must be a synchronous function that accepts two entries
     * and returns a boolean indicating whether or not the two entries match, to
     * deduplicate all entries in the cache for the given key.

     * If opts.validateEntry is provided, it will be called as a function with the
     * only parameter being a single index entry. The function must return a
     * Boolean, if it returns true the entry is considered valid and will be kept
     * in the index, if it returns false the entry will be removed from the index.

     * If opts.validateEntry is not provided, however, every entry in the index
     * will be deduplicated and kept until the first null integrity is reached,
     * removing all entries that were written before the null.

     * The deduplicated list of entries is both written to the index, replacing
     * the existing content, and returned in the Promise.
     */
    function compact(
        cache: string,
        key: string,
        matchFn: (obj1: CacheObject, obj2: CacheObject) => boolean,
        opts?: CompactOptions,
    ): Promise<CacheObject[]>;
}

export function clearMemoized(): Record<string, CacheObject>;

/**
 * Returns an object with the cached data, digest, and metadata identified by
 * `key`. The `data` property of this object will be a Buffer instance that
 * presumably holds some data that means something to you. I'm sure you know
 * what to do with it! cacache just won't care.
 *
 * `integrity` is a Subresource Integrity string. That is, a string that can be
 * used to verify `data`, which looks like
 * `<hash-algorithm>-<base64-integrity-hash>`.
 *
 * If there is no content identified by key, or if the locally-stored data does
 * not pass the validity checksum, the promise will be rejected.
 *
 * A sub-function, `get.byDigest` may be used for identical behavior, except
 * lookup will happen by integrity hash, bypassing the index entirely. This
 * version of the function only returns data itself, without any wrapper.
 *
 * **Note**
 *
 * This function loads the entire cache entry into memory before returning it.
 * If you're dealing with Very Large data, consider using `get.stream` instead.
 */
export function get(cachePath: string, key: string, options?: get.Options): Promise<GetCacheObject>;

/**
 * Lists info for all entries currently in the cache as a single large object.
 * Each entry in the object will be keyed by the unique index key, with
 * corresponding `get.info` objects as the values.
 */
export function ls(cachePath: string): Promise<ls.Cache>;

/**
 * Inserts data passed to it into the cache. The returned Promise resolves with
 * a digest (generated according to `opts.algorithms`) after the cache entry has
 * been successfully written.
 */
export function put(cachePath: string, key: string, data: any, opts?: put.Options): Promise<string>;

/**
 * Removes the index entry for `key`. Content will still be accessible if
 * requested directly by content address (`get.stream.byDigest`).
 *
 * To remove the content itself (which might still be used by other
 * entries), use `rm.content`. Or, to safely vacuum any unused content,
 * use `verify`.
 */
export function rm(cachePath: string, key: string): Promise<any>;

/**
 * Checks out and fixes up your cache:
 *
 * - Cleans up corrupted or invalid index entries
 * - Custom entry filtering options
 * - Garbage collects any content entries not referenced by the index
 * - Checks integrity for all content entries and removes invalid content
 * - Fixes cache ownership
 * - Removes the `tmp` directory in the cache and all its contents.
 *
 * When it's done, it'll return an object with various stats about the
 * verification process, including amount of storage reclaimed, number of valid
 * entries, number of entries removed, etc.
 */
export function verify(cachePath: string, opts?: verify.Options): Promise<any>;
