<?php

declare(strict_types=1);

namespace App\Services\Sitemaps;

use Illuminate\Support\Facades\Cache;

final class SitemapPublishService
{
    public function __construct(
        private readonly SitemapBuildService $build,
        private readonly SitemapReleaseCleanupService $cleanup,
        private readonly SitemapReleaseManager $releases,
        private readonly SitemapReleaseValidator $validator,
        private readonly SitemapStaticPublisher $staticPublisher,
    ) {
    }

    /**
     * @param  list<string>|null  $families
     * @return array<string, mixed>
     */
    public function buildRelease(?array $families = null, ?string $releaseId = null): array
    {
        return $this->withLock(function () use ($families, $releaseId): array {
            return $this->buildReleaseUnlocked($families, $releaseId);
        });
    }

    /**
     * @return array<string, mixed>
     */
    public function publish(?string $releaseId = null): array
    {
        return $this->withLock(function () use ($releaseId): array {
            $manifest = $releaseId !== null
                ? $this->releases->readManifest($releaseId)
                : $this->buildReleaseUnlocked();

            if ($manifest === null) {
                throw new \RuntimeException('Sitemap release [' . $releaseId . '] does not exist.');
            }

            $releaseId = (string) $manifest['release_id'];
            $validation = $this->validator->validate($releaseId);

            if (! ($validation['ok'] ?? false)) {
                $manifest['status'] = 'failed';
                $manifest['validation'] = $validation;
                $this->releases->writeManifest($releaseId, $manifest);

                throw new \RuntimeException('Sitemap release validation failed.');
            }

            $manifest['status'] = 'published';
            $manifest['published_at'] = now()->toAtomString();
            $manifest['validation'] = $validation;
            $this->releases->writeManifest($releaseId, $manifest);
            $this->releases->activate($releaseId);
            $deleted = $this->cleanup->cleanup();

            $staticResult = [];
            if ($this->staticPublisher->enabled()) {
                $staticResult = $this->staticPublisher->publish($releaseId);
            }

            return $manifest + ['cleanup_deleted' => $deleted, 'static_published' => $staticResult];
        });
    }

    /**
     * @return array<string, mixed>
     */
    public function rollback(?string $releaseId = null): array
    {
        return $this->withLock(function () use ($releaseId): array {
            if ($releaseId === null) {
                $activeReleaseId = $this->releases->activeReleaseId();
                foreach ($this->releases->listReleases() as $release) {
                    if ((string) ($release['status'] ?? '') === 'published' && (string) ($release['release_id'] ?? '') !== $activeReleaseId) {
                        $releaseId = (string) $release['release_id'];
                        break;
                    }
                }
            }

            if (! is_string($releaseId) || $releaseId === '') {
                throw new \RuntimeException('No rollback release is available.');
            }

            $manifest = $this->releases->readManifest($releaseId);
            if ($manifest === null) {
                throw new \RuntimeException('Rollback release [' . $releaseId . '] not found.');
            }

            $this->releases->activate($releaseId);

            return $manifest + ['rolled_back_at' => now()->toAtomString()];
        });
    }

    /**
     * @template TReturn
     * @param  callable(): TReturn  $callback
     * @return TReturn
     */
    private function withLock(callable $callback): mixed
    {
        $lock = Cache::lock('sitemaps:publish-flow', max(30, (int) config('sitemaps.releases.lock_seconds', 900)));

        if (! $lock->get()) {
            throw new \RuntimeException('Another sitemap build or publish operation is already running.');
        }

        try {
            return $callback();
        } finally {
            $lock->release();
        }
    }

    /**
     * @param  list<string>|null  $families
     * @return array<string, mixed>
     */
    private function buildReleaseUnlocked(?array $families = null, ?string $releaseId = null): array
    {
        $selectedFamilies = $families ?: $this->build->enabledFamilies();
        $releaseId ??= $this->releases->generateReleaseId();

        $familyManifest = [];
        $documents = [
            SitemapCacheService::INDEX_DOCUMENT => $this->releases->documentRelativePath(SitemapCacheService::INDEX_DOCUMENT),
        ];
        $totalUrls = 0;

        $rootIndex = $this->build->buildIndex(true, false, $selectedFamilies);
        $this->releases->putDocument($releaseId, SitemapCacheService::INDEX_DOCUMENT, (string) $rootIndex['content']);

        foreach ($selectedFamilies as $family) {
            $builder = app(SitemapRegistry::class)->get($family);
            if ($builder === null) {
                continue;
            }

            $canonicalNames = $this->build->canonicalDocumentNamesForFamily($family);
            $shardNames = [];
            $urlCount = 0;

            foreach ($canonicalNames as $documentName) {
                $built = $this->build->buildNamed($documentName, true, false);
                if ($built === null) {
                    throw new \RuntimeException('Failed to build sitemap document [' . $documentName . '].');
                }

                $this->releases->putDocument($releaseId, $documentName, (string) $built['content']);
                $documents[$documentName] = $this->releases->documentRelativePath($documentName);

                if ($built['type'] !== SitemapTarget::TYPE_INDEX) {
                    $urlCount += (int) $built['url_count'];
                }

                if (str_starts_with($documentName, $family . '-') && ! str_ends_with($documentName, '-index')) {
                    $shardNames[] = $documentName;
                }
            }

            $totalUrls += $urlCount;
            $familyManifest[$family] = [
                'family' => $family,
                'entry_name' => app(SitemapShardService::class)->rootEntryName($builder),
                'documents' => $canonicalNames,
                'shards' => $shardNames,
                'url_count' => $urlCount,
                'shard_count' => count($shardNames),
                'type' => $builder->name() === (string) config('sitemaps.news.google_variant_name', 'news-google')
                    ? SitemapTarget::TYPE_GOOGLE_NEWS
                    : (count($shardNames) > 0 ? SitemapTarget::TYPE_INDEX : SitemapTarget::TYPE_URLSET),
            ];
        }

        $manifest = [
            'release_id' => $releaseId,
            'status' => 'built',
            'built_at' => now()->toAtomString(),
            'published_at' => null,
            'families' => $familyManifest,
            'documents' => $documents,
            'totals' => [
                'families' => count($familyManifest),
                'documents' => count($documents),
                'urls' => $totalUrls,
            ],
        ];

        $this->releases->writeManifest($releaseId, $manifest);

        $validation = $this->validator->validate($releaseId);
        $manifest['status'] = ($validation['ok'] ?? false) ? 'validated' : 'failed';
        $manifest['validation'] = $validation;
        $this->releases->writeManifest($releaseId, $manifest);
        $this->releases->writeBuildReport($releaseId, $validation);

        return $manifest;
    }
}