<?php

declare(strict_types=1);

namespace App\Services\Sitemaps\Builders;

use App\Services\Sitemaps\AbstractSitemapBuilder;
use App\Services\Sitemaps\ShardableSitemapBuilder;
use App\Services\Sitemaps\SitemapUrl;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

abstract class AbstractIdShardableSitemapBuilder extends AbstractSitemapBuilder implements ShardableSitemapBuilder
{
    /**
     * @return Builder<Model>
     */
    abstract protected function query(): Builder;

    abstract protected function shardConfigKey(): string;

    abstract protected function mapRecord(Model $record): ?SitemapUrl;

    public function items(): array
    {
        return $this->query()
            ->orderBy($this->idColumn())
            ->cursor()
            ->map(fn (Model $record): ?SitemapUrl => $this->mapRecord($record))
            ->filter()
            ->values()
            ->all();
    }

    public function lastModified(): ?DateTimeInterface
    {
        return $this->dateTime((clone $this->query())->max($this->lastModifiedColumn()));
    }

    public function totalItems(): int
    {
        return (clone $this->query())->count();
    }

    public function shardSize(): int
    {
        return max(1, (int) \data_get(\config('sitemaps.shards', []), $this->shardConfigKey() . '.size', 10000));
    }

    public function itemsForShard(int $shard): array
    {
        $window = $this->shardWindow($shard);

        if ($window === null) {
            return [];
        }

        return $this->applyShardWindow($window['from'], $window['to'])
            ->get()
            ->map(fn (Model $record): ?SitemapUrl => $this->mapRecord($record))
            ->filter()
            ->values()
            ->all();
    }

    public function lastModifiedForShard(int $shard): ?DateTimeInterface
    {
        $window = $this->shardWindow($shard);

        if ($window === null) {
            return null;
        }

        return $this->dateTime(
            $this->applyShardWindow($window['from'], $window['to'])
                ->max($this->lastModifiedColumn()),
        );
    }

    /**
     * @return array{from: int, to: int}|null
     */
    protected function shardWindow(int $shard): ?array
    {
        if ($shard < 1) {
            return null;
        }

        $size = $this->shardSize();
        $current = 0;
        $from = null;
        $to = null;

        $windowQuery = (clone $this->query())
            ->setEagerLoads([])
            ->select([$this->idColumn()])
            ->orderBy($this->idColumn());

        foreach ($windowQuery->cursor() as $record) {
            $current++;

            if ((int) ceil($current / $size) !== $shard) {
                continue;
            }

            $recordId = (int) $record->getAttribute($this->idColumn());
            $from ??= $recordId;
            $to = $recordId;
        }

        if ($from === null || $to === null) {
            return null;
        }

        return ['from' => $from, 'to' => $to];
    }

    /**
     * @return Builder<Model>
     */
    protected function applyShardWindow(int $from, int $to): Builder
    {
        return (clone $this->query())
            ->whereBetween($this->qualifiedIdColumn(), [$from, $to])
            ->orderBy($this->idColumn());
    }

    protected function idColumn(): string
    {
        return 'id';
    }

    protected function qualifiedIdColumn(): string
    {
        return $this->idColumn();
    }

    protected function lastModifiedColumn(): string
    {
        return 'updated_at';
    }
}