<?php
/**
 * Upload - File upload helper library
 * Validates, stores and auto-resizes uploaded image files.
 * Uses the native GD extension for image resizing.
 */

class Upload
{
    // Allowed MIME types → canonical extension.
    // NB : SVG volontairement exclu — un SVG peut embarquer du <script> et,
    // servi statiquement depuis l'origine de l'app, provoquer un XSS stocké.
    private const ALLOWED_LOGO = [
        'image/png'     => 'png',
        'image/jpeg'    => 'jpg',
        'image/gif'     => 'gif',
        'image/webp'    => 'webp',
    ];

    private const ALLOWED_FAVICON = [
        'image/png'                  => 'png',
        'image/x-icon'               => 'ico',
        'image/vnd.microsoft.icon'   => 'ico',
    ];

    private const MAX_SIZE_LOGO    = 2 * 1024 * 1024; // 2 MB
    private const MAX_SIZE_FAVICON = 512 * 1024;       // 512 KB

    // Maximum pixel dimensions after auto-resize
    private const LOGO_MAX_W    = 128;
    private const LOGO_MAX_H    = 128;
    private const FAVICON_MAX_W = 64;
    private const FAVICON_MAX_H = 64;

    private Logger $log;

    public function __construct()
    {
        $this->log = Logger::getInstance();
    }

    /**
     * Handle an uploaded logo file.
     * Returns the web-relative path on success, null on no-upload/failure.
     * Populates $error on failure.
     */
    public function handleLogo(array $fileInput, ?string &$error): ?string
    {
        return $this->handle(
            $fileInput, 'logo',
            self::ALLOWED_LOGO, self::MAX_SIZE_LOGO,
            self::LOGO_MAX_W, self::LOGO_MAX_H,
            $error
        );
    }

    /**
     * Handle an uploaded favicon file.
     */
    public function handleFavicon(array $fileInput, ?string &$error): ?string
    {
        return $this->handle(
            $fileInput, 'favicon',
            self::ALLOWED_FAVICON, self::MAX_SIZE_FAVICON,
            self::FAVICON_MAX_W, self::FAVICON_MAX_H,
            $error
        );
    }

    // -------------------------------------------------------------------------
    // Internals
    // -------------------------------------------------------------------------

    private function handle(
        array   $fileInput,
        string  $prefix,
        array   $allowed,
        int     $maxBytes,
        int     $maxW,
        int     $maxH,
        ?string &$error
    ): ?string {
        if (empty($fileInput['name']) || $fileInput['error'] === UPLOAD_ERR_NO_FILE) {
            return null;
        }

        if ($fileInput['error'] !== UPLOAD_ERR_OK) {
            $error = 'Upload error code ' . $fileInput['error'];
            return null;
        }

        if ($fileInput['size'] > $maxBytes) {
            $error = 'File too large (max ' . round($maxBytes / 1024) . ' KB)';
            return null;
        }

        // Detect real MIME type from file content (not client-supplied header)
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $mime  = $finfo->file($fileInput['tmp_name']);

        if (!isset($allowed[$mime])) {
            $error = 'File type not allowed (' . $mime . ')';
            $this->log->warning('Upload rejected', ['mime' => $mime, 'prefix' => $prefix]);
            return null;
        }

        $ext     = $allowed[$mime];
        $destDir = WEBROOT_PATH . '/assets/img/uploads';

        if (!is_dir($destDir)) {
            mkdir($destDir, 0755, true);
        }

        // Remove previous file(s) of the same prefix
        foreach (glob($destDir . '/' . $prefix . '_*') ?: [] as $old) {
            @unlink($old);
        }

        $filename = $prefix . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
        $destPath = $destDir . '/' . $filename;

        if (!move_uploaded_file($fileInput['tmp_name'], $destPath)) {
            $error = 'Could not save uploaded file';
            return null;
        }

        // Auto-resize if needed (skipped for SVG/ICO — vector or opaque format)
        $resized = $this->resizeIfNeeded($destPath, $mime, $maxW, $maxH);
        if ($resized) {
            $this->log->info('Image resized', ['prefix' => $prefix, 'file' => $filename]);
        }

        $this->log->info('File uploaded', ['prefix' => $prefix, 'file' => $filename]);
        return '/assets/img/uploads/' . $filename;
    }

    /**
     * Resize $path in-place if either dimension exceeds $maxW/$maxH.
     * Keeps the original aspect ratio (fits inside the bounding box).
     * Returns true if the image was actually resized.
     */
    private function resizeIfNeeded(string $path, string $mime, int $maxW, int $maxH): bool
    {
        // SVG scales natively; ICO is a container format — skip both
        if (in_array($mime, ['image/svg+xml', 'image/x-icon', 'image/vnd.microsoft.icon'], true)) {
            return false;
        }

        if (!function_exists('imagecreatefromjpeg')) {
            $this->log->warning('GD extension not available — image not resized');
            return false;
        }

        $info = @getimagesize($path);
        if ($info === false) {
            return false;
        }

        [$origW, $origH] = $info;

        // Nothing to do
        if ($origW <= $maxW && $origH <= $maxH) {
            return false;
        }

        // Scale factor that fits both dimensions inside the bounding box
        $ratio = min($maxW / $origW, $maxH / $origH);
        $newW  = max(1, (int) round($origW * $ratio));
        $newH  = max(1, (int) round($origH * $ratio));

        $src = match ($mime) {
            'image/jpeg' => @imagecreatefromjpeg($path),
            'image/png'  => @imagecreatefrompng($path),
            'image/gif'  => @imagecreatefromgif($path),
            'image/webp' => @imagecreatefromwebp($path),
            default      => null,
        };

        if (!$src) {
            $this->log->warning('GD could not load image for resize', ['path' => $path]);
            return false;
        }

        $dst = imagecreatetruecolor($newW, $newH);

        // Preserve transparency for PNG and WebP
        if (in_array($mime, ['image/png', 'image/webp'], true)) {
            imagealphablending($dst, false);
            imagesavealpha($dst, true);
            $transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127);
            imagefill($dst, 0, 0, $transparent);
        }

        imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $origW, $origH);

        $ok = match ($mime) {
            'image/jpeg' => imagejpeg($dst, $path, 90),
            'image/png'  => imagepng($dst, $path, 9),
            'image/gif'  => imagegif($dst, $path),
            'image/webp' => imagewebp($dst, $path, 85),
            default      => false,
        };

        imagedestroy($src);
        imagedestroy($dst);

        return (bool) $ok;
    }
}
