/**
 * Storage utility functions
 * Helper functions for file handling, validation, and naming
 */

import crypto from 'crypto';

/**
 * Generate a unique filename with timestamp and random hash
 * @param {string} originalName - Original filename
 * @param {string} prefix - Optional prefix for the filename
 * @returns {string} Unique filename
 */
export function generateUniqueFilename(originalName, prefix = '') {
  const timestamp = Date.now();
  const randomHash = crypto.randomBytes(8).toString('hex');
  const extension = getFileExtension(originalName);
  const basePrefix = prefix ? `${prefix}_` : '';
  return `${basePrefix}${timestamp}_${randomHash}${extension}`;
}

/**
 * Get file extension from filename
 * @param {string} filename - Filename
 * @returns {string} File extension with dot (e.g., '.jpg') or empty string
 */
export function getFileExtension(filename) {
  if (!filename) return '';
  const lastDot = filename.lastIndexOf('.');
  return lastDot === -1 ? '' : filename.substring(lastDot).toLowerCase();
}

const MIME_TYPES = {
  // Images
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.png': 'image/png',
  '.gif': 'image/gif',
  '.webp': 'image/webp',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.bmp': 'image/bmp',

  // Documents
  '.pdf': 'application/pdf',
  '.doc': 'application/msword',
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  '.xls': 'application/vnd.ms-excel',
  '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  '.ppt': 'application/vnd.ms-powerpoint',
  '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  '.txt': 'text/plain',
  '.csv': 'text/csv',

  // Archives
  '.zip': 'application/zip',
  '.rar': 'application/x-rar-compressed',
  '.7z': 'application/x-7z-compressed',
  '.tar': 'application/x-tar',
  '.gz': 'application/gzip',

  // Media
  '.mp3': 'audio/mpeg',
  '.wav': 'audio/wav',
  '.mp4': 'video/mp4',
  '.avi': 'video/x-msvideo',
  '.mov': 'video/quicktime',
  '.wmv': 'video/x-ms-wmv',

  // Code
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.xml': 'application/xml',
  '.html': 'text/html',
  '.css': 'text/css',
};

/**
 * Get MIME type from file extension
 * @param {string} filename - Filename or extension
 * @returns {string} MIME type
 */
export function getMimeType(filename) {
  const ext = getFileExtension(filename).toLowerCase();
  return MIME_TYPES[ext] || 'application/octet-stream';
}

/**
 * Validate file type against allowed types
 * @param {string} filename - Filename or extension
 * @param {string[]} allowedTypes - Array of allowed extensions (e.g., ['.jpg', '.png']) or MIME types
 * @returns {boolean} True if file type is allowed
 */
export function validateFileType(filename, allowedTypes) {
  if (!allowedTypes || allowedTypes.length === 0) return true;
  
  const ext = getFileExtension(filename).toLowerCase();
  const mimeType = getMimeType(filename);
  
  return allowedTypes.some(type => {
    if (type.startsWith('.')) {
      return ext === type.toLowerCase();
    }
    return mimeType === type;
  });
}

/**
 * Validate file size
 * @param {number} size - File size in bytes
 * @param {number} maxSize - Maximum allowed size in bytes
 * @returns {boolean} True if file size is valid
 */
export function validateFileSize(size, maxSize) {
  if (!maxSize) return true;
  return size <= maxSize;
}

/**
 * Format file size to human-readable format
 * @param {number} bytes - Size in bytes
 * @param {number} decimals - Number of decimal places (default: 2)
 * @returns {string} Formatted size (e.g., '1.5 MB')
 */
export function formatFileSize(bytes, decimals = 2) {
  if (bytes === 0) return '0 Bytes';
  
  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
  
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

/**
 * Sanitize filename by removing special characters
 * @param {string} filename - Original filename
 * @returns {string} Sanitized filename
 */
export function sanitizeFilename(filename) {
  const ext = getFileExtension(filename);
  const nameWithoutExt = filename.substring(0, filename.length - ext.length);
  
  // Remove special characters and replace spaces with underscores
  const sanitized = nameWithoutExt
    .replace(/[^a-zA-Z0-9_-]/g, '_')
    .replace(/_{2,}/g, '_')
    .replace(/^_+|_+$/g, '');
  
  return sanitized + ext;
}

/**
 * Common file type presets
 */
export const FILE_TYPE_PRESETS = {
  IMAGES: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
  IMAGES_NO_GIF: ['.jpg', '.jpeg', '.png', '.webp'],
  DOCUMENTS: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.csv'],
  PDF_ONLY: ['.pdf'],
  VIDEOS: ['.mp4', '.avi', '.mov', '.wmv'],
  AUDIO: ['.mp3', '.wav'],
  ARCHIVES: ['.zip', '.rar', '.7z', '.tar', '.gz'],
};

/**
 * Common file size limits (in bytes)
 */
export const FILE_SIZE_LIMITS = {
  AVATAR: 5 * 1024 * 1024, // 5 MB
  IMAGE: 10 * 1024 * 1024, // 10 MB
  DOCUMENT: 50 * 1024 * 1024, // 50 MB
  VIDEO: 500 * 1024 * 1024, // 500 MB
  LARGE_FILE: 1024 * 1024 * 1024, // 1 GB
};

/**
 * Known magic-byte sequences for each permitted image extension.
 * Keyed by lower-case extension; value is the expected byte sequence at offset 0.
 * WEBP requires a special compound check (RIFF....WEBP).
 */
const MAGIC_BYTES = {
  '.jpg':  [0xFF, 0xD8, 0xFF],
  '.jpeg': [0xFF, 0xD8, 0xFF],
  '.png':  [0x89, 0x50, 0x4E, 0x47],
  '.gif':  [0x47, 0x49, 0x46, 0x38],   // GIF87a or GIF89a
};

/**
 * Confirm that the file buffer starts with the magic bytes expected for the
 * file's declared extension.  This is a *positive* assertion — the file must
 * prove it is the type it claims to be, not merely fail to match a denylist.
 *
 * A polyglot that prefixes a real image header before malicious payload still
 * satisfies this check, so this function must always be used together with
 * inspectBufferForDangerousContent which scans a larger region for script tags.
 *
 * @param {Buffer} buffer
 * @param {string} filename
 * @returns {boolean} true when the magic bytes match the declared extension
 */
export function validateMagicBytes(buffer, filename) {
  if (!Buffer.isBuffer(buffer) || buffer.length < 12) return false;
  const ext = getFileExtension(filename).toLowerCase();

  // WebP: 'RIFF' at bytes 0-3, 'WEBP' at bytes 8-11
  if (ext === '.webp') {
    return buffer.slice(0, 4).toString('ascii') === 'RIFF'
        && buffer.slice(8, 12).toString('ascii') === 'WEBP';
  }

  const expected = MAGIC_BYTES[ext];
  if (!expected) return false;
  return expected.every((byte, i) => buffer[i] === byte);
}

/**
 * Inspect the first bytes of a buffer for known-dangerous content signatures
 * that could indicate an HTML, SVG, or XML file disguised with an image extension.
 * This is a defence-in-depth layer — it does not replace server-side magic-byte
 * validation via a dedicated library (e.g. 'sharp' or 'file-type').
 *
 * The scan window is extended to 512 bytes to make it harder to hide a script
 * tag after a short but valid-looking binary header.
 *
 * @param {Buffer} buffer - File buffer (ideally the full file, at least 512 bytes)
 * @returns {{ safe: boolean, reason: string|null }}
 */
export function inspectBufferForDangerousContent(buffer) {
  if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
    return { safe: false, reason: 'Buffer too short to inspect' };
  }

  // Scan the first 512 bytes (up from 64) to reduce polyglot attack surface.
  const head = buffer.slice(0, Math.min(buffer.length, 512)).toString('latin1').toLowerCase();

  // Detect HTML/SVG/XML content that could carry executable scripts.
  const dangerousPatterns = [
    /^<(!doctype|html|svg|xml|script)/,
    /^[\s\ufeff]*<(!doctype|html|svg|xml|script)/,
    /<script[\s>]/,
    /<svg[\s>]/,
  ];

  for (const pattern of dangerousPatterns) {
    if (pattern.test(head)) {
      return { safe: false, reason: 'File content resembles HTML/SVG/XML and is not permitted' };
    }
  }

  return { safe: true, reason: null };
}

/**
 * Validate upload file
 * @param {Object} options - Validation options
 * @param {string} options.filename - Filename
 * @param {number} options.size - File size in bytes
 * @param {string[]} options.allowedTypes - Allowed file types
 * @param {number} options.maxSize - Maximum file size
 * @param {Buffer} [options.buffer] - Optional file buffer for content inspection
 * @returns {Object} Validation result
 */
export function validateUpload({ filename, size, allowedTypes, maxSize, buffer }) {
  const errors = [];

  if (!filename) {
    errors.push('Filename is required');
  }

  // Explicitly reject SVG regardless of allowedTypes — SVG can carry JavaScript.
  if (filename && getFileExtension(filename) === '.svg') {
    errors.push('SVG files are not permitted due to script execution risk');
  }

  if (allowedTypes && !validateFileType(filename, allowedTypes)) {
    const typesList = allowedTypes.join(', ');
    errors.push(`File type not allowed. Allowed types: ${typesList}`);
  }

  if (maxSize && !validateFileSize(size, maxSize)) {
    errors.push(`File size exceeds limit of ${formatFileSize(maxSize)}`);
  }

  // If a buffer is provided, apply two independent layers of content inspection.
  if (buffer) {
    // Layer 1 — Positive magic-byte assertion: the buffer must begin with the
    // exact byte sequence that corresponds to the declared file extension.
    // This catches files that lie about their extension entirely.
    if (filename && !validateMagicBytes(buffer, filename)) {
      errors.push('File magic bytes do not match the declared file type');
    }

    // Layer 2 — Dangerous-pattern denylist: scan a wider window for HTML/SVG/XML
    // tags that could carry executable scripts (polyglot defence-in-depth).
    const inspection = inspectBufferForDangerousContent(buffer);
    if (!inspection.safe) {
      errors.push(inspection.reason || 'File content failed safety inspection');
    }
  }

  return {
    valid: errors.length === 0,
    errors,
  };
}

