<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\Artwork;
use App\Services\Sitemaps\SitemapBuildService;
use App\Services\Sitemaps\SitemapImage;
use App\Services\Sitemaps\SitemapIndexItem;
use App\Services\Sitemaps\SitemapUrl;
use App\Services\ThumbnailPresenter;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

final class BuildSitemapsCommand extends Command
{
    protected $signature = 'skinbase:sitemaps:build
        {--only=*    : Limit to specific sitemap families (comma or space separated)}
        {--disk=     : Override the target filesystem disk (default: sitemaps.static_publish.disk)}
        {--progress  : Show a progress bar tracking processed URLs for each family}';    

    protected $description = 'Build all sitemaps and write them as static .xml files to public/.';

    public function handle(SitemapBuildService $build): int
    {
        $totalStart = microtime(true);
        $families   = $this->selectedFamilies($build);

        if ($families === []) {
            $this->error('No valid sitemap families selected.');

            return self::INVALID;
        }

        $diskName = (string) ($this->option('disk') ?: config('sitemaps.static_publish.disk', 'sitemaps_public'));
        $disk     = Storage::disk($diskName);
        $written  = 0;
        $failed   = 0;

        $this->info('Disk: ' . $diskName);
        $this->info('Families: ' . implode(', ', $families));
        $this->newLine();

        // ── Root sitemap index ────────────────────────────────────────────
        $t = microtime(true);
        $this->line('  Building sitemap index…');
        $index = $build->buildIndex(force: true, persist: false, families: $families);
        $disk->put('sitemap.xml', $index['content']);
        $written++;
        $this->line(sprintf(
            '  <info>✔</info>  sitemap.xml  %d entries  <comment>%.3fs</comment>',
            $index['url_count'],
            microtime(true) - $t,
        ));

        // ── Per-family documents ──────────────────────────────────────────
        foreach ($families as $family) {
            $familyStart = microtime(true);

            $this->newLine();

            if ($family === 'artworks') {
                // Direct MySQL path — no cursor-scan shard window computation
                [$shardNames, $fw, $ff] = $this->buildArtworksDirect($disk, (bool) $this->option('progress'));
                $written += $fw;
                $failed  += $ff;
                $this->line(sprintf(
                    '  <fg=cyan>artworks</> done  %d file(s)  <comment>%.3fs</comment>',
                    $fw,
                    microtime(true) - $familyStart,
                ));

                continue;
            }

            $names = $build->canonicalDocumentNamesForFamily($family);

            $this->line(sprintf('  <fg=cyan>%s</> (%d document(s))', $family, count($names)));

            foreach ($names as $documentName) {
                $t = microtime(true);
                $this->line(sprintf('    Building %s…', $documentName));

                $built = $build->buildNamed($documentName, force: true, persist: false);

                if ($built === null) {
                    $this->line(sprintf('    <comment>–</comment>  %s  <fg=red>SKIPPED</>  (builder returned null)', $documentName));
                    $failed++;

                    continue;
                }

                $disk->put('sitemaps/' . $documentName . '.xml', $built['content']);
                $written++;

                $this->line(sprintf(
                    '    <info>✔</info>  %s  %d URLs  <comment>%.3fs</comment>',
                    $documentName . '.xml',
                    $built['url_count'] ?? 0,
                    microtime(true) - $t,
                ));
            }

            $this->line(sprintf(
                '  <fg=cyan>%s</> done  <comment>%.3fs</comment>',
                $family,
                microtime(true) - $familyStart,
            ));
        }

        // ── Summary ───────────────────────────────────────────────────────
        $this->newLine();
        $this->info(sprintf(
            'Done: %d file(s) written, %d failed  total <comment>%.3fs</comment>',
            $written,
            $failed,
            microtime(true) - $totalStart,
        ));

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

    /**
     * Stream artworks directly from MySQL using chunkById — avoids cursor-scan shard windows.
     *
     * @return array{0: list<string>, 1: int, 2: int}  [shardNames, written, failed]
     */
    private function buildArtworksDirect(Filesystem $disk, bool $showProgress = false): array
    {
        $chunkSize  = max(1, (int) config('sitemaps.shards.artworks.size', 10_000));
        $padLen     = max(1, (int) config('sitemaps.shards.zero_pad_length', 4));
        $shardNum   = 0;
        $shardNames = [];
        $written    = 0;
        $failed     = 0;

        $baseQuery = Artwork::query()
            ->public()
            ->published();

        $total = $showProgress ? (clone $baseQuery)->count() : null;

        $this->line(sprintf(
            '  <fg=cyan>artworks</> (chunk size: %d%s)',
            $chunkSize,
            $total !== null ? ', total: ' . number_format($total) : '',
        ));

        $bar = null;
        if ($total !== null) {
            $bar = $this->output->createProgressBar($total);
            $bar->setFormat('    %current%/%max% [%bar%] %percent:3s%%  shard %message%  elapsed: %elapsed:6s%  mem: %memory:6s%');
            $bar->setMessage('—');
            $bar->start();
            $this->newLine();
        }

        $baseQuery
            ->select(['id', 'slug', 'title', 'updated_at', 'published_at', 'created_at', 'hash', 'file_path', 'file_name'])
            ->orderBy('id')
            ->chunkById($chunkSize, function ($artworks) use ($disk, $padLen, $bar, &$shardNum, &$shardNames, &$written, &$failed): void {
                $shardNum++;
                $t    = microtime(true);
                $name = 'artworks-' . str_pad((string) $shardNum, $padLen, '0', STR_PAD_LEFT);

                if ($bar !== null) {
                    $bar->setMessage($name);
                } else {
                    $this->line(sprintf('    Building %s (%d rows)…', $name, $artworks->count()));
                }

                /** @var list<SitemapUrl> $items */
                $items = $artworks
                    ->map(fn (Artwork $artwork): ?SitemapUrl => $this->artworkSitemapUrl($artwork))
                    ->filter()
                    ->values()
                    ->all();

                $xml = view('sitemaps.urlset', [
                    'items'     => $items,
                    'hasImages' => collect($items)->contains(fn (SitemapUrl $item): bool => $item->images !== []),
                ])->render();

                if (! $disk->put('sitemaps/' . $name . '.xml', $xml)) {
                    if ($bar !== null) {
                        $bar->advance($artworks->count());
                        $this->newLine();
                    }
                    $this->line(sprintf('    <comment>–</comment>  %s  <fg=red>WRITE FAILED</>', $name . '.xml'));
                    $failed++;

                    return;
                }

                $shardNames[] = $name;
                $written++;

                if ($bar !== null) {
                    $bar->advance($artworks->count());
                } else {
                    $this->line(sprintf(
                        '    <info>✔</info>  %s  %d URLs  <comment>%.3fs</comment>',
                        $name . '.xml',
                        count($items),
                        microtime(true) - $t,
                    ));
                }
            });

        if ($bar !== null) {
            $bar->setMessage('done');
            $bar->finish();
            $this->newLine();
        }

        if ($shardNames === []) {
            return [[], 0, $failed];
        }

        // Write artworks-index.xml when there are multiple shards (matches SitemapShardService behaviour)
        if (count($shardNames) > 1) {
            $t = microtime(true);
            $indexItems = array_map(
                fn (string $n): SitemapIndexItem => new SitemapIndexItem(url('/sitemaps/' . $n . '.xml')),
                $shardNames,
            );

            $indexXml = view('sitemaps.index', ['items' => $indexItems])->render();
            $disk->put('sitemaps/artworks-index.xml', $indexXml);
            $written++;

            $this->line(sprintf(
                '    <info>✔</info>  artworks-index.xml  %d shards  <comment>%.3fs</comment>',
                count($shardNames),
                microtime(true) - $t,
            ));
        }

        return [$shardNames, $written, $failed];
    }

    private function artworkSitemapUrl(Artwork $artwork): ?SitemapUrl
    {
        $slug = Str::slug((string) ($artwork->slug ?: $artwork->title));
        if ($slug === '') {
            $slug = (string) $artwork->id;
        }

        $preview = ThumbnailPresenter::present($artwork, 'xl');

        $images = [];
        if (! empty($preview['url'])) {
            $images[] = new SitemapImage((string) $preview['url'], $artwork->title ?: null);
        }

        $timestamps = array_filter(array_map(
            static fn (mixed $v): ?Carbon => $v instanceof Carbon ? $v : (is_string($v) ? Carbon::parse($v) : null),
            [$artwork->updated_at, $artwork->published_at, $artwork->created_at],
        ));

        usort($timestamps, static fn (Carbon $a, Carbon $b): int => $b->timestamp <=> $a->timestamp);

        return new SitemapUrl(
            route('art.show', ['id' => (int) $artwork->id, 'slug' => $slug]),
            $timestamps[0] ?? null,
            $images,
        );
    }

    /**
     * @return list<string>
     */
    private function selectedFamilies(SitemapBuildService $build): array
    {
        $only = [];

        foreach ((array) $this->option('only') as $value) {
            foreach (explode(',', (string) $value) as $family) {
                $normalized = trim($family);
                if ($normalized !== '') {
                    $only[] = $normalized;
                }
            }
        }

        $enabled = $build->enabledFamilies();

        if ($only === []) {
            return $enabled;
        }

        return array_values(array_filter($enabled, fn (string $family): bool => in_array($family, $only, true)));
    }
}