<?php

declare(strict_types=1);

namespace App\Services\Images;

use App\Contracts\Images\SubjectDetectorInterface;
use App\Data\Images\CropBoxData;
use App\Data\Images\SquareThumbnailResultData;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\ImageManager;
use RuntimeException;

final class SquareThumbnailService
{
    private ?ImageManager $manager = null;

    public function __construct(private readonly SubjectDetectorInterface $subjectDetector)
    {
        try {
            $this->manager = extension_loaded('gd') ? ImageManager::gd() : ImageManager::imagick();
        } catch (\Throwable $e) {
            Log::warning('Square thumbnail image manager configuration failed', [
                'error' => $e->getMessage(),
            ]);
            $this->manager = null;
        }
    }

    /**
     * @param array<string, mixed> $options
     */
    public function generateFromPath(string $sourcePath, string $destinationPath, array $options = []): SquareThumbnailResultData
    {
        $this->assertImageManagerAvailable();

        if (! is_file($sourcePath) || ! is_readable($sourcePath)) {
            throw new RuntimeException('Square thumbnail source image is not readable.');
        }

        $size = @getimagesize($sourcePath);
        if (! is_array($size) || ($size[0] ?? 0) < 1 || ($size[1] ?? 0) < 1) {
            throw new RuntimeException('Square thumbnail source image dimensions are invalid.');
        }

        $sourceWidth = (int) $size[0];
        $sourceHeight = (int) $size[1];
        $config = $this->resolveOptions($options);
        $context = is_array($options['context'] ?? null) ? $options['context'] : $options;

        $detection = null;
        if ($config['smart_crop']) {
            $detection = $this->subjectDetector->detect($sourcePath, $sourceWidth, $sourceHeight, $context);
        }

        $cropBox = $this->calculateCropBox($sourceWidth, $sourceHeight, $detection?->cropBox, $config);
        $cropMode = $detection?->strategy ?? $config['fallback_strategy'];

        $image = $this->manager->read($sourcePath)->crop($cropBox->width, $cropBox->height, $cropBox->x, $cropBox->y);
        $outputWidth = $config['target_width'];
        $outputHeight = $config['target_height'];

        if ($config['allow_upscale']) {
            $image = $image->resize($config['target_width'], $config['target_height']);
        } else {
            $image = $image->resizeDown($config['target_width'], $config['target_height']);
            $outputWidth = min($config['target_width'], $cropBox->width);
            $outputHeight = min($config['target_height'], $cropBox->height);
        }

        $encoded = (string) $image->encode(new WebpEncoder($config['quality']));
        File::ensureDirectoryExists(dirname($destinationPath));
        File::put($destinationPath, $encoded);

        $result = new SquareThumbnailResultData(
            destinationPath: $destinationPath,
            cropBox: $cropBox,
            cropMode: $cropMode,
            sourceWidth: $sourceWidth,
            sourceHeight: $sourceHeight,
            targetWidth: $config['target_width'],
            targetHeight: $config['target_height'],
            outputWidth: $outputWidth,
            outputHeight: $outputHeight,
            detectionReason: $detection?->reason,
            meta: [
                'smart_crop' => $config['smart_crop'],
                'padding_ratio' => $config['padding_ratio'],
                'confidence' => $detection?->confidence,
            ],
        );

        if ($config['log']) {
            Log::debug('square-thumbnail-generated', $result->toArray());
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $options
     */
    public function calculateCropBox(int $sourceWidth, int $sourceHeight, ?CropBoxData $focusBox = null, array $options = []): CropBoxData
    {
        $config = $this->resolveOptions($options);

        if ($focusBox === null) {
            return $this->safeCenterCrop($sourceWidth, $sourceHeight);
        }

        $baseSide = max($focusBox->width, $focusBox->height);
        $side = max(1, (int) ceil($baseSide * (1 + ($config['padding_ratio'] * 2))));
        $side = min($side, min($sourceWidth, $sourceHeight));

        $x = (int) round($focusBox->centerX() - ($side / 2));
        $y = (int) round($focusBox->centerY() - ($side / 2));

        return (new CropBoxData($x, $y, $side, $side))->clampToImage($sourceWidth, $sourceHeight);
    }

    private function safeCenterCrop(int $sourceWidth, int $sourceHeight): CropBoxData
    {
        $side = min($sourceWidth, $sourceHeight);
        $x = (int) floor(($sourceWidth - $side) / 2);
        $y = (int) floor(($sourceHeight - $side) / 2);

        return new CropBoxData($x, $y, $side, $side);
    }

    /**
     * @param array<string, mixed> $options
     * @return array{target_width: int, target_height: int, quality: int, smart_crop: bool, padding_ratio: float, allow_upscale: bool, fallback_strategy: string, log: bool}
     */
    private function resolveOptions(array $options): array
    {
        $config = (array) config('uploads.square_thumbnails', []);
        $targetWidth = (int) ($options['target_width'] ?? $options['target_size'] ?? $config['width'] ?? config('uploads.derivatives.sq.size', 512));
        $targetHeight = (int) ($options['target_height'] ?? $options['target_size'] ?? $config['height'] ?? $targetWidth);

        return [
            'target_width' => max(1, $targetWidth),
            'target_height' => max(1, $targetHeight),
            'quality' => max(1, min(100, (int) ($options['quality'] ?? $config['quality'] ?? 82))),
            'smart_crop' => (bool) ($options['smart_crop'] ?? $config['smart_crop'] ?? true),
            'padding_ratio' => max(0.0, min(0.5, (float) ($options['padding_ratio'] ?? $config['padding_ratio'] ?? 0.18))),
            'allow_upscale' => (bool) ($options['allow_upscale'] ?? $config['allow_upscale'] ?? false),
            'fallback_strategy' => (string) ($options['fallback_strategy'] ?? $config['fallback_strategy'] ?? 'center'),
            'log' => (bool) ($options['log'] ?? $config['log'] ?? false),
        ];
    }

    private function assertImageManagerAvailable(): void
    {
        if ($this->manager === null) {
            throw new RuntimeException('Square thumbnail generation requires Intervention Image.');
        }
    }
}