<?php

declare(strict_types=1);

namespace Grav\Plugin\Api\Services;

class ThumbnailService
{
    private string $cacheDir;
    private int $maxSize;
    private int $quality;

    public function __construct(string $cacheDir, int $maxSize = 500, int $quality = 85)
    {
        $this->cacheDir = rtrim($cacheDir, '/');
        $this->maxSize = $maxSize;
        $this->quality = $quality;

        if (!is_dir($this->cacheDir) && !mkdir($this->cacheDir, 0755, true) && !is_dir($this->cacheDir)) {
            throw new \RuntimeException(sprintf('Unable to create thumbnail cache directory "%s"', $this->cacheDir));
        }
    }

    /**
     * Get the hash for a thumbnail based on source path and modification time.
     */
    public function getHash(string $sourcePath): string
    {
        $mtime = file_exists($sourcePath) ? filemtime($sourcePath) : 0;
        return md5($sourcePath . '|' . $mtime . '|' . $this->maxSize);
    }

    /**
     * Get the thumbnail filename (hash.ext) for a source image.
     * Returns null if not a supported image.
     */
    public function getThumbnailFilename(string $sourcePath): ?string
    {
        if (!file_exists($sourcePath)) {
            return null;
        }

        $mime = mime_content_type($sourcePath);
        if (!$mime || !str_starts_with($mime, 'image/') || $mime === 'image/svg+xml') {
            return null;
        }

        return $this->getHash($sourcePath) . '.' . $this->getOutputExtension($mime);
    }

    /**
     * Resolve the cached thumbnail filename (hash.ext) for a source image,
     * generating the thumbnail only when the cache entry is missing.
     *
     * This is the single-pass path for listings: calling getThumbnailFilename()
     * followed by getThumbnail() sniffs the file's magic bytes and stats it
     * twice per item, which dominates the warm-path cost of serializing a
     * media-heavy page. Callers that already know the mime type (media
     * metadata) should pass it so the sniff is skipped entirely.
     *
     * Returns null if the source is not a supported raster image.
     */
    public function ensureThumbnail(string $sourcePath, ?string $mime = null): ?string
    {
        if (!file_exists($sourcePath)) {
            return null;
        }

        $sniffed = $mime === null;
        if ($sniffed) {
            $mime = mime_content_type($sourcePath) ?: null;
        }
        if (!$mime || !str_starts_with($mime, 'image/') || $mime === 'image/svg+xml') {
            return null;
        }

        $filename = $this->getHash($sourcePath) . '.' . $this->getOutputExtension($mime);
        $cachePath = $this->cacheDir . '/' . $filename;

        if (file_exists($cachePath) || $this->generate($sourcePath, $cachePath, $mime)) {
            return $filename;
        }

        // A caller-supplied mime can lie (misnamed extension, stale metadata);
        // the sniffed type is authoritative, so retry once with it.
        return $sniffed ? null : $this->ensureThumbnail($sourcePath);
    }

    /**
     * Get the cached thumbnail path, generating it if needed.
     * Returns null if the source is not a supported image.
     */
    public function getThumbnail(string $sourcePath): ?string
    {
        if (!file_exists($sourcePath)) {
            return null;
        }

        $mime = mime_content_type($sourcePath);
        if (!$mime || !str_starts_with($mime, 'image/')) {
            return null;
        }

        // Skip SVGs — serve as-is
        if ($mime === 'image/svg+xml') {
            return null;
        }

        $hash = $this->getHash($sourcePath);
        $ext = $this->getOutputExtension($mime);
        $cachePath = $this->cacheDir . '/' . $hash . '.' . $ext;

        if (file_exists($cachePath)) {
            return $cachePath;
        }

        return $this->generate($sourcePath, $cachePath, $mime);
    }

    /**
     * Generate a thumbnail and save to cache.
     */
    private function generate(string $sourcePath, string $cachePath, string $mime): ?string
    {
        $sourceImage = $this->loadImage($sourcePath, $mime);
        if (!$sourceImage) {
            return null;
        }

        $origWidth = imagesx($sourceImage);
        $origHeight = imagesy($sourceImage);

        // Already small enough — cache as-is so we don't re-check every time
        if ($origWidth <= $this->maxSize && $origHeight <= $this->maxSize) {
            return $this->saveImage($sourceImage, $cachePath, $mime, $origWidth, $origHeight);
        }

        // Calculate new dimensions maintaining aspect ratio
        if ($origWidth >= $origHeight) {
            $newWidth = $this->maxSize;
            $newHeight = (int) round($origHeight * ($this->maxSize / $origWidth));
        } else {
            $newHeight = $this->maxSize;
            $newWidth = (int) round($origWidth * ($this->maxSize / $origHeight));
        }

        $thumb = imagecreatetruecolor($newWidth, $newHeight);
        if (!$thumb) {
            imagedestroy($sourceImage);
            return null;
        }

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

        imagecopyresampled($thumb, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $origWidth, $origHeight);
        imagedestroy($sourceImage);

        return $this->saveImage($thumb, $cachePath, $mime, $newWidth, $newHeight);
    }

    /**
     * Load an image resource from file.
     */
    private function loadImage(string $path, string $mime): ?\GdImage
    {
        return match ($mime) {
            'image/jpeg' => @imagecreatefromjpeg($path) ?: null,
            'image/png' => @imagecreatefrompng($path) ?: null,
            'image/gif' => @imagecreatefromgif($path) ?: null,
            'image/webp' => @imagecreatefromwebp($path) ?: null,
            'image/avif' => function_exists('imagecreatefromavif') ? (@imagecreatefromavif($path) ?: null) : null,
            default => null,
        };
    }

    /**
     * Save an image resource to the cache path.
     */
    private function saveImage(\GdImage $image, string $cachePath, string $mime, int $width, int $height): ?string
    {
        $result = match ($mime) {
            'image/png' => imagepng($image, $cachePath, 6),
            'image/gif' => imagegif($image, $cachePath),
            'image/webp' => imagewebp($image, $cachePath, $this->quality),
            'image/avif' => function_exists('imageavif') ? imageavif($image, $cachePath, $this->quality) : false,
            default => imagejpeg($image, $cachePath, $this->quality),
        };

        imagedestroy($image);

        return $result ? $cachePath : null;
    }

    /**
     * Get the output file extension for a MIME type.
     */
    private function getOutputExtension(string $mime): string
    {
        return match ($mime) {
            'image/png' => 'png',
            'image/gif' => 'gif',
            'image/webp' => 'webp',
            'image/avif' => 'avif',
            default => 'jpg',
        };
    }
}
