<?php

declare(strict_types=1);

namespace App\Support\Ui\DerivedState;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

final class RequestScopedDerivedStateStore
{
    public const string FRESHNESS_REQUEST_STABLE = 'request_stable';

    public const string FRESHNESS_INVALIDATE_AFTER_MUTATION = 'invalidate_after_mutation';

    public const string FRESHNESS_NO_REUSE = 'no_reuse';

    private string $requestScopeId;

    /**
     * @var array<string, array{
     *     key: DerivedStateKey,
     *     value: mixed,
     *     negative_result: bool,
     *     freshness_policy: string,
     *     resolved_at: int
     * }>
     */
    private array $entries = [];

    /**
     * @var list<string>
     */
    private array $invalidations = [];

    private int $resolutionSequence = 0;

    public function __construct(?string $requestScopeId = null)
    {
        $this->requestScopeId = $requestScopeId ?? (string) Str::uuid();
    }

    public function requestScopeId(): string
    {
        return $this->requestScopeId;
    }

    public function resolve(
        DerivedStateKey $key,
        callable $resolver,
        ?string $freshnessPolicy = null,
        ?bool $allowNegativeResultCache = null,
    ): mixed {
        $freshnessPolicy ??= $key->family->defaultFreshnessPolicy();

        if ($freshnessPolicy === self::FRESHNESS_NO_REUSE) {
            return $resolver();
        }

        $fingerprint = $key->fingerprint();

        if (array_key_exists($fingerprint, $this->entries)) {
            return $this->entries[$fingerprint]['value'];
        }

        $value = $resolver();
        $negativeResult = $this->isNegativeResult($value);
        $allowNegativeResultCache ??= $key->family->allowsNegativeResultCache();

        if ($negativeResult && ! $allowNegativeResultCache) {
            return $value;
        }

        $this->entries[$fingerprint] = [
            'key' => $key,
            'value' => $value,
            'negative_result' => $negativeResult,
            'freshness_policy' => $freshnessPolicy,
            'resolved_at' => ++$this->resolutionSequence,
        ];

        return $value;
    }

    public function resolveFresh(
        DerivedStateKey $key,
        callable $resolver,
        ?string $freshnessPolicy = null,
        ?bool $allowNegativeResultCache = null,
    ): mixed {
        $this->invalidateKey($key);

        return $this->resolve($key, $resolver, $freshnessPolicy, $allowNegativeResultCache);
    }

    public function invalidateKey(DerivedStateKey $key): int
    {
        $fingerprint = $key->fingerprint();

        if (! array_key_exists($fingerprint, $this->entries)) {
            return 0;
        }

        unset($this->entries[$fingerprint]);
        $this->invalidations[] = $fingerprint;

        return 1;
    }

    public function invalidateFamily(
        DerivedStateFamily $family,
        ?string $recordClass = null,
        string|int|null $recordKey = null,
        ?string $variant = null,
        ?int $workspaceId = null,
        ?int $tenantId = null,
    ): int {
        $invalidated = 0;

        foreach ($this->entries as $fingerprint => $record) {
            if (! $record['key']->matches($family, $recordClass, $recordKey, $variant, $workspaceId, $tenantId)) {
                continue;
            }

            unset($this->entries[$fingerprint]);
            $this->invalidations[] = $fingerprint;
            $invalidated++;
        }

        return $invalidated;
    }

    public function invalidateModel(DerivedStateFamily $family, Model $record, ?string $variant = null): int
    {
        return $this->invalidateFamily(
            family: $family,
            recordClass: $record::class,
            recordKey: $record->getKey(),
            variant: $variant,
        );
    }

    public function entryCount(): int
    {
        return count($this->entries);
    }

    public function countStored(
        DerivedStateFamily $family,
        ?string $recordClass = null,
        string|int|null $recordKey = null,
        ?string $variant = null,
    ): int {
        return count(array_filter(
            $this->entries,
            static fn (array $record): bool => $record['key']->matches($family, $recordClass, $recordKey, $variant),
        ));
    }

    /**
     * @return array{
     *     key: DerivedStateKey,
     *     value: mixed,
     *     negative_result: bool,
     *     freshness_policy: string,
     *     resolved_at: int
     * }|null
     */
    public function resolutionRecord(DerivedStateKey $key): ?array
    {
        return $this->entries[$key->fingerprint()] ?? null;
    }

    /**
     * @return list<string>
     */
    public function invalidations(): array
    {
        return $this->invalidations;
    }

    private function isNegativeResult(mixed $value): bool
    {
        return $value === null || $value === [];
    }
}
