import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { fromBuffer as fileTypeFromBuffer, FileTypeResult } from 'file-type';

export interface MimeValidationResult {
  mime: string;
  extension: string;
  detected: FileTypeResult | undefined;
}

/**
 * Validador de tipo MIME por inspección de magic bytes (firma binaria del archivo).
 *
 * Por qué no confiar sólo en la extensión / Content-Type del cliente:
 *   - Un atacante puede renombrar `.exe` a `.pdf` y el navegador reporta `application/pdf`.
 *   - `file-type` lee los primeros bytes y devuelve el tipo real.
 *
 * Algunos formatos legítimos (CSV / TXT) no tienen magic bytes — para esos casos
 * se aceptan en base a extensión + Content-Type del cliente, siempre que estén
 * en la lista de tipos habilitados.
 */
@Injectable()
export class LegajosMimeValidator {
  private readonly logger = new Logger(LegajosMimeValidator.name);

  /** Tipos sin firma binaria que se aceptan en base al Content-Type + extensión. */
  private static readonly TEXT_TYPES = new Map<string, string>([
    ['text/plain', 'txt'],
    ['text/csv', 'csv'],
  ]);

  /** Magic bytes / extensiones rechazados aunque vinieran "limpios" del cliente. */
  private static readonly BLOCKED_EXTENSIONS = new Set([
    'exe', 'bat', 'cmd', 'com', 'sh', 'msi', 'dll', 'scr', 'vbs', 'jar', 'apk', 'ps1',
  ]);

  /**
   * Valida un archivo recibido por multipart.
   *
   * @param buffer Contenido binario completo (Multer guarda en memoria si no se configura disk storage).
   * @param clientMimeType Content-Type declarado por el navegador (opcional, fallback).
   * @param originalName Nombre original (para fallback de extensión).
   * @param allowedMimeTypes Lista habilitada (parámetro `LEGAJO_MIME_HABILITADOS`).
   * @returns MimeValidationResult con el mime y extensión finales.
   * @throws HttpException 415 si el archivo no está permitido.
   */
  async validate(
    buffer: Buffer,
    clientMimeType: string | undefined,
    originalName: string,
    allowedMimeTypes: string[],
  ): Promise<MimeValidationResult> {
    if (!buffer || buffer.length === 0) {
      throw new HttpException(
        { success: false, message: 'El archivo está vacío.' },
        HttpStatus.BAD_REQUEST,
      );
    }

    const extFromName = this.extractExtension(originalName);

    if (LegajosMimeValidator.BLOCKED_EXTENSIONS.has(extFromName)) {
      throw new HttpException(
        {
          success: false,
          message: `Tipo de archivo no permitido: archivos .${extFromName} están bloqueados por seguridad.`,
        },
        HttpStatus.UNSUPPORTED_MEDIA_TYPE,
      );
    }

    const detected = await fileTypeFromBuffer(buffer);
    const allowed = new Set(allowedMimeTypes.map((m) => m.trim()).filter(Boolean));

    if (detected) {
      // Caso normal: archivo binario con firma reconocible (PDF, DOCX, imagen, ZIP).
      if (!allowed.has(detected.mime)) {
        throw new HttpException(
          {
            success: false,
            message: `Tipo de archivo no permitido. Detectado: ${detected.mime}.`,
          },
          HttpStatus.UNSUPPORTED_MEDIA_TYPE,
        );
      }
      // Magic bytes superan a la extensión del nombre original.
      return { mime: detected.mime, extension: detected.ext, detected };
    }

    // Sin magic bytes detectables. Permitimos TXT/CSV si coinciden cliente + extensión.
    const candidateMime = (clientMimeType ?? '').toLowerCase();
    if (
      candidateMime &&
      allowed.has(candidateMime) &&
      LegajosMimeValidator.TEXT_TYPES.has(candidateMime) &&
      LegajosMimeValidator.TEXT_TYPES.get(candidateMime) === extFromName
    ) {
      return { mime: candidateMime, extension: extFromName, detected: undefined };
    }

    throw new HttpException(
      {
        success: false,
        message:
          'Tipo de archivo no permitido. No se pudo verificar la firma del archivo o el formato no está habilitado.',
      },
      HttpStatus.UNSUPPORTED_MEDIA_TYPE,
    );
  }

  private extractExtension(name: string): string {
    const idx = name.lastIndexOf('.');
    if (idx < 0 || idx === name.length - 1) return '';
    return name.slice(idx + 1).toLowerCase();
  }
}
