<?php
/**
 * Safe image-upload helpers.
 *
 * Validate that an upload really is an image, cap its size, and store it under
 * a random, non-guessable name so a caller can never smuggle an executable
 * filename (e.g. "shell.php") into a web-served directory.
 *
 * Both helpers return the stored path (string) on success or null on failure,
 * setting $error to a human-readable reason.
 */

if (!function_exists('save_uploaded_image')) {

    /** Allowed MIME type => canonical extension. */
    function upload_allowed_image_types(): array
    {
        return [
            'image/jpeg' => 'jpg',
            'image/png'  => 'png',
            'image/gif'  => 'gif',
            'image/webp' => 'webp',
        ];
    }

    /**
     * Validate and store a $_FILES entry.
     *
     * @param array       $file     One entry from $_FILES.
     * @param string      $destDir  Directory to store into (created if missing).
     * @param string|null $error    Populated with the failure reason on null return.
     * @param int         $maxBytes Maximum accepted size (default 5 MB).
     */
    function save_uploaded_image(array $file, string $destDir, ?string &$error = null, int $maxBytes = 5242880): ?string
    {
        $error = null;

        if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
            $error = 'Upload failed (error code ' . ($file['error'] ?? '?') . ').';
            return null;
        }
        if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
            $error = 'Invalid upload.';
            return null;
        }
        if (($file['size'] ?? 0) > $maxBytes) {
            $error = 'Image is too large (max ' . (int) ($maxBytes / 1048576) . ' MB).';
            return null;
        }

        $allowed = upload_allowed_image_types();
        $finfo   = new finfo(FILEINFO_MIME_TYPE);
        $mime    = (string) $finfo->file($file['tmp_name']);
        if (!isset($allowed[$mime]) || @getimagesize($file['tmp_name']) === false) {
            $error = 'Only JPG, PNG, GIF or WebP images are allowed.';
            return null;
        }

        return upload_store_bytes(file_get_contents($file['tmp_name']), $destDir, $allowed[$mime], $error);
    }

    /**
     * Validate and store a base64 data-URL image (e.g. a webcam capture).
     */
    function save_base64_image(string $dataUrl, string $destDir, ?string &$error = null, int $maxBytes = 5242880): ?string
    {
        $error = null;

        if (!preg_match('#^data:image/(jpeg|jpg|png|gif|webp);base64,#i', $dataUrl, $m)) {
            $error = 'Unsupported image data.';
            return null;
        }
        $ext = strtolower($m[1]) === 'jpeg' ? 'jpg' : strtolower($m[1]);
        $bin = base64_decode(substr($dataUrl, strpos($dataUrl, ',') + 1), true);
        if ($bin === false || $bin === '' || strlen($bin) > $maxBytes) {
            $error = 'Invalid image data.';
            return null;
        }
        if (@getimagesizefromstring($bin) === false) {
            $error = 'Invalid image data.';
            return null;
        }

        return upload_store_bytes($bin, $destDir, $ext, $error);
    }

    /** Write validated bytes to a random filename under $destDir. */
    function upload_store_bytes(string $bytes, string $destDir, string $ext, ?string &$error = null): ?string
    {
        if (!is_dir($destDir) && !@mkdir($destDir, 0755, true) && !is_dir($destDir)) {
            $error = 'Could not create the upload directory.';
            return null;
        }
        $name = bin2hex(random_bytes(16)) . '.' . $ext;
        $path = rtrim($destDir, '/\\') . '/' . $name;
        if (file_put_contents($path, $bytes) === false) {
            $error = 'Could not store the uploaded image.';
            return null;
        }
        @chmod($path, 0644);
        return $path;
    }
}
