<?php
// ============================================================
//  MJPOS – Security Helper
//  XSS · Input Sanitization · File Validation · Headers
// ============================================================

class Security
{
    // ── Output escaping (XSS prevention) ────────────────────
    public static function escape(mixed $val): string
    {
        return htmlspecialchars((string)$val, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    // ── Strip all HTML tags ──────────────────────────────────
    public static function strip(string $val): string
    {
        return strip_tags(trim($val));
    }

    // ── Sanitize integer ────────────────────────────────────
    public static function int(mixed $val, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX): int
    {
        return max($min, min($max, (int)$val));
    }

    // ── Sanitize float ──────────────────────────────────────
    public static function float(mixed $val, float $min = 0): float
    {
        return max($min, round((float)$val, 4));
    }

    // ── Sanitize email ──────────────────────────────────────
    public static function email(string $val): string
    {
        return filter_var(trim($val), FILTER_SANITIZE_EMAIL) ?: '';
    }

    // ── Sanitize URL ────────────────────────────────────────
    public static function url(string $val): string
    {
        return filter_var(trim($val), FILTER_SANITIZE_URL) ?: '';
    }

    // ── Alphanumeric only ────────────────────────────────────
    public static function alnum(string $val): string
    {
        return preg_replace('/[^a-zA-Z0-9_\-]/', '', $val);
    }

    // ── Validate date ────────────────────────────────────────
    public static function date(string $val, string $format = 'Y-m-d'): ?string
    {
        $d = DateTime::createFromFormat($format, $val);
        return ($d && $d->format($format) === $val) ? $val : null;
    }

    // ── Validate enum value ──────────────────────────────────
    public static function enum(string $val, array $allowed, string $default = ''): string
    {
        return in_array($val, $allowed, true) ? $val : $default;
    }

    // ── File upload validation ───────────────────────────────
    public static function validateUpload(array $file): array
    {
        $errors = [];

        if ($file['error'] !== UPLOAD_ERR_OK) {
            $codes = [
                UPLOAD_ERR_INI_SIZE   => 'File too large (server limit).',
                UPLOAD_ERR_FORM_SIZE  => 'File too large (form limit).',
                UPLOAD_ERR_PARTIAL    => 'File only partially uploaded.',
                UPLOAD_ERR_NO_FILE    => 'No file uploaded.',
                UPLOAD_ERR_NO_TMP_DIR => 'Missing temp folder.',
                UPLOAD_ERR_CANT_WRITE => 'Cannot write file.',
                UPLOAD_ERR_EXTENSION  => 'Upload blocked by extension.',
            ];
            $errors[] = $codes[$file['error']] ?? 'Upload error ' . $file['error'];
            return $errors;
        }

        if ($file['size'] > MAX_FILE_SIZE) {
            $errors[] = 'File size exceeds ' . round(MAX_FILE_SIZE / 1048576, 1) . ' MB limit.';
        }

        // MIME check using finfo (more reliable than extension)
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $mime  = $finfo->file($file['tmp_name']);
        if (!in_array($mime, ALLOWED_IMG_TYPES, true)) {
            $errors[] = 'File type not allowed. Accepted: JPG, PNG, WebP.';
        }

        // Extension check
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, ALLOWED_IMG_EXTS, true)) {
            $errors[] = 'File extension not allowed.';
        }

        // Double extension attack (e.g. file.php.jpg)
        $parts = explode('.', $file['name']);
        if (count($parts) > 2) {
            foreach (array_slice($parts, 1, -1) as $mid) {
                if (in_array(strtolower($mid), ['php','php3','php4','php5','phtml','asp','aspx'], true)) {
                    $errors[] = 'Dangerous file extension detected.';
                    break;
                }
            }
        }

        return $errors;
    }

    // ── Safe filename generation ─────────────────────────────
    public static function safeFilename(string $originalName): string
    {
        $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        return bin2hex(random_bytes(8)) . '_' . time() . '.' . $ext;
    }

    // ── Path traversal check ─────────────────────────────────
    public static function safePath(string $path, string $baseDir): bool
    {
        $real = realpath($path);
        $base = realpath($baseDir);
        return $real !== false && $base !== false && str_starts_with($real, $base);
    }

    // ── HTTP Security Headers ────────────────────────────────
    public static function sendHeaders(): void
    {
        // Prevent clickjacking
        header('X-Frame-Options: SAMEORIGIN');
        // Prevent MIME sniffing
        header('X-Content-Type-Options: nosniff');
        // XSS protection for older browsers
        header('X-XSS-Protection: 1; mode=block');
        // Referrer policy
        header('Referrer-Policy: strict-origin-when-cross-origin');
        // Permissions policy — restrict unnecessary APIs
        header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
        // Remove server/PHP version fingerprinting
        header_remove('X-Powered-By');
        header_remove('Server');
        // Content Security Policy
        $nonce = base64_encode(random_bytes(16));
        header("Content-Security-Policy: "
            . "default-src 'self'; "
            . "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdn.stimulsoft.com; "
            . "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdn.stimulsoft.com; "
            . "img-src 'self' data: blob: https:; "
            . "font-src 'self' data: https://cdn.jsdelivr.net; "
            . "connect-src 'self' https://cdn.jsdelivr.net https://*.jsdelivr.net https://cdn.stimulsoft.com; "
            . "worker-src 'self' blob:; "
            . "frame-ancestors 'self';"
        );
    }

    // ── Password strength check ──────────────────────────────
    public static function passwordStrength(string $password): array
    {
        $errors = [];
        if (strlen($password) < 6)   $errors[] = 'Minimum 6 characters.';
        if (strlen($password) > 128) $errors[] = 'Maximum 128 characters.';
        return $errors;
    }

    // ── Generate secure random token ────────────────────────
    public static function token(int $bytes = 32): string
    {
        return bin2hex(random_bytes($bytes));
    }

    // ── Rate limit (session-based) ───────────────────────────
    public static function rateLimit(string $key, int $limit, int $window): bool
    {
        $sKey = 'rl_' . md5($key);
        $now  = time();
        $data = $_SESSION[$sKey] ?? ['count' => 0, 'start' => $now];
        if ($now - $data['start'] > $window) {
            $data = ['count' => 0, 'start' => $now];
        }
        $data['count']++;
        $_SESSION[$sKey] = $data;
        return $data['count'] <= $limit;
    }
}

// ── Global helper functions ──────────────────────────────────

/** HTML-escape a value */
function e(mixed $v): string { return Security::escape($v); }

/** Sanitize to plain string */
function clean(string $v): string { return Security::strip($v); }