/**
 * Input Validation Module for DashCaddy
 * Comprehensive validation to prevent injection attacks and ensure data integrity
 */

const path = require('path');
const validator = require('validator');

class ValidationError extends Error {
  constructor(message, field = null) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.statusCode = 400;
  }
}

/**
 * Validate DNS record data
 */
function validateDNSRecord(data) {
  const errors = [];

  // Validate subdomain
  if (!data.subdomain || typeof data.subdomain !== 'string') {
    errors.push({ field: 'subdomain', message: 'Subdomain is required' });
  } else {
    // DNS label validation: alphanumeric and hyphens, 1-63 chars, no leading/trailing hyphens
    const subdomainRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i;
    if (!subdomainRegex.test(data.subdomain)) {
      errors.push({ 
        field: 'subdomain', 
        message: 'Invalid subdomain format. Use only letters, numbers, and hyphens (1-63 chars)' 
      });
    }

    // Prevent DNS injection attempts
    const dangerousChars = [';', '&', '|', '`', '$', '(', ')', '<', '>', '\n', '\r', '\\'];
    if (dangerousChars.some(char => data.subdomain.includes(char))) {
      errors.push({ field: 'subdomain', message: 'Subdomain contains invalid characters' });
    }
  }

  // Validate domain
  if (data.domain && typeof data.domain === 'string') {
    if (!validator.isFQDN(data.domain, { require_tld: false })) {
      errors.push({ field: 'domain', message: 'Invalid domain format' });
    }
  }

  // Validate IP address
  if (!data.ip || typeof data.ip !== 'string') {
    errors.push({ field: 'ip', message: 'IP address is required' });
  } else {
    if (!validator.isIP(data.ip, 4) && !validator.isIP(data.ip, 6)) {
      errors.push({ field: 'ip', message: 'Invalid IP address format' });
    }

    // Prevent SSRF by blocking private IPs in certain contexts
    if (data.blockPrivateIPs && isPrivateIP(data.ip)) {
      errors.push({ field: 'ip', message: 'Private IP addresses are not allowed in this context' });
    }
  }

  // Validate TTL if provided
  if (data.ttl !== undefined) {
    const ttl = parseInt(data.ttl, 10);
    if (isNaN(ttl) || ttl < 60 || ttl > 86400) {
      errors.push({ field: 'ttl', message: 'TTL must be between 60 and 86400 seconds' });
    }
  }

  if (errors.length > 0) {
    const error = new ValidationError('DNS record validation failed');
    error.errors = errors;
    throw error;
  }

  return {
    subdomain: data.subdomain.toLowerCase().trim(),
    domain: data.domain ? data.domain.toLowerCase().trim() : null,
    ip: data.ip.trim(),
    ttl: data.ttl ? parseInt(data.ttl, 10) : 3600
  };
}

/**
 * Validate Docker container deployment data
 */
function validateDockerDeployment(data) {
  const errors = [];

  // Validate container name
  if (!data.name || typeof data.name !== 'string') {
    errors.push({ field: 'name', message: 'Container name is required' });
  } else {
    // Docker name validation: alphanumeric, underscores, periods, hyphens
    const nameRegex = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
    if (!nameRegex.test(data.name)) {
      errors.push({ 
        field: 'name', 
        message: 'Invalid container name. Use only letters, numbers, underscores, periods, and hyphens' 
      });
    }

    if (data.name.length > 255) {
      errors.push({ field: 'name', message: 'Container name too long (max 255 chars)' });
    }
  }

  // Validate Docker image
  if (!data.image || typeof data.image !== 'string') {
    errors.push({ field: 'image', message: 'Docker image is required' });
  } else {
    // Docker image validation: registry/repo:tag format
    // Allow: alpine, nginx:latest, docker.io/library/nginx:1.21, ghcr.io/user/repo:tag
    const imageRegex = /^(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?::[0-9]{1,5})?\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*(?::[a-z0-9]+(?:[._-][a-z0-9]+)*)?$/i;
    
    if (!imageRegex.test(data.image)) {
      errors.push({ 
        field: 'image', 
        message: 'Invalid Docker image format' 
      });
    }

    // Block dangerous image patterns
    const dangerousPatterns = [';', '&', '|', '`', '$', '$(', '&&', '||', '\n', '\r'];
    if (dangerousPatterns.some(pattern => data.image.includes(pattern))) {
      errors.push({ field: 'image', message: 'Docker image contains invalid characters' });
    }

    if (data.image.length > 512) {
      errors.push({ field: 'image', message: 'Docker image name too long' });
    }
  }

  // Validate ports
  if (data.ports) {
    if (!Array.isArray(data.ports)) {
      errors.push({ field: 'ports', message: 'Ports must be an array' });
    } else {
      data.ports.forEach((port, index) => {
        if (typeof port === 'string') {
          // Format: "8080:80" or "8080:80/tcp"
          const portRegex = /^(\d{1,5}):(\d{1,5})(?:\/(tcp|udp))?$/;
          if (!portRegex.test(port)) {
            errors.push({ 
              field: `ports[${index}]`, 
              message: 'Invalid port format. Use "host:container" or "host:container/protocol"' 
            });
          } else {
            const [, hostPort, containerPort] = port.match(portRegex);
            if (!isValidPort(hostPort) || !isValidPort(containerPort)) {
              errors.push({ field: `ports[${index}]`, message: 'Port numbers must be between 1 and 65535' });
            }
          }
        } else if (typeof port === 'number') {
          if (!isValidPort(port)) {
            errors.push({ field: `ports[${index}]`, message: 'Port number must be between 1 and 65535' });
          }
        } else {
          errors.push({ field: `ports[${index}]`, message: 'Invalid port type' });
        }
      });
    }
  }

  // Validate volumes
  if (data.volumes) {
    if (!Array.isArray(data.volumes)) {
      errors.push({ field: 'volumes', message: 'Volumes must be an array' });
    } else {
      data.volumes.forEach((volume, index) => {
        if (typeof volume !== 'string') {
          errors.push({ field: `volumes[${index}]`, message: 'Volume must be a string' });
        } else {
          // Validate volume format and prevent path traversal
          const volumeErrors = validateVolumePath(volume, index);
          errors.push(...volumeErrors);
        }
      });
    }
  }

  // Validate environment variables
  if (data.environment) {
    if (typeof data.environment !== 'object' || Array.isArray(data.environment)) {
      errors.push({ field: 'environment', message: 'Environment must be an object' });
    } else {
      Object.entries(data.environment).forEach(([key, value]) => {
        // Validate env var name
        const envKeyRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
        if (!envKeyRegex.test(key)) {
          errors.push({ 
            field: `environment.${key}`, 
            message: 'Invalid environment variable name' 
          });
        }

        // Ensure value is string or number
        if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
          errors.push({ 
            field: `environment.${key}`, 
            message: 'Environment variable value must be string, number, or boolean' 
          });
        }
      });
    }
  }

  if (errors.length > 0) {
    const error = new ValidationError('Docker deployment validation failed');
    error.errors = errors;
    throw error;
  }

  return {
    name: data.name.trim(),
    image: data.image.trim(),
    ports: data.ports || [],
    volumes: data.volumes || [],
    environment: data.environment || {}
  };
}

/**
 * Validate file path to prevent directory traversal
 */
function validateFilePath(filePath, allowedBasePaths = []) {
  if (!filePath || typeof filePath !== 'string') {
    throw new ValidationError('File path is required', 'path');
  }

  // Normalize path
  const normalized = path.normalize(filePath);

  // Check for directory traversal attempts
  if (normalized.includes('..') || normalized.includes('~')) {
    throw new ValidationError('Path traversal detected', 'path');
  }

  // Block absolute paths to sensitive locations
  const blockedPaths = [
    '/etc',
    '/sys',
    '/proc',
    '/root',
    'C:\\Windows',
    'C:\\Program Files',
    '/var/run',
    '/var/lib/docker'
  ];

  const lowerPath = normalized.toLowerCase();
  if (blockedPaths.some(blocked => lowerPath.startsWith(blocked.toLowerCase()))) {
    throw new ValidationError('Access to this path is not allowed', 'path');
  }

  // If allowed base paths specified, ensure path is within them
  if (allowedBasePaths.length > 0) {
    const isAllowed = allowedBasePaths.some(basePath => {
      const normalizedBase = path.normalize(basePath);
      return normalized.startsWith(normalizedBase);
    });

    if (!isAllowed) {
      throw new ValidationError('Path is outside allowed directories', 'path');
    }
  }

  return normalized;
}

/**
 * Validate volume path for Docker
 */
function validateVolumePath(volume, index) {
  const errors = [];

  // Format: /host/path:/container/path or /host/path:/container/path:ro
  const volumeRegex = /^([^:]+):([^:]+)(?::(ro|rw|z|Z))?$/;
  const match = volume.match(volumeRegex);

  if (!match) {
    errors.push({ 
      field: `volumes[${index}]`, 
      message: 'Invalid volume format. Use "host:container" or "host:container:mode"' 
    });
    return errors;
  }

  const [, hostPath, containerPath, mode] = match;

  // Validate host path
  try {
    validateFilePath(hostPath);
  } catch (error) {
    errors.push({ 
      field: `volumes[${index}].hostPath`, 
      message: `Invalid host path: ${error.message}` 
    });
  }

  // Validate container path
  if (containerPath.includes('..') || !path.isAbsolute(containerPath)) {
    errors.push({ 
      field: `volumes[${index}].containerPath`, 
      message: 'Container path must be absolute and not contain ..' 
    });
  }

  // Validate mode if present
  if (mode && !['ro', 'rw', 'z', 'Z'].includes(mode)) {
    errors.push({ 
      field: `volumes[${index}].mode`, 
      message: 'Invalid volume mode. Use ro, rw, z, or Z' 
    });
  }

  return errors;
}

/**
 * Validate URL
 */
function validateURL(url, options = {}) {
  if (!url || typeof url !== 'string') {
    throw new ValidationError('URL is required', 'url');
  }

  const validatorOptions = {
    protocols: options.protocols || ['http', 'https'],
    require_protocol: options.requireProtocol !== false,
    require_valid_protocol: true,
    allow_underscores: false,
    ...options
  };

  if (!validator.isURL(url, validatorOptions)) {
    throw new ValidationError('Invalid URL format', 'url');
  }

  // Block localhost/private IPs if specified
  if (options.blockPrivate) {
    try {
      const urlObj = new URL(url);
      if (urlObj.hostname === 'localhost' || 
          urlObj.hostname === '127.0.0.1' ||
          isPrivateIP(urlObj.hostname)) {
        throw new ValidationError('Private URLs are not allowed', 'url');
      }
    } catch (e) {
      if (e instanceof ValidationError) throw e;
      throw new ValidationError('Invalid URL', 'url');
    }
  }

  return url;
}

/**
 * Validate API token format
 */
function validateToken(token) {
  if (!token || typeof token !== 'string') {
    throw new ValidationError('Token is required', 'token');
  }

  // Token should be alphanumeric with possible special chars, reasonable length
  if (token.length < 8) {
    throw new ValidationError('Token too short (minimum 8 characters)', 'token');
  }

  if (token.length > 512) {
    throw new ValidationError('Token too long (maximum 512 characters)', 'token');
  }

  // Block obvious injection attempts
  const dangerousPatterns = [';', '&', '|', '`', '\n', '\r', '$(', '&&'];
  if (dangerousPatterns.some(pattern => token.includes(pattern))) {
    throw new ValidationError('Token contains invalid characters', 'token');
  }

  return token.trim();
}

/**
 * Validate service configuration
 */
function validateServiceConfig(service) {
  const errors = [];

  // Validate ID
  if (!service.id || typeof service.id !== 'string') {
    errors.push({ field: 'id', message: 'Service ID is required' });
  } else {
    const idRegex = /^[a-z0-9-_]+$/i;
    if (!idRegex.test(service.id)) {
      errors.push({ field: 'id', message: 'Invalid service ID format' });
    }
  }

  // Validate name
  if (!service.name || typeof service.name !== 'string') {
    errors.push({ field: 'name', message: 'Service name is required' });
  } else if (service.name.length > 100) {
    errors.push({ field: 'name', message: 'Service name too long (max 100 chars)' });
  }

  // Validate URL if provided
  if (service.url) {
    try {
      validateURL(service.url);
    } catch (error) {
      errors.push({ field: 'url', message: error.message });
    }
  }

  // Validate port if provided
  if (service.port !== undefined && !isValidPort(service.port)) {
    errors.push({ field: 'port', message: 'Invalid port number' });
  }

  if (errors.length > 0) {
    const error = new ValidationError('Service configuration validation failed');
    error.errors = errors;
    throw error;
  }

  return service;
}

/**
 * Helper: Check if port is valid
 */
function isValidPort(port) {
  const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
  return !isNaN(portNum) && portNum >= 1 && portNum <= 65535;
}

/**
 * Helper: Check if IP is private
 */
function isPrivateIP(ip) {
  // IPv4 private ranges
  const privateRanges = [
    /^10\./,
    /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
    /^192\.168\./,
    /^127\./,
    /^169\.254\./,
    /^::1$/,
    /^fc00:/,
    /^fe80:/
  ];

  return privateRanges.some(range => range.test(ip));
}

/**
 * Sanitize string for safe display (prevent XSS)
 */
function sanitizeString(str, maxLength = 1000) {
  if (typeof str !== 'string') return '';

  return str
    .slice(0, maxLength)
    .replace(/[<>'"]/g, char => {
      const entities = { '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' };
      return entities[char] || char;
    });
}

/**
 * Validate secure path with realpath resolution and traversal detection
 * This is CRITICAL for preventing path traversal attacks
 * @param {string} requestedPath - The path requested by the user
 * @param {Array<string>} allowedRoots - Array of allowed root directories
 * @param {object} auditLogger - Optional audit logger for security events
 * @returns {Promise<string>} - Resolved safe path
 */
async function validateSecurePath(requestedPath, allowedRoots, auditLogger = null) {
  const fs = require('fs').promises;

  if (!requestedPath || typeof requestedPath !== 'string') {
    throw new ValidationError('Path is required', 'path');
  }

  if (!Array.isArray(allowedRoots) || allowedRoots.length === 0) {
    throw new ValidationError('No allowed roots configured', 'path');
  }

  // Check for null byte injection
  if (requestedPath.includes('\0')) {
    if (auditLogger) {
      auditLogger.logSecurityEvent('path_traversal_blocked', {
        requestedPath,
        reason: 'null_byte_detected',
        severity: 'high'
      });
    }
    throw new ValidationError('Invalid path - null byte detected', 'path');
  }

  // Check for encoded traversal sequences
  const decodedPath = decodeURIComponent(requestedPath);
  const suspiciousPatterns = [
    /\.\./,           // ..
    /%2e%2e/i,        // URL encoded ..
    /\.%2f/i,        // .%2F (encoded ./)
    /%2e\./i,         // %2E.
    /\.\\/,           // .\ (Windows)
    /%5c/i            // URL encoded backslash
  ];

  if (suspiciousPatterns.some(pattern => pattern.test(requestedPath)) ||
      suspiciousPatterns.some(pattern => pattern.test(decodedPath))) {
    if (auditLogger) {
      auditLogger.logSecurityEvent('path_traversal_blocked', {
        requestedPath,
        decodedPath,
        reason: 'traversal_sequence_detected',
        severity: 'high'
      });
    }
    throw new ValidationError('Path traversal detected', 'path');
  }

  // Normalize the path for the current platform
  const normalized = path.normalize(requestedPath);

  // Try to resolve the real path (follows symlinks)
  let realPath;
  try {
    realPath = await fs.realpath(normalized);
  } catch (error) {
    if (error.code === 'ENOENT') {
      // Path doesn't exist - that's okay, just use normalized path
      // But we still need to check if parent exists and is within allowed roots
      const parentDir = path.dirname(normalized);
      try {
        const parentReal = await fs.realpath(parentDir);
        // Construct the real path using the resolved parent
        realPath = path.join(parentReal, path.basename(normalized));
      } catch (parentError) {
        if (parentError.code === 'ENOENT') {
          // Parent doesn't exist either - use normalized path
          realPath = normalized;
        } else if (parentError.code === 'EACCES') {
          throw new ValidationError('Access denied to path', 'path');
        } else {
          throw parentError;
        }
      }
    } else if (error.code === 'EACCES') {
      throw new ValidationError('Access denied to path', 'path');
    } else {
      throw error;
    }
  }

  // Normalize for cross-platform comparison (Windows is case-insensitive)
  const isWindows = process.platform === 'win32';
  const normalizePath = (p) => {
    const normalized = path.normalize(p).replace(/\\/g, '/');
    return isWindows ? normalized.toLowerCase() : normalized;
  };

  const normalizedReal = normalizePath(realPath);

  // Check if the resolved path is within any allowed root
  const isWithinAllowedRoot = allowedRoots.some(root => {
    const normalizedRoot = normalizePath(root);
    return normalizedReal.startsWith(normalizedRoot);
  });

  if (!isWithinAllowedRoot) {
    if (auditLogger) {
      auditLogger.logSecurityEvent('path_traversal_blocked', {
        requestedPath,
        realPath,
        allowedRoots,
        reason: 'outside_allowed_roots',
        severity: 'critical'
      });
    }
    throw new ValidationError('Access denied - path is outside allowed directories', 'path');
  }

  return realPath;
}

module.exports = {
  ValidationError,
  validateDNSRecord,
  validateDockerDeployment,
  validateVolumePath,
  validateFilePath,
  validateURL,
  validateToken,
  validateServiceConfig,
  sanitizeString,
  isValidPort,
  isPrivateIP,
  validateSecurePath
};
