<?php

declare(strict_types=1);

namespace App\Services\Images;

use App\Models\Artwork;
use App\Services\Cdn\ArtworkCdnPurgeService;
use App\Services\ArtworkOriginalFileLocator;
use App\Services\Uploads\UploadStorageService;
use App\Support\ArtworkFeaturedImagePath;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\ImageManager;
use RuntimeException;

final class FeaturedArtworkThumbnailGenerator
{
    private const ALLOWED_SOURCE_EXTENSIONS = ['avif', 'bmp', 'gif', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'webp'];

    private ?ImageManager $manager = null;

    public function __construct(
        private readonly ArtworkFeaturedImagePath $paths,
        private readonly ArtworkOriginalFileLocator $locator,
        private readonly UploadStorageService $storage,
        private readonly ArtworkCdnPurgeService $cdnPurge,
    ) {
        try {
            $this->manager = extension_loaded('gd')
                ? new ImageManager(new GdDriver)
                : new ImageManager(new ImagickDriver);
        } catch (\Throwable) {
            $this->manager = null;
        }
    }

    /**
     * @return array{existing:list<string>,missing:list<string>,target_variants:list<string>}
     */
    public function plan(Artwork $artwork, bool $force = false): array
    {
        $disk = Storage::disk($this->storage->objectDiskName());
        $existing = [];
        $missing = [];

        foreach ($this->paths->variantNames() as $variant) {
            $objectPath = $this->paths->objectPath($artwork, $variant);

            if ($disk->exists($objectPath)) {
                $existing[] = $variant;

                continue;
            }

            $missing[] = $variant;
        }

        return [
            'existing' => $existing,
            'missing' => $missing,
            'target_variants' => $force ? $this->paths->variantNames() : $missing,
        ];
    }

    /**
     * @return array{existing:list<string>,missing:list<string>,target_variants:list<string>,generated:int,skipped:int,generated_variants:list<string>,generated_paths:list<string>,failed:array<string,string>}
     */
    public function generate(Artwork $artwork, bool $force = false): array
    {
        $this->assertImageManager();

        $plan = $this->plan($artwork, $force);
        $targetVariants = $plan['target_variants'];

        if ($targetVariants === []) {
            return $plan + [
                'generated' => 0,
                'skipped' => count($plan['existing']),
                'generated_variants' => [],
                'generated_paths' => [],
                'failed' => [],
            ];
        }

        ['path' => $sourcePath, 'temporary' => $temporaryPath] = $this->resolveSourcePath($artwork);

        $generatedVariants = [];
        $generatedPaths = [];
        $failed = [];

        try {
            foreach ($targetVariants as $variant) {
                $config = $this->paths->variantConfig($variant);

                if ($config === null) {
                    $failed[$variant] = 'Unknown featured thumbnail variant.';

                    continue;
                }

                try {
                    $image = $this->manager->read($sourcePath)->cover((int) $config['width'], (int) $config['height']);
                    $encoded = (string) $image->encode(new WebpEncoder((int) $config['quality']));
                    $objectPath = $this->paths->objectPath($artwork, $variant);

                    $this->storage->putObjectContents($objectPath, $encoded, 'image/webp');

                    $generatedVariants[] = $variant;
                    $generatedPaths[] = $objectPath;
                } catch (\Throwable $exception) {
                    $failed[$variant] = $exception->getMessage();
                }
            }
        } finally {
            if ($temporaryPath !== null && $temporaryPath !== '') {
                File::delete($temporaryPath);
            }
        }

        if ($generatedPaths !== []) {
            $this->cdnPurge->purgeArtworkObjectPaths($generatedPaths, [
                'artwork_id' => $artwork->id,
                'reason' => 'featured_artwork_thumbnails_generated',
                'force' => $force,
            ]);
        }

        return $plan + [
            'generated' => count($generatedVariants),
            'skipped' => max(0, count($targetVariants) - count($generatedVariants) - count($failed)) + count($plan['existing']),
            'generated_variants' => $generatedVariants,
            'generated_paths' => $generatedPaths,
            'failed' => $failed,
        ];
    }

    private function assertImageManager(): void
    {
        if ($this->manager !== null) {
            return;
        }

        throw new RuntimeException('Image processing is not available on this environment.');
    }

    /**
     * @return array{path:string,temporary:?string}
     */
    private function resolveSourcePath(Artwork $artwork): array
    {
        $localPath = $this->locator->resolveLocalPath($artwork);

        if ($this->isUsableSourceFile($localPath)) {
            return ['path' => $localPath, 'temporary' => null];
        }

        $objectPath = $this->locator->resolveObjectPath($artwork);
        $extension = strtolower((string) pathinfo($objectPath, PATHINFO_EXTENSION));

        if (! in_array($extension, self::ALLOWED_SOURCE_EXTENSIONS, true)) {
            throw new RuntimeException('Original artwork source is not a supported image file.');
        }

        $contents = $this->storage->readObject($objectPath);

        if (! is_string($contents) || $contents === '' || ! $this->isImageContents($contents)) {
            throw new RuntimeException('Original artwork source is missing or is not a valid image.');
        }

        $temporaryPath = tempnam(sys_get_temp_dir(), 'featured-artwork-');
        if ($temporaryPath === false) {
            throw new RuntimeException('Unable to allocate a temporary source file for featured thumbnail generation.');
        }

        $targetPath = $temporaryPath.($extension !== '' ? '.'.$extension : '');
        if (! @rename($temporaryPath, $targetPath)) {
            File::delete($temporaryPath);
            throw new RuntimeException('Unable to prepare a temporary source file for featured thumbnail generation.');
        }

        if (file_put_contents($targetPath, $contents) === false) {
            File::delete($targetPath);
            throw new RuntimeException('Unable to write the temporary source file for featured thumbnail generation.');
        }

        return ['path' => $targetPath, 'temporary' => $targetPath];
    }

    private function isUsableSourceFile(string $path): bool
    {
        if ($path === '' || ! is_file($path) || ! is_readable($path)) {
            return false;
        }

        $extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
        if (! in_array($extension, self::ALLOWED_SOURCE_EXTENSIONS, true)) {
            return false;
        }

        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $mime = strtolower((string) $finfo->file($path));

        return str_starts_with($mime, 'image/');
    }

    private function isImageContents(string $contents): bool
    {
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $mime = strtolower((string) $finfo->buffer($contents));

        return str_starts_with($mime, 'image/');
    }
}
