<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Services\ArtworkSearchIndexer;
use App\Models\Artwork;
use Illuminate\Console\Command;
use Meilisearch\Client as MeilisearchClient;

class RebuildArtworkSearchIndex extends Command
{
    protected $signature   = 'artworks:search-rebuild
                            {--chunk=500 : Number of artworks per chunk}
                            {--limit= : Stop after processing this many artworks (useful for testing)}
                            {--reverse : Process artworks newest-first (highest ID first)}
                            {--sync : Write directly to Meilisearch (no queue) and show per-artwork results}';
    protected $description = 'Re-queue all artworks for Meilisearch indexing (non-blocking, chunk-based). Use --sync for verbose direct writes.';

    public function __construct(private readonly ArtworkSearchIndexer $indexer)
    {
        parent::__construct();
    }

    public function handle(MeilisearchClient $client): int
    {
        $chunk   = max(1, (int) $this->option('chunk'));
        $limit   = $this->option('limit') !== null ? max(1, (int) $this->option('limit')) : null;
        $reverse = (bool) $this->option('reverse');
        $sync    = (bool) $this->option('sync');

        if ($sync) {
            return $this->handleSync($client, $chunk, $limit, $reverse);
        }

        return $this->handleQueue($chunk, $limit, $reverse);
    }

    // ── Queue mode (default) ──────────────────────────────────────────────────

    private function handleQueue(int $chunk, ?int $limit, bool $reverse): int
    {
        $uncapped = Artwork::query()->public()->published()->count();
        $total    = $limit !== null ? min($limit, $uncapped) : $uncapped;

        if ($total === 0) {
            $this->warn('No public, published artworks matched the rebuild query. Nothing was queued.');

            return self::SUCCESS;
        }

        $estimatedChunks = (int) ceil($total / $chunk);

        $this->info(sprintf(
            'Queueing Meilisearch rebuild for %d artwork(s) in %d chunk(s) of up to %d%s%s.',
            $total,
            $estimatedChunks,
            $chunk,
            $reverse ? ', newest first' : '',
            $limit !== null ? " (limit {$limit})" : '',
        ));
        $this->line('This command only dispatches queue jobs. Workers process the actual indexing asynchronously.');

        $bar = $this->output->createProgressBar($total);
        $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%%');
        $bar->start();

        $startedAt = microtime(true);

        $stats = $this->indexer->rebuildAll(
            $chunk,
            function (int $chunkNumber, int $chunkCount, int $dispatched, int $totalItems, int $firstId, int $lastId) use ($bar): void {

                $bar->advance($chunkCount);

                if ($this->output->isVerbose()) {
                    $bar->clear();
                    $this->line(sprintf(
                        'Chunk %d queued %d artwork(s) [ids %d-%d] (%d/%d dispatched).',
                        $chunkNumber,
                        $chunkCount,
                        $firstId,
                        $lastId,
                        $dispatched,
                        $totalItems,
                    ));
                    $bar->display();
                }
            },
            $reverse,
            $limit,
        );

        $bar->finish();
        $this->newLine(2);

        $elapsed = microtime(true) - $startedAt;

        $this->info(sprintf(
            'Queued %d artwork(s) across %d chunk(s) in %.2f seconds.',
            $stats['dispatched'],
            $stats['chunks'],
            $elapsed,
        ));
        $this->line('Workers will process the actual Meilisearch writes asynchronously.');

        if ($this->output->isVerbose()) {
            $this->line('Tip: use -v for per-chunk output, or monitor Horizon/queue workers for completion.');
        }

        return self::SUCCESS;
    }

    // ── Sync mode (--sync) ────────────────────────────────────────────────────

    private function handleSync(MeilisearchClient $client, int $chunk, ?int $limit, bool $reverse): int
    {
        $this->info(sprintf(
            '<options=bold>[SYNC MODE]</> Writing directly to Meilisearch%s%s — no queue involved.',
            $reverse ? ', newest first' : '',
            $limit !== null ? ", limit {$limit}" : '',
        ));
        $this->newLine();

        $query = Artwork::with([
                'user', 'group', 'tags', 'categories.contentType', 'stats', 'awardStat',
            ])
            ->withoutGlobalScopes()  // include non-public so we can report "why not"
            ->whereNotNull('id');     // all artworks

        if ($reverse) {
            $query->orderByDesc('id');
        } else {
            $query->orderBy('id');
        }

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

        $total      = (clone $query)->count();
        $indexed    = 0;
        $removed    = 0;
        $failed     = 0;
        $processed  = 0;
        $startedAt  = microtime(true);

        $bar = $this->output->createProgressBar($total);
        $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%%');
        $bar->start();

        $query->chunk($chunk, function ($artworks) use ($client, $bar, &$indexed, &$removed, &$failed, &$processed): void {
            foreach ($artworks as $artwork) {
                $processed++;
                $id    = (int) $artwork->id;
                $title = (string) ($artwork->title ?? '(no title)');

                // Determine eligibility and reason
                $reasons = [];
                if (! $artwork->is_public)          { $reasons[] = 'not public'; }
                if (! $artwork->is_approved)         { $reasons[] = 'not approved'; }
                if ($artwork->published_at === null) { $reasons[] = 'not published'; }
                if ($artwork->deleted_at !== null)   { $reasons[] = 'soft-deleted'; }

                $eligible = empty($reasons);

                try {
                    $indexName = $artwork->searchableAs();

                    if ($eligible) {
                        $document = $artwork->toSearchableArray();
                        $client->index($indexName)->addDocuments([$document]);
                        $indexed++;
                        $bar->clear();
                        $this->line(sprintf('  <info>✓ indexed</info>  #%d "%s"', $id, $title));
                    } else {
                        $client->index($indexName)->deleteDocument($id);
                        $removed++;
                        $bar->clear();
                        $this->line(sprintf('  <comment>– removed</comment>  #%d "%s"  [%s]', $id, $title, implode(', ', $reasons)));
                    }
                } catch (\Throwable $e) {
                    $failed++;
                    $bar->clear();
                    $this->line(sprintf('  <error>✗ failed</error>   #%d "%s"  %s', $id, $title, $e->getMessage()));
                }

                $bar->advance();
            }
        });

        $bar->finish();
        $this->newLine(2);

        $elapsed = microtime(true) - $startedAt;

        $this->info(sprintf(
            'Done in %.2f s — %d indexed, %d removed from index, %d failed (of %d processed).',
            $elapsed,
            $indexed,
            $removed,
            $failed,
            $processed,
        ));

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