<?php

declare(strict_types=1);

namespace App\Services\News;

use App\Support\News\NewsCoverImage;
use Illuminate\Http\UploadedFile;
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 NewsCoverImageService
{
    private const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'];

    private const MAX_FILE_SIZE_KB = 6144;

    private const MAX_WIDTH = 2200;

    private const MAX_HEIGHT = 1400;

    private const MIN_WIDTH = 1200;

    private const MIN_HEIGHT = 630;

    private ?ImageManager $manager = null;

    public function __construct()
    {
        try {
            $this->manager = extension_loaded('gd')
                ? new ImageManager(new GdDriver())
                : new ImageManager(new ImagickDriver());
        } catch (\Throwable) {
            $this->manager = null;
        }
    }

    public function maxFileSizeKb(): int
    {
        return self::MAX_FILE_SIZE_KB;
    }

    public function storeUploadedFile(UploadedFile $file): array
    {
        $this->assertImageManager();
        $this->assertStorageIsAllowed();

        $raw = $this->readUploadBytes($file);
        $this->assertSupportedMimeType($raw);
        $this->assertMinimumDimensions($raw);

        $masterImage = $this->manager->read($raw)->scaleDown(width: self::MAX_WIDTH, height: self::MAX_HEIGHT);
        $masterEncoded = (string) $masterImage->encode(new WebpEncoder(85));

        $path = NewsCoverImage::path(hash('sha256', $masterEncoded));

        $this->writeImage($path, $masterEncoded);

        foreach (NewsCoverImage::VARIANTS as $variant => $config) {
            $variantImage = $this->manager->read($raw)->scaleDown(width: (int) $config['width']);
            $variantEncoded = (string) $variantImage->encode(new WebpEncoder((int) $config['quality']));
            $this->writeImage(NewsCoverImage::variantPath($path, $variant), $variantEncoded);
        }

        return [
            'path' => $path,
            'url' => NewsCoverImage::url($path),
            'width' => (int) $masterImage->width(),
            'height' => (int) $masterImage->height(),
            'size_bytes' => strlen($masterEncoded),
            'mobile_url' => NewsCoverImage::variantUrl($path, 'mobile'),
            'desktop_url' => NewsCoverImage::variantUrl($path, 'desktop'),
            'large_url' => NewsCoverImage::variantUrl($path, 'large'),
            'srcset' => NewsCoverImage::srcset($path),
        ];
    }

    public function ensureVariants(string $path, bool $force = false): array
    {
        $trimmed = NewsCoverImage::normalizePath($path);

        if (! NewsCoverImage::isManagedPath($trimmed)) {
            return ['generated' => 0, 'skipped' => count(NewsCoverImage::VARIANTS)];
        }

        $this->assertImageManager();
        $this->assertStorageIsAllowed();

        $disk = Storage::disk($this->mediaDiskName());
        if (! $disk->exists($trimmed)) {
            throw new RuntimeException('Managed cover image is missing from object storage.');
        }

        $raw = $disk->get($trimmed);
        if (! is_string($raw) || $raw === '') {
            throw new RuntimeException('Unable to read managed cover image from object storage.');
        }

        $generated = 0;
        $skipped = 0;

        foreach (NewsCoverImage::VARIANTS as $variant => $config) {
            $variantPath = NewsCoverImage::variantPath($trimmed, $variant);

            if (! $force && $disk->exists($variantPath)) {
                $skipped++;
                continue;
            }

            $variantImage = $this->manager->read($raw)->scaleDown(width: (int) $config['width']);
            $variantEncoded = (string) $variantImage->encode(new WebpEncoder((int) $config['quality']));
            $this->writeImage($variantPath, $variantEncoded);
            $generated++;
        }

        return ['generated' => $generated, 'skipped' => $skipped];
    }

    public function deleteManagedFiles(string $path): void
    {
        $paths = NewsCoverImage::managedPaths($path);

        if ($paths === []) {
            return;
        }

        Storage::disk($this->mediaDiskName())->delete($paths);
    }

    private function mediaDiskName(): string
    {
        return (string) config('uploads.object_storage.disk', 's3');
    }

    private function writeImage(string $path, string $encoded): void
    {
        $written = Storage::disk($this->mediaDiskName())->put($path, $encoded, [
            'visibility' => 'public',
            'CacheControl' => 'public, max-age=31536000, immutable',
            'ContentType' => 'image/webp',
        ]);

        if ($written !== true) {
            throw new RuntimeException('Unable to store image in object storage.');
        }
    }

    private function readUploadBytes(UploadedFile $file): string
    {
        $uploadPath = (string) ($file->getRealPath() ?: $file->getPathname());

        if ($uploadPath === '' || ! is_readable($uploadPath)) {
            throw new RuntimeException('Unable to resolve uploaded image path.');
        }

        $raw = file_get_contents($uploadPath);
        if ($raw === false || $raw === '') {
            throw new RuntimeException('Unable to read uploaded image.');
        }

        return $raw;
    }

    private function assertSupportedMimeType(string $raw): void
    {
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $mime = strtolower((string) $finfo->buffer($raw));

        if (! in_array($mime, self::ALLOWED_MIME_TYPES, true)) {
            throw new RuntimeException('Unsupported image mime type.');
        }
    }

    private function assertMinimumDimensions(string $raw): void
    {
        $size = @getimagesizefromstring($raw);
        if (! is_array($size) || ($size[0] ?? 0) < 1 || ($size[1] ?? 0) < 1) {
            throw new RuntimeException('Uploaded file is not a valid image.');
        }

        $width = (int) ($size[0] ?? 0);
        $height = (int) ($size[1] ?? 0);

        if ($width < self::MIN_WIDTH || $height < self::MIN_HEIGHT) {
            throw new RuntimeException(sprintf(
                'Image is too small. Minimum required size is %dx%d.',
                self::MIN_WIDTH,
                self::MIN_HEIGHT,
            ));
        }
    }

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

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

    private function assertStorageIsAllowed(): void
    {
        if (! app()->environment('production')) {
            return;
        }

        $diskName = $this->mediaDiskName();
        if (in_array($diskName, ['local', 'public'], true)) {
            throw new RuntimeException('Production news media storage must use object storage, not local/public disks.');
        }
    }
}