<?php
/**
 * Enhanced Security Configuration for ENSI.LK API
 * Comprehensive protection against SQL injection, XSS, and other attacks
 */

// API Security Configuration
define('API_KEY', 'ensi_app_2025_secure_key_v1');
define('ALLOWED_USER_AGENTS', [
    'Dart/',
    'ensi-flutter-app',
    'ensi-tts-service'
]);

// Rate limiting configuration
define('RATE_LIMIT_REQUESTS', 100);
define('RATE_LIMIT_WINDOW', 60);

/**
 * Comprehensive input validation and sanitization class
 */
class SecurityValidator {
    
    // Maximum lengths for different input types
    const MAX_EMAIL_LENGTH = 254;
    const MAX_USERNAME_LENGTH = 50;
    const MAX_PASSWORD_LENGTH = 128;
    const MAX_COMMENT_LENGTH = 1000;
    const MAX_NAME_LENGTH = 100;
    
    // Minimum password requirements
    const MIN_PASSWORD_LENGTH = 8;
    
    /**
     * Validate and sanitize email input
     */
    public static function validateEmail($email) {
        // Trim and convert to lowercase
        $email = trim(strtolower($email));
        
        // Check length
        if (strlen($email) > self::MAX_EMAIL_LENGTH) {
            throw new InvalidArgumentException('Email too long (max ' . self::MAX_EMAIL_LENGTH . ' characters)');
        }
        
        // Validate format using PHP's built-in filter
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email format');
        }
        
        // Additional security: check for dangerous patterns
        if (self::containsSQLPatterns($email) || self::containsXSSPatterns($email)) {
            throw new InvalidArgumentException('Email contains invalid characters');
        }
        
        return $email;
    }
    
    /**
     * Validate and sanitize username input
     */
    public static function validateUsername($username) {
        // Trim whitespace
        $username = trim($username);
        
        // Check length
        if (strlen($username) < 3) {
            throw new InvalidArgumentException('Username must be at least 3 characters long');
        }
        
        if (strlen($username) > self::MAX_USERNAME_LENGTH) {
            throw new InvalidArgumentException('Username too long (max ' . self::MAX_USERNAME_LENGTH . ' characters)');
        }
        
        // Allow only alphanumeric characters, underscores, and hyphens
        if (!preg_match('/^[a-zA-Z0-9_-]+$/', $username)) {
            throw new InvalidArgumentException('Username can only contain letters, numbers, underscores, and hyphens');
        }
        
        // Check for reserved words
        $reservedWords = ['admin', 'root', 'system', 'null', 'undefined', 'test'];
        if (in_array(strtolower($username), $reservedWords)) {
            throw new InvalidArgumentException('Username is reserved');
        }
        
        return $username;
    }
    
    /**
     * Validate password strength
     */
    public static function validatePassword($password) {
        // Check length
        if (strlen($password) < self::MIN_PASSWORD_LENGTH) {
            throw new InvalidArgumentException('Password must be at least ' . self::MIN_PASSWORD_LENGTH . ' characters long');
        }
        
        if (strlen($password) > self::MAX_PASSWORD_LENGTH) {
            throw new InvalidArgumentException('Password too long (max ' . self::MAX_PASSWORD_LENGTH . ' characters)');
        }
        
        // Check for at least one number
        if (!preg_match('/[0-9]/', $password)) {
            throw new InvalidArgumentException('Password must contain at least one number');
        }
        
        // Check for at least one letter
        if (!preg_match('/[a-zA-Z]/', $password)) {
            throw new InvalidArgumentException('Password must contain at least one letter');
        }
        
        // Check for common weak passwords
        $weakPasswords = ['password', '12345678', 'qwerty123', 'admin123'];
        if (in_array(strtolower($password), $weakPasswords)) {
            throw new InvalidArgumentException('Password is too weak');
        }
        
        return $password;
    }
    
    /**
     * Validate and sanitize comment input
     */
    public static function validateComment($comment) {
        // Trim whitespace
        $comment = trim($comment);
        
        // Check if empty
        if (empty($comment)) {
            throw new InvalidArgumentException('Comment cannot be empty');
        }
        
        // Check length
        if (strlen($comment) > self::MAX_COMMENT_LENGTH) {
            throw new InvalidArgumentException('Comment too long (max ' . self::MAX_COMMENT_LENGTH . ' characters)');
        }
        
        // Remove HTML tags (prevent XSS)
        $comment = strip_tags($comment);
        
        // Escape HTML entities
        $comment = htmlspecialchars($comment, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        
        // Check for SQL injection patterns
        if (self::containsSQLPatterns($comment)) {
            throw new InvalidArgumentException('Comment contains invalid characters');
        }
        
        // Check for spam patterns
        if (self::containsSpamPatterns($comment)) {
            throw new InvalidArgumentException('Comment appears to be spam');
        }
        
        return $comment;
    }
    
    /**
     * Validate and sanitize general text input
     */
    public static function validateText($text, $maxLength = 255, $allowHTML = false) {
        // Trim whitespace
        $text = trim($text);
        
        // Check length
        if (strlen($text) > $maxLength) {
            throw new InvalidArgumentException("Text too long (max {$maxLength} characters)");
        }
        
        // Remove or escape HTML
        if (!$allowHTML) {
            $text = strip_tags($text);
        }
        
        // Escape HTML entities
        $text = htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        
        // Check for dangerous patterns
        if (self::containsSQLPatterns($text) || self::containsXSSPatterns($text)) {
            throw new InvalidArgumentException('Text contains invalid characters');
        }
        
        return $text;
    }
    
    /**
     * Check for SQL injection patterns
     */
    private static function containsSQLPatterns($input) {
        $sqlPatterns = [
            '/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION)\b)/i',
            '/(\-\-|\#|\/\*|\*\/)/i',
            '/(\'|\"|`|;|\||&)/i',
            '/(\b(OR|AND)\b.*=)/i',
            '/(\b(SCRIPT|JAVASCRIPT|VBSCRIPT)\b)/i'
        ];
        
        foreach ($sqlPatterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check for XSS patterns
     */
    private static function containsXSSPatterns($input) {
        $xssPatterns = [
            '/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi',
            '/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi',
            '/javascript:/i',
            '/on\w+\s*=/i',
            '/<.*?>/i'
        ];
        
        foreach ($xssPatterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check for spam patterns
     */
    private static function containsSpamPatterns($input) {
        $spamPatterns = [
            '/\b(viagra|cialis|casino|lottery|winner|prize)\b/i',
            '/\b(click here|free money|make money|get rich)\b/i',
            '/(http:\/\/|https:\/\/|www\.)/i', // URLs in comments
            '/(.)\1{10,}/', // Repeated characters
            '/[^\w\s.,!?-]/u' // Non-standard characters
        ];
        
        foreach ($spamPatterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Generate secure random token
     */
    public static function generateSecureToken($length = 32) {
        return bin2hex(random_bytes($length));
    }
    
    /**
     * Hash password securely
     */
    public static function hashPassword($password) {
        return password_hash($password, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536, // 64 MB
            'time_cost' => 4,       // 4 iterations
            'threads' => 3,         // 3 threads
        ]);
    }
    
    /**
     * Verify password hash
     */
    public static function verifyPassword($password, $hash) {
        return password_verify($password, $hash);
    }
}

/**
 * Rate limiting class
 */
class RateLimiter {
    private static $cacheFile = __DIR__ . '/cache/rate_limits.json';
    
    public static function checkLimit($identifier, $maxRequests = RATE_LIMIT_REQUESTS, $window = RATE_LIMIT_WINDOW) {
        // Create cache directory if it doesn't exist
        $cacheDir = dirname(self::$cacheFile);
        if (!file_exists($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }
        
        // Load existing data
        $data = [];
        if (file_exists(self::$cacheFile)) {
            $data = json_decode(file_get_contents(self::$cacheFile), true) ?: [];
        }
        
        $now = time();
        $windowStart = $now - $window;
        
        // Clean old entries
        foreach ($data as $id => $requests) {
            $data[$id] = array_filter($requests, function($timestamp) use ($windowStart) {
                return $timestamp > $windowStart;
            });
            
            if (empty($data[$id])) {
                unset($data[$id]);
            }
        }
        
        // Check current identifier
        if (!isset($data[$identifier])) {
            $data[$identifier] = [];
        }
        
        if (count($data[$identifier]) >= $maxRequests) {
            return false; // Rate limit exceeded
        }
        
        // Add current request
        $data[$identifier][] = $now;
        
        // Save data
        file_put_contents(self::$cacheFile, json_encode($data));
        
        return true;
    }
}

/**
 * Enhanced API access validation
 */
function validateApiAccess() {
    // Rate limiting by IP
    $clientIP = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    if (!RateLimiter::checkLimit($clientIP)) {
        http_response_code(429);
        echo json_encode([
            'success' => false,
            'message' => 'Rate limit exceeded. Please try again later.',
            'status_code' => 429
        ]);
        exit;
    }
    
    // Check API Key
    $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? $_GET['api_key'] ?? '';
    if ($apiKey !== API_KEY) {
        // Log unauthorized access attempt
        error_log("Unauthorized API access attempt from IP: $clientIP");
        
        http_response_code(403);
        echo json_encode([
            'success' => false,
            'message' => 'Unauthorized access. Invalid API key.',
            'status_code' => 403
        ]);
        exit;
    }
    
    // Check User Agent
    $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $isValidUserAgent = false;
    
    foreach (ALLOWED_USER_AGENTS as $allowedAgent) {
        if (strpos($userAgent, $allowedAgent) !== false) {
            $isValidUserAgent = true;
            break;
        }
    }
    
    if (!$isValidUserAgent) {
        error_log("Invalid user agent access attempt: $userAgent from IP: $clientIP");
        
        http_response_code(403);
        echo json_encode([
            'success' => false,
            'message' => 'Unauthorized access. Invalid client.',
            'status_code' => 403
        ]);
        exit;
    }
    
    // Log successful access
    error_log("API Access: $userAgent from $clientIP");
}

/**
 * Validate CSRF token (for session-based requests)
 */
function validateCSRFToken($token, $sessionToken) {
    return hash_equals($sessionToken, $token);
}

/**
 * Sanitize output for JSON response
 */
function sanitizeOutput($data) {
    if (is_array($data)) {
        return array_map('sanitizeOutput', $data);
    } elseif (is_string($data)) {
        return htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }
    return $data;
}

/**
 * Secure JSON response function
 */
function sendSecureResponse($success, $message, $data = null, $httpCode = 200) {
    http_response_code($httpCode);
    
    $response = [
        'success' => (bool)$success,
        'message' => SecurityValidator::validateText($message, 500),
        'timestamp' => time()
    ];
    
    if ($data !== null) {
        $response['data'] = sanitizeOutput($data);
    }
    
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    exit;
}

?>