<?php

declare(strict_types=1);

namespace App\Services\Sitemaps;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

final class SitemapReleaseManager
{
    public function generateReleaseId(): string
    {
        return now()->utc()->format('YmdHis') . '-' . Str::lower((string) Str::ulid());
    }

    public function releaseExists(string $releaseId): bool
    {
        return Storage::disk($this->disk())->exists($this->manifestPath($releaseId));
    }

    public function putDocument(string $releaseId, string $documentName, string $content): void
    {
        Storage::disk($this->disk())->put($this->releaseDocumentPath($releaseId, $documentName), $content);
    }

    public function getDocument(string $releaseId, string $documentName): ?string
    {
        $disk = Storage::disk($this->disk());
        $path = $this->releaseDocumentPath($releaseId, $documentName);

        if (! $disk->exists($path)) {
            return null;
        }

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

        return is_string($content) && $content !== '' ? $content : null;
    }

    public function writeManifest(string $releaseId, array $manifest): void
    {
        Storage::disk($this->disk())->put($this->manifestPath($releaseId), json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
    }

    public function readManifest(string $releaseId): ?array
    {
        $disk = Storage::disk($this->disk());
        $path = $this->manifestPath($releaseId);

        if (! $disk->exists($path)) {
            return null;
        }

        $decoded = json_decode((string) $disk->get($path), true);

        return is_array($decoded) ? $decoded : null;
    }

    public function writeBuildReport(string $releaseId, array $report): void
    {
        Storage::disk($this->disk())->put($this->buildReportPath($releaseId), json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
    }

    public function activate(string $releaseId): void
    {
        $payload = [
            'release_id' => $releaseId,
            'activated_at' => now()->toAtomString(),
        ];

        $this->atomicJsonWrite($this->activePointerPath(), $payload);

        Cache::forget('sitemaps:active-release-id');
    }

    public function activeReleaseId(): ?string
    {
        $disk = Storage::disk($this->disk());
        $path = $this->activePointerPath();

        if (! $disk->exists($path)) {
            return null;
        }

        $decoded = json_decode((string) $disk->get($path), true);

        return is_array($decoded) && is_string($decoded['release_id'] ?? null)
            ? $decoded['release_id']
            : null;
    }

    public function activeManifest(): ?array
    {
        $releaseId = $this->activeReleaseId();

        return $releaseId !== null ? $this->readManifest($releaseId) : null;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listReleases(): array
    {
        $releases = [];

        foreach (Storage::disk($this->disk())->allDirectories($this->releasesRootPath()) as $directory) {
            $releaseId = basename($directory);
            $manifest = $this->readManifest($releaseId);

            if ($manifest !== null) {
                $releases[] = $manifest;
            }
        }

        usort($releases, static fn (array $left, array $right): int => strcmp((string) ($right['release_id'] ?? ''), (string) ($left['release_id'] ?? '')));

        return $releases;
    }

    public function deleteRelease(string $releaseId): void
    {
        Storage::disk($this->disk())->deleteDirectory($this->releaseRootPath($releaseId));
    }

    public function documentRelativePath(string $documentName): string
    {
        return $documentName === SitemapCacheService::INDEX_DOCUMENT
            ? 'sitemaps/sitemap.xml'
            : 'sitemaps/' . $documentName . '.xml';
    }

    public function releaseDocumentPath(string $releaseId, string $documentName): string
    {
        return $this->releaseRootPath($releaseId) . '/' . $this->documentRelativePath($documentName);
    }

    public function manifestPath(string $releaseId): string
    {
        return $this->releaseRootPath($releaseId) . '/manifest.json';
    }

    public function buildReportPath(string $releaseId): string
    {
        return $this->releaseRootPath($releaseId) . '/build-report.json';
    }

    private function releaseRootPath(string $releaseId): string
    {
        return $this->releasesRootPath() . '/' . $releaseId;
    }

    private function releasesRootPath(): string
    {
        return trim((string) config('sitemaps.releases.path', 'sitemaps'), '/') . '/releases';
    }

    private function activePointerPath(): string
    {
        return trim((string) config('sitemaps.releases.path', 'sitemaps'), '/') . '/active.json';
    }

    private function disk(): string
    {
        return (string) config('sitemaps.releases.disk', 'local');
    }

    private function atomicJsonWrite(string $relativePath, array $payload): void
    {
        $disk = Storage::disk($this->disk());
        $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

        try {
            $absolutePath = $disk->path($relativePath);
            $directory = dirname($absolutePath);

            if (! is_dir($directory)) {
                mkdir($directory, 0755, true);
            }

            $temporaryPath = $absolutePath . '.tmp';
            file_put_contents($temporaryPath, $json, LOCK_EX);
            rename($temporaryPath, $absolutePath);
        } catch (\Throwable) {
            $disk->put($relativePath, $json);
        }
    }
}