import { BusboyConfig, BusboyFileStream } from '@fastify/busboy'
import { FastifyPluginCallback, FastifyRequest } from 'fastify'
import { Readable } from 'node:stream'
import { FastifyErrorConstructor } from '@fastify/error'

declare module 'fastify' {
  interface FastifyRequest {
    isMultipart: () => boolean;

    formData: () => Promise<FormData>;

    // promise api
    parts: (
      options?: Omit<BusboyConfig, 'headers'>
    ) => AsyncIterableIterator<fastifyMultipart.Multipart>;

    // Stream mode
    file: (
      options?: Omit<BusboyConfig, 'headers'> | fastifyMultipart.FastifyMultipartBaseOptions
    ) => Promise<fastifyMultipart.MultipartFile | undefined>;
    files: (
      options?: Omit<BusboyConfig, 'headers'> | fastifyMultipart.FastifyMultipartBaseOptions
    ) => AsyncIterableIterator<fastifyMultipart.MultipartFile>;

    // Disk mode
    saveRequestFiles: (
      options?: Omit<BusboyConfig, 'headers'> & { tmpdir?: string }
    ) => Promise<fastifyMultipart.SavedMultipartFilesResult>;
    cleanRequestFiles: () => Promise<void>;
    tmpUploads: Array<string> | null;
    /** This will get populated as soon as a call to `saveRequestFiles` gets resolved. Avoiding any future duplicate work */
    savedRequestFiles: Array<fastifyMultipart.SavedMultipartFile> | null;
  }

  interface FastifyInstance {
    multipartErrors: MultipartErrors;
  }

  interface FastifyContextConfig {
    multipartOptions?: Omit<BusboyConfig, 'headers'>
  }
}

type FastifyMultipartPlugin = FastifyPluginCallback<
| fastifyMultipart.FastifyMultipartBaseOptions
| fastifyMultipart.FastifyMultipartOptions
| fastifyMultipart.FastifyMultipartAttachFieldsToBodyOptions
>

interface BodyEntry {
  data: Buffer;
  filename: string;
  encoding: string;
  mimetype: string;
  limit: false;
}

interface MultipartErrors {
  PartsLimitError: FastifyErrorConstructor;
  FilesLimitError: FastifyErrorConstructor;
  FieldsLimitError: FastifyErrorConstructor;
  PrototypeViolationError: FastifyErrorConstructor;
  InvalidMultipartContentTypeError: FastifyErrorConstructor;
  RequestFileTooLargeError: FastifyErrorConstructor;
}

declare namespace fastifyMultipart {
  export interface SavedMultipartFile extends MultipartFile {
    /**
     * Path to the temporary file
     */
    filepath: string;
  }

  export interface SavedMultipartFilesResult {
    files: Array<SavedMultipartFile>;
    values: MultipartFields | Record<string, unknown>;
  }

  export type Multipart = MultipartFile | MultipartValue

  export interface MultipartFile {
    type: 'file';
    toBuffer: () => Promise<Buffer>;
    file: BusboyFileStream;
    fieldname: string;
    filename: string;
    encoding: string;
    mimetype: string;
    fields: MultipartFields;
  }

  export interface MultipartValue<T = unknown> {
    type: 'field';
    value: T;
    fieldname: string;
    mimetype: string;
    encoding: string;
    fieldnameTruncated: boolean;
    valueTruncated: boolean;
    fields: MultipartFields;
  }

  export interface MultipartFields {
    [fieldname: string]: Multipart | Multipart[] | undefined;
  }

  export interface FastifyMultipartBaseOptions extends Partial<BusboyConfig> {
    /**
     * Add a shared schema to validate the input fields
     */
    sharedSchemaId?: string;

    /**
     * Allow throwing error when file size limit reached.
     */
    throwFileSizeLimit?: boolean;

    /**
     * Detect if a Part is a file.
     *
     * By default a file is detected if contentType
     * is application/octet-stream or fileName is not
     * undefined.
     *
     * Modify this to handle e.g. Blobs.
     */
    isPartAFile?: (
      fieldName: string | undefined,
      contentType: string | undefined,
      fileName: string | undefined
    ) => boolean;

    limits?: {
      /**
       * Max field name size in bytes
       */
      fieldNameSize?: number;

      /**
       * Max field value size in bytes
       */
      fieldSize?: number;

      /**
       * Max number of non-file fields
       */
      fields?: number;

      /**
       * For multipart forms, the max file size
       */
      fileSize?: number;

      /**
       * Max number of file fields
       */
      files?: number;

      /**
       * Max number of header key=>value pairs
       */
      headerPairs?: number;

      /**
       * For multipart forms, the max number of parts (fields + files)
       * @default 1000
       */
      parts?: number;
    };
  }

  export interface FastifyMultipartOptions extends FastifyMultipartBaseOptions {
    /**
     * Only valid in the promise api. Append the multipart parameters to the body object.
     */
    attachFieldsToBody?: false;

    /**
     * Manage the file stream like you need
     */
    onFile?: (
      fieldName: string,
      stream: Readable,
      filename: string,
      encoding: string,
      mimetype: string,
      body: Record<string, BodyEntry>
    ) => void | Promise<void>;
  }

  export interface FastifyMultipartAttachFieldsToBodyOptions
    extends FastifyMultipartBaseOptions {
    /**
     * Only valid in the promise api. Append the multipart parameters to the body object.
     */
    attachFieldsToBody: true | 'keyValues';

    /**
     * Manage the file stream like you need
     */
    onFile?: (this: FastifyRequest, part: MultipartFile) => void | Promise<void>;
  }

  /**
   * Adds a new type `isFile` to help @fastify/swagger generate the correct schema.
   */
  export function ajvFilePlugin (ajv: any): void

  export const fastifyMultipart: FastifyMultipartPlugin
  export { fastifyMultipart as default }
}
declare function fastifyMultipart (
  ...params: Parameters<FastifyMultipartPlugin>
): ReturnType<FastifyMultipartPlugin>

export = fastifyMultipart
