/**
 * Input Validator
 * Comprehensive input validation and sanitization system
 */

import { PathTraversalPrevention } from './path-traversal-prevention';
import { SqlInjectionPrevention } from './sql-injection-prevention';
import type {
  InputValidatorConfig,
  SanitizationOptions,
  ValidationError,
  ValidationResult,
  ValidationRule,
  ValidationSchema
} from './types';

export class InputValidator {
  private config: Required<InputValidatorConfig>;
  private customRules: Map<string, ValidationRule> = new Map();

  constructor(config: InputValidatorConfig = {}) {
    this.config = {
      enableSqlInjectionPrevention: config.enableSqlInjectionPrevention ?? true,
      enablePathTraversalPrevention: config.enablePathTraversalPrevention ?? true,
      enableXssProtection: config.enableXssProtection ?? true,
      enableRateLimiting: config.enableRateLimiting ?? false,
      customRules: config.customRules ?? [],
      sanitizationDefaults: {
        stripHtml: true,
        escapeHtml: true,
        trimWhitespace: true,
        normalizeUnicode: false,
        maxLength: 10000,
        allowedChars: /^[\w\s\-_.@#$%^&*()+={}[\]|\\:";'<>?,./`~!]*$/,
        blockedPatterns: [],
        ...config.sanitizationDefaults
      }
    };

    // Register custom rules
    this.config.customRules.forEach(rule => {
      this.customRules.set(rule.name, rule);
    });

    // Register built-in rules
    this.registerBuiltInRules();
  }

  /**
   * Validates input against a schema
   */
  public validate(data: Record<string, any>, schema: ValidationSchema): ValidationResult {
    const errors: ValidationError[] = [];
    const sanitizedData: Record<string, any> = {};

    for (const [field, rules] of Object.entries(schema)) {
      const value = data[field];
      let sanitizedValue = value;

      for (const rule of rules) {
        try {
          // Validate the original value first (before sanitization)
          if (!rule.validate(sanitizedValue)) {
            errors.push({
              field,
              rule: rule.name,
              message: rule.message,
              value: sanitizedValue
            });
          }

          // Apply sanitization if available
          if (rule.sanitize) {
            sanitizedValue = rule.sanitize(sanitizedValue);
          }
        } catch (error) {
          errors.push({
            field,
            rule: rule.name,
            message: error instanceof Error ? error.message : 'Validation error',
            value: sanitizedValue
          });
        }
      }

      sanitizedData[field] = sanitizedValue;
    }

    return {
      isValid: errors.length === 0,
      errors,
      sanitizedData
    };
  }

  /**
   * Sanitizes a single value
   */
  public sanitize(value: any, options: SanitizationOptions = {}): any {
    const opts = { ...this.config.sanitizationDefaults, ...options };

    if (typeof value !== 'string') {
      return value;
    }

    let sanitized = value;

    // Trim whitespace
    if (opts.trimWhitespace) {
      sanitized = sanitized.trim();
    }

    // Normalize unicode
    if (opts.normalizeUnicode) {
      sanitized = sanitized.normalize('NFC');
    }

    // Apply max length
    if (opts.maxLength && sanitized.length > opts.maxLength) {
      sanitized = sanitized.substring(0, opts.maxLength);
    }

    // Remove blocked patterns
    if (opts.blockedPatterns) {
      for (const pattern of opts.blockedPatterns) {
        sanitized = sanitized.replace(pattern, '');
      }
    }

    // Filter allowed characters
    if (opts.allowedChars) {
      sanitized = sanitized.replace(new RegExp(`[^${opts.allowedChars.source.slice(2, -2)}]`, 'g'), '');
    }

    // HTML handling
    if (opts.stripHtml) {
      sanitized = this.stripHtml(sanitized);
    } else if (opts.escapeHtml) {
      sanitized = this.escapeHtml(sanitized);
    }

    return sanitized;
  }

  /**
   * Validates and sanitizes input for SQL injection
   */
  public validateSqlInput(input: any): { isValid: boolean; sanitized: any; threats: string[] } {
    if (!this.config.enableSqlInjectionPrevention) {
      return { isValid: true, sanitized: input, threats: [] };
    }

    try {
      const result = SqlInjectionPrevention.validateAndSanitizeInputs(
        typeof input === 'object' ? input : { value: input }
      );

      const sanitized = typeof input === 'object'
        ? result.sanitizedInputs
        : result.sanitizedInputs.value ?? input;

      return {
        isValid: result.isValid,
        sanitized,
        threats: result.threats.flatMap(t => t.threats.map(threat => threat.description))
      };
    } catch (error) {
      return {
        isValid: false,
        sanitized: input,
        threats: [error instanceof Error ? error.message : 'SQL validation error']
      };
    }
  }

  /**
   * Validates path for traversal attacks
   */
  public validatePath(path: string): { isValid: boolean; sanitized?: string; errors: string[] } {
    if (!this.config.enablePathTraversalPrevention) {
      return { isValid: true, sanitized: path, errors: [] };
    }

    return PathTraversalPrevention.validatePath(path);
  }

  /**
   * Creates a validation rule
   */
  public createRule(
    name: string,
    validate: (value: any) => boolean,
    message: string,
    sanitize?: (value: any) => any
  ): ValidationRule {
    const rule: ValidationRule = {
      name,
      validate,
      message,
      ...(sanitize && { sanitize })
    };

    this.customRules.set(name, rule);
    return rule;
  }

  /**
   * Gets a validation rule by name
   */
  public getRule(name: string): ValidationRule | undefined {
    return this.customRules.get(name);
  }

  /**
   * Registers built-in validation rules
   */
  private registerBuiltInRules(): void {
    // Required field rule
    this.customRules.set('required', {
      name: 'required',
      validate: (value: any) => value !== null && value !== undefined && value !== '',
      message: 'This field is required'
    });

    // Email validation rule
    this.customRules.set('email', {
      name: 'email',
      validate: (value: string) => {
        if (!value) return true; // Allow empty for optional fields
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(value);
      },
      message: 'Please enter a valid email address',
      sanitize: (value: string) => value?.toLowerCase().trim()
    });

    // URL validation rule
    this.customRules.set('url', {
      name: 'url',
      validate: (value: string) => {
        if (!value) return true;
        try {
          new URL(value);
          return true;
        } catch {
          return false;
        }
      },
      message: 'Please enter a valid URL'
    });

    // Numeric validation rule
    this.customRules.set('numeric', {
      name: 'numeric',
      validate: (value: any) => {
        if (value === null || value === undefined || value === '') return true;
        return !isNaN(Number(value));
      },
      message: 'This field must be a number',
      sanitize: (value: any) => {
        const num = Number(value);
        return isNaN(num) ? value : num;
      }
    });

    // Integer validation rule
    this.customRules.set('integer', {
      name: 'integer',
      validate: (value: any) => {
        if (value === null || value === undefined || value === '') return true;
        const num = Number(value);
        return Number.isInteger(num);
      },
      message: 'This field must be an integer',
      sanitize: (value: any) => {
        const num = Number(value);
        return Number.isInteger(num) ? Math.floor(num) : value;
      }
    });

    // Length validation rules
    this.customRules.set('minLength', {
      name: 'minLength',
      validate: (value: string, min: number = 0) => {
        if (!value) return true;
        return value.length >= min;
      },
      message: 'This field is too short'
    });

    this.customRules.set('maxLength', {
      name: 'maxLength',
      validate: (value: string, max: number = Infinity) => {
        if (!value) return true;
        return value.length <= max;
      },
      message: 'This field is too long',
      sanitize: (value: string, max: number = Infinity) => {
        return value?.substring(0, max);
      }
    });

    // Pattern validation rule
    this.customRules.set('pattern', {
      name: 'pattern',
      validate: (value: any) => {
        if (!value) return true;
        // Note: This is a simplified pattern validation
        // In practice, you'd need to pass the pattern differently
        return true;
      },
      message: 'This field format is invalid'
    });

    // SQL injection prevention rule
    if (this.config.enableSqlInjectionPrevention) {
      this.customRules.set('noSqlInjection', {
        name: 'noSqlInjection',
        validate: (value: string) => {
          if (!value) return true;
          const result = SqlInjectionPrevention.validateInput(value);
          return result.isValid;
        },
        message: 'Input contains potentially dangerous SQL patterns',
        sanitize: (value: string) => {
          if (!value) return value;
          return SqlInjectionPrevention.sanitizeInput(value);
        }
      });
    }

    // Path traversal prevention rule
    if (this.config.enablePathTraversalPrevention) {
      this.customRules.set('safePath', {
        name: 'safePath',
        validate: (value: string) => {
          if (!value) return true;
          const result = PathTraversalPrevention.validatePath(value);
          return result.isValid;
        },
        message: 'Path contains potentially dangerous patterns',
        sanitize: (value: string) => {
          if (!value) return value;
          return PathTraversalPrevention.sanitizePath(value);
        }
      });
    }

    // XSS prevention rule
    if (this.config.enableXssProtection) {
      this.customRules.set('noXss', {
        name: 'noXss',
        validate: (value: string) => {
          if (!value) return true;
          // Basic XSS pattern detection
          const xssPatterns = [
            /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
            /javascript:/gi,
            /on\w+\s*=/gi,
            /<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi
          ];

          return !xssPatterns.some(pattern => pattern.test(value));
        },
        message: 'Input contains potentially dangerous script content',
        sanitize: (value: string) => this.escapeHtml(value)
      });
    }
  }

  /**
   * Strips HTML tags from input
   */
  private stripHtml(input: string): string {
    return input.replace(/<[^>]*>/g, '');
  }

  /**
   * Escapes HTML characters
   */
  private escapeHtml(input: string): string {
    const htmlEscapes: Record<string, string> = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      '/': '&#x2F;'
    };

    return input.replace(/[&<>"'/]/g, (match) => htmlEscapes[match] || match);
  }
}
