<?php

/**
 * Sanitize user-entered free text before storage (defense in depth; queries use prepared statements).
 */
function sanitize_free_text(mixed $value, int $maxLength = 2000): string {
    if (!is_string($value) && !is_numeric($value)) {
        return '';
    }
    $s = trim((string) $value);
    if ($s === '') {
        return '';
    }

    $maxLength = max(1, min($maxLength, 10000));
    $s = mb_substr($s, 0, $maxLength);

    // Control characters (keep tab/newline for multiline answers).
    $s = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $s) ?? '';

    $s = str_replace(["\0", '--', '/*', '*/', ';'], '', $s);

    $patterns = [
        '/\bunion\s+select\b/i',
        '/\bselect\s+.+\s+from\b/i',
        '/\binsert\s+into\b/i',
        '/\bupdate\s+.+\s+set\b/i',
        '/\bdelete\s+from\b/i',
        '/\bdrop\s+(table|database)\b/i',
        '/\bexec(\s|\()/i',
        '/\bxp_\w+/i',
        '/\bor\s+1\s*=\s*1\b/i',
        '/\band\s+1\s*=\s*1\b/i',
        "/'\s*or\s+'/i",
        '/"\s*or\s+"/i',
        '/\bchar\s*\(/i',
        '/\bconcat\s*\(/i',
        '/\bsleep\s*\(/i',
        '/\bbenchmark\s*\(/i',
    ];
    foreach ($patterns as $pattern) {
        $s = preg_replace($pattern, '', $s) ?? $s;
    }

    return trim(preg_replace('/\s{2,}/u', ' ', $s) ?? '');
}

/**
 * Returns sanitized text or calls json_error when nothing safe remains.
 */
function require_safe_free_text(mixed $value, string $fieldName, int $maxLength = 2000): string {
    $raw = is_string($value) || is_numeric($value) ? trim((string) $value) : '';
    if ($raw === '') {
        json_error('INVALID_FIELD', "$fieldName is required", 400);
    }
    $clean = sanitize_free_text($raw, $maxLength);
    if ($clean === '') {
        json_error('INVALID_FIELD', "$fieldName contains disallowed content", 400);
    }
    return $clean;
}
