<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\AcademyPromptTemplate;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
use Throwable;

final class GenerateAcademyPromptThumbnailsCommand extends Command
{
    private const PROMPT_PREVIEW_PREFIX = 'academy-prompts/previews';

    /**
     * @var array<string, int>
     */
    private const VARIANT_WIDTHS = [
        'thumb' => 480,
        'md' => 960,
    ];

    private const PREVIEW_WEBP_QUALITY = 84;

    private const LESSON_MEDIA_WEBP_QUALITY = 85;

    protected $signature = 'academy:prompts:generate-missing-thumbnails
        {--id=* : Restrict to one or more prompt IDs}
        {--slug=* : Restrict to one or more prompt slugs}
        {--limit= : Stop after processing this many prompts}
        {--force : Regenerate variants even when they already exist}
        {--dry-run : Report planned thumbnail work without writing files or saving prompt JSON}';

    protected $description = 'Generate missing prompt preview and comparison thumbnails for existing Academy prompts';

    public function handle(): int
    {
        if (! function_exists('imagecreatefromstring') || ! function_exists('imagewebp')) {
            $this->error('GD WebP support is required to generate prompt thumbnails.');

            return self::FAILURE;
        }

        $ids = collect((array) $this->option('id'))
            ->map(static fn (mixed $id): int => (int) $id)
            ->filter(static fn (int $id): bool => $id > 0)
            ->values()
            ->all();

        $slugs = collect((array) $this->option('slug'))
            ->map(static fn (mixed $slug): string => trim((string) $slug))
            ->filter(static fn (string $slug): bool => $slug !== '')
            ->values()
            ->all();

        $limit = $this->option('limit') !== null ? max(1, (int) $this->option('limit')) : null;
        $force = (bool) $this->option('force');
        $dryRun = (bool) $this->option('dry-run');

        $query = AcademyPromptTemplate::query()
            ->select(['id', 'slug', 'title', 'preview_image', 'tool_notes'])
            ->orderBy('id');

        if ($ids !== []) {
            $query->whereIn('id', $ids);
        }

        if ($slugs !== []) {
            $query->whereIn('slug', $slugs);
        }

        $processed = 0;
        $changed = 0;
        $generatedVariants = 0;
        $plannedVariants = 0;
        $skipped = 0;
        $failed = 0;

        $query->chunkById(100, function ($prompts) use ($limit, $force, $dryRun, &$processed, &$changed, &$generatedVariants, &$plannedVariants, &$skipped, &$failed) {
            foreach ($prompts as $prompt) {
                if ($limit !== null && $processed >= $limit) {
                    return false;
                }

                try {
                    $result = $this->backfillPrompt($prompt, $force, $dryRun);

                    $generatedVariants += (int) ($result['generated_variants'] ?? 0);
                    $plannedVariants += (int) ($result['planned_variants'] ?? 0);

                    if (($result['changed'] ?? false) === true) {
                        $changed++;
                    } else {
                        $skipped++;
                    }
                } catch (Throwable $e) {
                    $failed++;
                    $this->warn(sprintf('Prompt %d (%s) failed: %s', (int) $prompt->id, (string) $prompt->slug, $e->getMessage()));
                }

                $processed++;
            }

            return true;
        });

        $this->info(sprintf(
            'Prompt thumbnail backfill complete. processed=%d changed=%d generated_variants=%d planned_variants=%d skipped=%d failed=%d',
            $processed,
            $changed,
            $generatedVariants,
            $plannedVariants,
            $skipped,
            $failed,
        ));

        return $failed > 0 ? self::FAILURE : self::SUCCESS;
    }

    /**
     * @return array{changed:bool,generated_variants:int,planned_variants:int}
     */
    private function backfillPrompt(AcademyPromptTemplate $prompt, bool $force, bool $dryRun): array
    {
        $generatedVariants = 0;
        $plannedVariants = 0;
        $changed = false;

        $previewResult = $this->ensureManagedImageVariants((string) ($prompt->preview_image ?? ''), $force, $dryRun);
        $generatedVariants += $previewResult['generated_variants'];
        $plannedVariants += $previewResult['planned_variants'];
        $changed = $changed || $previewResult['changed'];

        $notes = is_array($prompt->tool_notes) ? $prompt->tool_notes : [];
        $nextNotes = [];

        foreach ($notes as $note) {
            if (! is_array($note)) {
                $nextNotes[] = $note;
                continue;
            }

            $noteResult = $this->ensurePromptComparisonNoteVariants($note, $force, $dryRun);
            $generatedVariants += $noteResult['generated_variants'];
            $plannedVariants += $noteResult['planned_variants'];
            $changed = $changed || $noteResult['changed'];
            $nextNotes[] = $noteResult['note'];
        }

        if ($changed && ! $dryRun && $nextNotes !== $notes) {
            $prompt->forceFill([
                'tool_notes' => $nextNotes,
            ])->save();
        }

        return [
            'changed' => $changed,
            'generated_variants' => $generatedVariants,
            'planned_variants' => $plannedVariants,
        ];
    }

    /**
     * @param  array<string, mixed>  $note
     * @return array{note:array<string, mixed>,changed:bool,generated_variants:int,planned_variants:int}
     */
    private function ensurePromptComparisonNoteVariants(array $note, bool $force, bool $dryRun): array
    {
        $imagePath = trim((string) ($note['image_path'] ?? ''));

        if (! $this->isManagedLessonMediaPath($imagePath)) {
            return [
                'note' => $note,
                'changed' => false,
                'generated_variants' => 0,
                'planned_variants' => 0,
            ];
        }

        $variants = $this->ensureManagedImageVariants($imagePath, $force, $dryRun);
        $thumbPath = $variants['thumb_path'] ?? '';

        if ($thumbPath === '') {
            $thumbPath = $imagePath;
        }

        $nextNote = $note;
        $currentThumbPath = trim((string) ($note['thumb_path'] ?? ''));

        if ($currentThumbPath !== $thumbPath) {
            $nextNote['thumb_path'] = $thumbPath;
            $variants['changed'] = true;
        }

        return [
            'note' => $nextNote,
            'changed' => (bool) $variants['changed'],
            'generated_variants' => (int) $variants['generated_variants'],
            'planned_variants' => (int) $variants['planned_variants'],
        ];
    }

    /**
     * @return array{thumb_path:string,changed:bool,generated_variants:int,planned_variants:int}
     */
    private function ensureManagedImageVariants(string $path, bool $force, bool $dryRun): array
    {
        $path = trim($path);

        if (! $this->isManagedPromptPreviewPath($path) && ! $this->isManagedLessonMediaPath($path)) {
            return [
                'thumb_path' => '',
                'changed' => false,
                'generated_variants' => 0,
                'planned_variants' => 0,
            ];
        }

        $source = $this->openManagedImage($path);

        try {
            $generatedVariants = 0;
            $plannedVariants = 0;
            $changed = false;
            $thumbPath = $path;

            foreach (self::VARIANT_WIDTHS as $variant => $targetWidth) {
                $status = $this->ensureVariantForWidth(
                    $source['image'],
                    $source['width'],
                    $source['height'],
                    $path,
                    $variant,
                    $targetWidth,
                    $force,
                    $dryRun,
                );

                if ($variant === 'thumb' && $source['width'] > $targetWidth) {
                    $thumbPath = $this->variantPath($path, 'thumb');
                }

                if ($status === 'generated') {
                    $generatedVariants++;
                    $changed = true;
                }

                if ($status === 'planned') {
                    $plannedVariants++;
                    $changed = true;
                }
            }

            return [
                'thumb_path' => $thumbPath,
                'changed' => $changed,
                'generated_variants' => $generatedVariants,
                'planned_variants' => $plannedVariants,
            ];
        } finally {
            imagedestroy($source['image']);
        }
    }

    /**
     * @return array{image:\GdImage,width:int,height:int}
     */
    private function openManagedImage(string $path): array
    {
        $disk = Storage::disk($this->storageDisk());

        if (! $disk->exists($path)) {
            throw new RuntimeException(sprintf('Source image is missing: %s', $path));
        }

        $binary = $disk->get($path);

        if (! is_string($binary) || $binary === '') {
            throw new RuntimeException(sprintf('Source image could not be read: %s', $path));
        }

        $image = @imagecreatefromstring($binary);

        if (! $image instanceof \GdImage) {
            throw new RuntimeException(sprintf('Source image is not a supported raster image: %s', $path));
        }

        if (! imageistruecolor($image)) {
            imagepalettetotruecolor($image);
        }

        imagealphablending($image, true);
        imagesavealpha($image, true);

        return [
            'image' => $image,
            'width' => imagesx($image),
            'height' => imagesy($image),
        ];
    }

    private function ensureVariantForWidth(\GdImage $source, int $sourceWidth, int $sourceHeight, string $sourcePath, string $variant, int $targetWidth, bool $force, bool $dryRun): string
    {
        if ($sourceWidth <= $targetWidth || $sourceWidth < 1 || $sourceHeight < 1) {
            return 'skipped';
        }

        $variantPath = $this->variantPath($sourcePath, $variant);
        $disk = Storage::disk($this->storageDisk());

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

        if ($dryRun) {
            return 'planned';
        }

        $targetHeight = max(1, (int) round(($sourceHeight / $sourceWidth) * $targetWidth));
        $canvas = imagecreatetruecolor($targetWidth, $targetHeight);

        if (! $canvas instanceof \GdImage) {
            throw new RuntimeException(sprintf('Could not allocate variant canvas for %s', $sourcePath));
        }

        imagealphablending($canvas, false);
        imagesavealpha($canvas, true);
        $transparent = imagecolorallocatealpha($canvas, 0, 0, 0, 127);
        imagefilledrectangle($canvas, 0, 0, $targetWidth, $targetHeight, $transparent);
        imagecopyresampled($canvas, $source, 0, 0, 0, 0, $targetWidth, $targetHeight, $sourceWidth, $sourceHeight);

        try {
            ob_start();
            $converted = imagewebp($canvas, null, $this->qualityForPath($sourcePath));
            $webpBinary = ob_get_clean();

            if (! $converted || ! is_string($webpBinary) || $webpBinary === '') {
                throw new RuntimeException(sprintf('Could not encode %s variant for %s', $variant, $sourcePath));
            }

            $disk->put($variantPath, $webpBinary, ['visibility' => 'public']);
        } finally {
            imagedestroy($canvas);
        }

        return 'generated';
    }

    private function variantPath(string $path, string $variant): string
    {
        $directory = pathinfo($path, PATHINFO_DIRNAME);
        $filename = pathinfo($path, PATHINFO_FILENAME);
        $baseFilename = preg_replace('/-(thumb|md)$/', '', $filename) ?? $filename;

        return sprintf('%s/%s-%s.webp', $directory, $baseFilename, $variant);
    }

    private function isManagedPromptPreviewPath(string $path): bool
    {
        return $this->isLocalPath($path) && str_starts_with($path, self::PROMPT_PREVIEW_PREFIX . '/');
    }

    private function isManagedLessonMediaPath(string $path): bool
    {
        return $this->isLocalPath($path)
            && (str_starts_with($path, 'academy/lessons/body/') || str_starts_with($path, 'academy/lessons/covers/'));
    }

    private function isLocalPath(string $path): bool
    {
        return $path !== ''
            && ! str_starts_with($path, 'http://')
            && ! str_starts_with($path, 'https://')
            && ! str_starts_with($path, '/');
    }

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

    private function qualityForPath(string $path): int
    {
        return $this->isManagedPromptPreviewPath($path)
            ? self::PREVIEW_WEBP_QUALITY
            : self::LESSON_MEDIA_WEBP_QUALITY;
    }
}