<?php

declare(strict_types=1);

namespace App\Framework\GraphQL\DataLoader;

/**
 * Generic DataLoader for batching and caching data fetching
 *
 * Prevents N+1 queries by:
 * 1. Batching multiple load() calls into single batch
 * 2. Caching results within request
 * 3. De-duplicating identical requests
 *
 * Usage:
 * $userLoader = new DataLoader(fn($ids) => $userRepository->findByIds($ids));
 * $user1 = $userLoader->load(1);  // Queued
 * $user2 = $userLoader->load(2);  // Queued
 * $userLoader->dispatch();        // Single batch query executed
 */
final class DataLoader
{
    /** @var array<int|string, mixed> Cached results */
    private array $cache = [];

    /** @var array<int|string> Keys queued for loading */
    private array $queue = [];

    /** @var bool Whether batch has been dispatched */
    private bool $dispatched = false;

    /**
     * @param \Closure(array<int|string>): array<int|string, mixed> $batchLoadFn
     */
    public function __construct(
        private readonly \Closure $batchLoadFn
    ) {
    }

    /**
     * Load single item by key
     *
     * Returns immediately if cached, otherwise queues for batch loading
     */
    public function load(int|string $key): mixed
    {
        // Return from cache if available
        if (array_key_exists($key, $this->cache)) {
            return $this->cache[$key];
        }

        // Queue for batch loading if not already queued
        if (! in_array($key, $this->queue, strict: true)) {
            $this->queue[] = $key;
        }

        // Return cached value if available (after adding to queue)
        return $this->cache[$key] ?? null;
    }

    /**
     * Load multiple items by keys
     *
     * @param array<int|string> $keys
     * @return array<int|string, mixed>
     */
    public function loadMany(array $keys): array
    {
        $results = [];

        foreach ($keys as $key) {
            $results[$key] = $this->load($key);
        }

        return $results;
    }

    /**
     * Dispatch queued loads as single batch
     *
     * Executes batchLoadFn with all queued keys
     */
    public function dispatch(): void
    {
        if ($this->dispatched || empty($this->queue)) {
            return;
        }

        // Get unique keys to load
        $keysToLoad = array_values(array_unique($this->queue));

        // Execute batch load function
        $batchResults = ($this->batchLoadFn)($keysToLoad);

        // Cache all results
        foreach ($batchResults as $key => $value) {
            $this->cache[$key] = $value;
        }

        // Mark as dispatched and clear queue
        $this->dispatched = true;
        $this->queue = [];
    }

    /**
     * Prime cache with pre-loaded data
     */
    public function prime(int|string $key, mixed $value): void
    {
        $this->cache[$key] = $value;
    }

    /**
     * Clear cache and queue
     */
    public function clear(): void
    {
        $this->cache = [];
        $this->queue = [];
        $this->dispatched = false;
    }

    /**
     * Clear specific key from cache
     */
    public function clearKey(int|string $key): void
    {
        unset($this->cache[$key]);
    }

    /**
     * Get cache statistics
     */
    public function getStats(): array
    {
        return [
            'cached_count' => count($this->cache),
            'queued_count' => count($this->queue),
            'dispatched' => $this->dispatched,
        ];
    }
}
