/**
 * Path Traversal Prevention
 * Provides comprehensive protection against path traversal attacks
 */

import * as path from 'path';
import type { PathValidationOptions } from './types';

export class PathTraversalPrevention {
  private static readonly DANGEROUS_PATTERNS = [
    // Directory traversal patterns
    /\.\./g,
    /\.\.\/|\.\.\\/,
    /\.\.\//g,
    /\.\.\\/g,

    // URL encoded traversal
    /%2e%2e%2f/gi,
    /%2e%2e%5c/gi,
    /%2e%2e/gi,

    // Double URL encoded
    /%252e%252e%252f/gi,
    /%252e%252e%255c/gi,

    // Unicode encoded
    /\u002e\u002e\u002f/g,
    /\u002e\u002e\u005c/g,

    // Null byte injection
    /\x00/g,
    /%00/gi,

    // Absolute path attempts
    /^\/|^[a-zA-Z]:\\/,

    // UNC path attempts (Windows)
    /^\\\\|^\\\\/,

    // Stream access (Windows)
    /:/g
  ];

  private static readonly DANGEROUS_EXTENSIONS = [
    '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar',
    '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl', '.sh', '.bash'
  ];

  private static readonly SYSTEM_DIRECTORIES = [
    'windows', 'system32', 'program files', 'programdata',
    'etc', 'bin', 'sbin', 'usr', 'var', 'tmp', 'boot',
    'proc', 'sys', 'dev', 'root'
  ];

  /**
   * Validates a file path for traversal attacks
   */
  public static validatePath(
    inputPath: string,
    options: PathValidationOptions = {}
  ): { isValid: boolean; errors: string[]; sanitizedPath?: string } {
    const errors: string[] = [];

    if (!inputPath || typeof inputPath !== 'string') {
      errors.push('Path must be a non-empty string');
      return { isValid: false, errors };
    }

    // Check for dangerous patterns
    for (const pattern of this.DANGEROUS_PATTERNS) {
      if (pattern.test(inputPath)) {
        errors.push(`Dangerous path pattern detected: ${pattern.source}`);
      }
    }

    // Check for null bytes and control characters
    if (/[\x00-\x1f\x7f-\x9f]/.test(inputPath)) {
      errors.push('Path contains control characters or null bytes');
    }

    // Check path length
    if (inputPath.length > 260) { // Windows MAX_PATH limit
      errors.push('Path exceeds maximum allowed length');
    }

    // Normalize and resolve the path
    let normalizedPath: string;
    try {
      normalizedPath = path.normalize(inputPath);

      // Check if normalization revealed traversal attempts
      if (normalizedPath.includes('..')) {
        errors.push('Path contains directory traversal after normalization');
      }
    } catch (error) {
      errors.push('Path normalization failed');
      return { isValid: false, errors };
    }

    // Check absolute vs relative path restrictions
    const isAbsolute = path.isAbsolute(normalizedPath);
    if (isAbsolute && !options.allowAbsolute) {
      errors.push('Absolute paths are not allowed');
    }
    if (!isAbsolute && options.allowRelative === false) {
      errors.push('Relative paths are not allowed');
    }

    // Check base path restriction
    if (options.basePath && isAbsolute) {
      const resolvedBase = path.resolve(options.basePath);
      const resolvedPath = path.resolve(normalizedPath);

      if (!resolvedPath.startsWith(resolvedBase)) {
        errors.push('Path is outside allowed base directory');
      }
    }

    // Check file extension
    const ext = path.extname(normalizedPath).toLowerCase();
    if (ext) {
      if (options.blockedExtensions?.includes(ext)) {
        errors.push(`File extension '${ext}' is not allowed`);
      }

      if (options.allowedExtensions && !options.allowedExtensions.includes(ext)) {
        errors.push(`File extension '${ext}' is not in allowed list`);
      }

      if (this.DANGEROUS_EXTENSIONS.includes(ext)) {
        errors.push(`Potentially dangerous file extension '${ext}' detected`);
      }
    }

    // Check directory depth
    if (options.maxDepth) {
      const depth = normalizedPath.split(path.sep).length - 1;
      if (depth > options.maxDepth) {
        errors.push(`Path depth (${depth}) exceeds maximum allowed (${options.maxDepth})`);
      }
    }

    // Check for system directories
    const pathParts = normalizedPath.toLowerCase().split(path.sep);
    for (const systemDir of this.SYSTEM_DIRECTORIES) {
      if (pathParts.includes(systemDir)) {
        errors.push(`Access to system directory '${systemDir}' is not allowed`);
      }
    }

    return {
      isValid: errors.length === 0,
      errors,
      ...(errors.length === 0 && { sanitizedPath: normalizedPath })
    };
  }

  /**
   * Sanitizes a path by removing dangerous elements
   */
  public static sanitizePath(inputPath: string, options: PathValidationOptions = {}): string {
    if (!inputPath || typeof inputPath !== 'string') {
      return '';
    }

    let sanitized = inputPath;

    // Remove dangerous patterns
    for (const pattern of this.DANGEROUS_PATTERNS) {
      sanitized = sanitized.replace(pattern, '');
    }

    // Remove control characters and null bytes
    sanitized = sanitized.replace(/[\x00-\x1f\x7f-\x9f]/g, '');

    // Remove multiple consecutive slashes/backslashes
    sanitized = sanitized.replace(/[\/\\]+/g, path.sep);

    // Remove leading/trailing whitespace and path separators
    sanitized = sanitized.trim().replace(/^[\/\\]+|[\/\\]+$/g, '');

    // Normalize the path
    try {
      sanitized = path.normalize(sanitized);
    } catch {
      // If normalization fails, return empty string
      return '';
    }

    // Ensure it doesn't start with .. after sanitization
    if (sanitized.startsWith('..')) {
      sanitized = sanitized.replace(/^\.\.+[\/\\]?/, '');
    }

    // Apply length limit
    if (sanitized.length > 255) {
      sanitized = sanitized.substring(0, 255);
    }

    return sanitized;
  }

  /**
   * Creates a safe filename from user input
   */
  public static createSafeFilename(filename: string, options: {
    maxLength?: number;
    allowedExtensions?: string[];
    defaultExtension?: string;
  } = {}): string {
    if (!filename || typeof filename !== 'string') {
      return 'untitled';
    }

    const maxLength = options.maxLength || 100;
    let safe = filename;

    // Remove path separators and dangerous characters
    safe = safe.replace(/[\/\\:*?"<>|]/g, '_');

    // Remove control characters
    safe = safe.replace(/[\x00-\x1f\x7f-\x9f]/g, '');

    // Remove leading/trailing dots and spaces
    safe = safe.replace(/^[\.\s]+|[\.\s]+$/g, '');

    // Ensure it's not empty after sanitization
    if (!safe) {
      safe = 'untitled';
    }

    // Handle extension
    const ext = path.extname(safe).toLowerCase();
    const basename = path.basename(safe, ext);

    let finalExt = ext;
    if (options.allowedExtensions && ext && !options.allowedExtensions.includes(ext)) {
      finalExt = options.defaultExtension || '.txt';
    }

    // Truncate if too long
    const maxBasenameLength = maxLength - finalExt.length;
    const truncatedBasename = basename.length > maxBasenameLength
      ? basename.substring(0, maxBasenameLength)
      : basename;

    return truncatedBasename + finalExt;
  }

  /**
   * Validates multiple paths at once
   */
  public static validatePaths(
    paths: string[],
    options: PathValidationOptions = {}
  ): { isValid: boolean; results: Array<{ path: string; isValid: boolean; errors: string[] }> } {
    const results = paths.map(inputPath => ({
      path: inputPath,
      ...this.validatePath(inputPath, options)
    }));

    return {
      isValid: results.every(result => result.isValid),
      results
    };
  }
}
