<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\Artwork;
use App\Services\Uploads\UploadStorageService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Throwable;

final class AuditArtworkThumbnailsCommand extends Command
{
    protected $signature = 'artworks:audit-thumbnails
        {--id= : Audit only this artwork ID}
        {--limit= : Stop after processing this many artworks}
        {--chunk=200 : Number of artworks to scan per batch}
        {--variant=* : Specific thumbnail variants to check (defaults to all configured derivatives)}
        {--dry-run : Report missing thumbnails without updating the artworks table}';

    protected $description = 'Check artwork thumbnails on the configured object storage disk and mark artworks with missing thumbnails.';

    public function handle(UploadStorageService $storage): int
    {
        $artworkId = $this->option('id') !== null ? max(1, (int) $this->option('id')) : null;
        $limit = $this->option('limit') !== null ? max(1, (int) $this->option('limit')) : null;
        $chunkSize = max(1, min((int) $this->option('chunk'), 1000));
        $dryRun = (bool) $this->option('dry-run');

        $variants = $this->resolveVariants();
        if ($variants === []) {
            $this->error('No thumbnail variants are configured. Check uploads.derivatives.');

            return self::FAILURE;
        }

        if (! $dryRun && ! Schema::hasColumns('artworks', [
            'has_missing_thumbnails',
            'missing_thumbnail_variants_json',
            'thumbnails_checked_at',
        ])) {
            $this->error('Artwork thumbnail audit columns are missing. Run the latest database migrations first.');

            return self::FAILURE;
        }

        $diskName = $storage->objectDiskName();
        $diskConfig = config("filesystems.disks.{$diskName}");
        if (! is_array($diskConfig)) {
            $this->error("Filesystem disk [{$diskName}] is not configured.");

            return self::FAILURE;
        }

        $disk = Storage::disk($diskName);
        $this->info(sprintf(
            'Starting thumbnail audit. disk=%s variants=%s chunk=%d limit=%s dry_run=%s',
            $diskName,
            implode(',', $variants),
            $chunkSize,
            $limit !== null ? (string) $limit : 'all',
            $dryRun ? 'yes' : 'no',
        ));

        $query = Artwork::query()
            ->select(['id', 'hash', 'thumb_ext'])
            ->orderBy('id');

        if ($artworkId !== null) {
            $query->whereKey($artworkId);
        }

        $processed = 0;
        $healthy = 0;
        $missing = 0;
        $written = 0;
        $failed = 0;

        $query->chunkById($chunkSize, function ($artworks) use ($storage, $disk, $variants, $limit, $dryRun, &$processed, &$healthy, &$missing, &$written, &$failed) {
            foreach ($artworks as $artwork) {
                if ($limit !== null && $processed >= $limit) {
                    return false;
                }

                try {
                    $missingVariants = $this->resolveMissingVariants($artwork, $variants, $storage, $disk);
                    $hasMissing = $missingVariants !== [];

                    if ($hasMissing) {
                        $missing++;
                        $this->warn(sprintf(
                            'Artwork %d missing thumbnails: %s',
                            (int) $artwork->id,
                            implode(',', $missingVariants),
                        ));
                    } else {
                        $healthy++;
                    }

                    if (! $dryRun) {
                        $this->persistAuditResult((int) $artwork->id, $hasMissing, $missingVariants);
                        $written++;
                    }
                } catch (Throwable $exception) {
                    $failed++;
                    $this->warn(sprintf('Artwork %d audit failed: %s', (int) $artwork->id, $exception->getMessage()));
                }

                $processed++;
            }

            return true;
        });

        $this->info(sprintf(
            'Thumbnail audit complete. processed=%d healthy=%d missing=%d written=%d failed=%d',
            $processed,
            $healthy,
            $missing,
            $written,
            $failed,
        ));

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

    /**
     * @return list<string>
     */
    private function resolveVariants(): array
    {
        $configured = array_keys((array) config('uploads.derivatives', []));
        $configured = array_values(array_filter(array_map(
            static fn ($variant): string => strtolower(trim((string) $variant)),
            $configured,
        )));

        $requested = (array) $this->option('variant');
        if ($requested === []) {
            return $configured;
        }

        $normalizedRequested = array_values(array_unique(array_filter(array_map(
            static fn ($variant): string => strtolower(trim((string) $variant)),
            $requested,
        ))));

        $invalid = array_values(array_diff($normalizedRequested, $configured));
        if ($invalid !== []) {
            $this->error('Unknown thumbnail variants: ' . implode(', ', $invalid));
            $this->line('Configured variants: ' . implode(', ', $configured));

            return [];
        }

        return $normalizedRequested;
    }

    /**
     * @param list<string> $variants
     * @return list<string>
     */
    private function resolveMissingVariants(Artwork $artwork, array $variants, UploadStorageService $storage, mixed $disk): array
    {
        $hash = strtolower((string) preg_replace('/[^a-z0-9]/', '', (string) ($artwork->hash ?? '')));
        $thumbExt = strtolower(ltrim((string) ($artwork->thumb_ext ?? ''), '.'));

        if ($hash === '' || $thumbExt === '') {
            return $variants;
        }

        $filename = $hash . '.' . $thumbExt;
        $missing = [];

        foreach ($variants as $variant) {
            $objectPath = $storage->objectPathForVariant($variant, $hash, $filename);
            if (! $disk->exists($objectPath)) {
                $missing[] = $variant;
            }
        }

        return $missing;
    }

    /**
     * @param list<string> $missingVariants
     */
    private function persistAuditResult(int $artworkId, bool $hasMissing, array $missingVariants): void
    {
        DB::table('artworks')
            ->where('id', $artworkId)
            ->update([
                'has_missing_thumbnails' => $hasMissing,
                'missing_thumbnail_variants_json' => $missingVariants === []
                    ? null
                    : json_encode(array_values($missingVariants), JSON_UNESCAPED_SLASHES),
                'thumbnails_checked_at' => now(),
            ]);
    }
}