import {
  BadRequestException,
  Injectable,
  PayloadTooLargeException,
  type PipeTransform,
} from '@nestjs/common';

export interface UploadedFile {
  fieldname: string;
  originalname: string;
  encoding: string;
  mimetype: string;
  size: number;
  buffer: Buffer;
}

export interface FileValidationOptions {
  /** Max file size in bytes (fallback when no per-type limit matches). Default: 5 MB */
  maxSizeBytes?: number;
  /** Per-MIME-type size limits in bytes. Takes precedence over maxSizeBytes for matching types. */
  maxSizeByMimeType?: Record<string, number>;
  /** Allowed MIME types. Default: common image types + PDF */
  allowedMimeTypes?: string[];
  /** Whether to verify file content matches declared MIME type via magic bytes. Default: true */
  verifyMagicBytes?: boolean;
}

const DEFAULT_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
const DEFAULT_ALLOWED_MIMES = [
  'image/jpeg',
  'image/png',
  'image/webp',
  'image/gif',
  'application/pdf',
];

/** Magic byte signatures for supported file types. */
const MAGIC_BYTES: Record<string, { offset: number; bytes: number[] }[]> = {
  'image/jpeg': [{ offset: 0, bytes: [0xFF, 0xD8, 0xFF] }],
  'image/png': [{ offset: 0, bytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] }],
  'image/webp': [{ offset: 0, bytes: [0x52, 0x49, 0x46, 0x46] }], // "RIFF" header
  'image/gif': [
    { offset: 0, bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] }, // GIF87a
    { offset: 0, bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] }, // GIF89a
  ],
  'application/pdf': [{ offset: 0, bytes: [0x25, 0x50, 0x44, 0x46] }], // %PDF
  'video/mp4': [{ offset: 4, bytes: [0x66, 0x74, 0x79, 0x70] }], // "ftyp" at offset 4
};

/**
 * Validates uploaded files for size, MIME type, and file content
 * (magic bytes) to prevent malicious file uploads and resource exhaustion.
 */
@Injectable()
export class FileValidationPipe implements PipeTransform {
  private readonly maxSize: number;
  private readonly maxSizeByMimeType: Record<string, number>;
  private readonly allowedMimes: string[];
  private readonly verifyMagicBytes: boolean;

  constructor(options?: FileValidationOptions) {
    this.maxSize = options?.maxSizeBytes ?? DEFAULT_MAX_SIZE;
    this.maxSizeByMimeType = options?.maxSizeByMimeType ?? {};
    this.allowedMimes = options?.allowedMimeTypes ?? DEFAULT_ALLOWED_MIMES;
    this.verifyMagicBytes = options?.verifyMagicBytes ?? true;
  }

  transform(file: UploadedFile): UploadedFile {
    if (!file) {
      throw new BadRequestException('File is required');
    }

    if (!this.allowedMimes.includes(file.mimetype)) {
      throw new BadRequestException(
        `File type '${file.mimetype}' is not allowed. Allowed types: ${this.allowedMimes.join(', ')}`,
      );
    }

    const effectiveMaxSize = this.maxSizeByMimeType[file.mimetype] ?? this.maxSize;
    if (file.size > effectiveMaxSize) {
      const maxSizeMB = (effectiveMaxSize / (1024 * 1024)).toFixed(0);
      throw new PayloadTooLargeException(
        `File size exceeds maximum of ${maxSizeMB}MB for type '${file.mimetype}'`,
      );
    }

    if (this.verifyMagicBytes) {
      this.validateMagicBytes(file);
    }

    return file;
  }

  private validateMagicBytes(file: UploadedFile): void {
    const signatures = MAGIC_BYTES[file.mimetype];
    if (!signatures) {
      // No known signature for this type — skip magic byte check
      return;
    }

    const matches = signatures.some((sig) => {
      if (file.buffer.length < sig.offset + sig.bytes.length) return false;
      return sig.bytes.every((byte, i) => file.buffer[sig.offset + i] === byte);
    });

    if (!matches) {
      throw new BadRequestException(
        `File content does not match declared type '${file.mimetype}'. The file may be corrupted or mislabeled.`,
      );
    }
  }
}
