<?php

declare(strict_types=1);

namespace App\Services\Uploads;

use App\Services\Images\SquareThumbnailService;
use Illuminate\Support\Facades\File;
use Intervention\Image\ImageManager as ImageManager;
use Intervention\Image\Interfaces\ImageInterface as InterventionImageInterface;
use RuntimeException;

final class UploadDerivativesService
{
    private bool $imageAvailable = false;
    private ?ImageManager $manager = null;

    public function __construct(
        private readonly UploadStorageService $storage,
        private readonly SquareThumbnailService $squareThumbnails,
    )
    {
        // Intervention Image v3 uses ImageManager; instantiate appropriate driver
        try {
            $this->manager = extension_loaded('gd') ? ImageManager::gd() : ImageManager::imagick();
            $this->imageAvailable = true;
        } catch (\Throwable $e) {
            logger()->warning('Intervention Image present but configuration failed: ' . $e->getMessage());
            $this->imageAvailable = false;
            $this->manager = null;
        }
    }

    /**
     * @return array{local_path: string, object_path: string, filename: string, mime: string, size: int, ext: string}
     */
    public function storeOriginal(string $sourcePath, string $hash, ?string $originalFileName = null): array
    {
        $origExt = $this->resolveOriginalExtension($sourcePath, $originalFileName);
        $filename = $hash . ($origExt !== '' ? '.' . $origExt : '');
        $target = $this->storage->localOriginalPath($hash, $filename);

        if (! File::copy($sourcePath, $target)) {
            throw new RuntimeException('Unable to copy original file into local artwork originals storage.');
        }

        $mime = (string) (File::mimeType($target) ?: 'application/octet-stream');
        $size = (int) filesize($target);
        $objectPath = $this->storage->objectPathForVariant('original', $hash, $filename);
        $this->storage->putObjectFromPath($target, $objectPath, $mime);

        return [
            'local_path' => $target,
            'object_path' => $objectPath,
            'filename' => $filename,
            'mime' => $mime,
            'size' => $size,
            'ext' => $origExt,
        ];
    }

    private function resolveOriginalExtension(string $sourcePath, ?string $originalFileName): string
    {
        $fromClientName = strtolower((string) pathinfo((string) $originalFileName, PATHINFO_EXTENSION));
        if ($fromClientName !== '' && preg_match('/^[a-z0-9]{1,12}$/', $fromClientName) === 1) {
            return $fromClientName;
        }

        $fromSource = strtolower((string) pathinfo($sourcePath, PATHINFO_EXTENSION));
        if ($fromSource !== '' && $fromSource !== 'upload' && preg_match('/^[a-z0-9]{1,12}$/', $fromSource) === 1) {
            return $fromSource;
        }

        $mime = File::exists($sourcePath) ? (string) (File::mimeType($sourcePath) ?? '') : '';
        return $this->extensionFromMime($mime);
    }

    private function extensionFromMime(string $mime): string
    {
        return match (strtolower($mime)) {
            'image/jpeg', 'image/jpg' => 'jpg',
            'image/png' => 'png',
            'image/webp' => 'webp',
            'image/gif' => 'gif',
            'image/bmp' => 'bmp',
            'image/tiff' => 'tif',
            'application/zip', 'application/x-zip-compressed' => 'zip',
            'application/x-rar-compressed', 'application/vnd.rar' => 'rar',
            'application/x-7z-compressed' => '7z',
            'application/x-tar' => 'tar',
            'application/gzip', 'application/x-gzip' => 'gz',
            default => 'bin',
        };
    }

    /**
     * @return array<string, array{path: string, size: int, mime: string}>
     */
    public function generatePublicDerivatives(string $sourcePath, string $hash): array
    {
        $this->assertImageAvailable();
        $quality = (int) config('uploads.quality', 85);
        $variants = (array) config('uploads.derivatives', []);
        $written = [];

        foreach ($variants as $variant => $options) {
            $variant = (string) $variant;

            if ($variant === 'sq') {
                $written[$variant] = $this->generateSquareDerivative($sourcePath, $hash);
                continue;
            }

            $filename = $hash . '.webp';
            $objectPath = $this->storage->objectPathForVariant($variant, $hash, $filename);

            /** @var InterventionImageInterface $img */
            $img = $this->manager->read($sourcePath);

            if (isset($options['size'])) {
                $size = (int) $options['size'];
                $out = $img->cover($size, $size);
            } else {
                $max = (int) ($options['max'] ?? 0);
                if ($max <= 0) {
                    $max = 2560;
                }

                $out = $img->scaleDown($max, $max);
            }

            $encoder = new \Intervention\Image\Encoders\WebpEncoder($quality);
            $encoded = (string) $out->encode($encoder);
            $this->storage->putObjectContents($objectPath, $encoded, 'image/webp');
            $written[$variant] = [
                'path' => $objectPath,
                'size' => strlen($encoded),
                'mime' => 'image/webp',
            ];
        }

        return $written;
    }

    /**
     * @param array<string, mixed> $options
     * @return array{path: string, size: int, mime: string, result: SquareThumbnailResultData}
     */
    public function generateSquareDerivative(string $sourcePath, string $hash, array $options = []): array
    {
        $filename = $hash . '.webp';
        $objectPath = $this->storage->objectPathForVariant('sq', $hash, $filename);
        $temporaryPath = tempnam(sys_get_temp_dir(), 'sq-derivative-');

        if ($temporaryPath === false) {
            throw new RuntimeException('Unable to allocate a temporary file for square derivative generation.');
        }

        $temporaryWebp = $temporaryPath . '.webp';
        rename($temporaryPath, $temporaryWebp);

        try {
            $result = $this->squareThumbnails->generateFromPath($sourcePath, $temporaryWebp, $options);
            $mime = 'image/webp';
            $size = (int) filesize($temporaryWebp);
            $this->storage->putObjectFromPath($temporaryWebp, $objectPath, $mime);

            return [
                'path' => $objectPath,
                'size' => $size,
                'mime' => $mime,
                'result' => $result,
            ];
        } finally {
            File::delete($temporaryWebp);
        }
    }

    private function assertImageAvailable(): void
    {
        if (! $this->imageAvailable) {
            throw new RuntimeException('Intervention Image is not available.');
        }
    }
}
