<?php

declare(strict_types=1);

namespace App\Framework\Idempotency;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\IdempotencyKey;

/**
 * Generic Idempotency Service
 *
 * Ensures that duplicate requests with the same idempotency key
 * return the same cached result without re-executing the operation.
 *
 * Use Cases:
 * - Preventing duplicate form submissions
 * - Handling network retries safely
 * - Ensuring at-most-once execution of critical actions
 * - Payment processing deduplication
 * - Queue job deduplication
 * - Webhook deduplication
 *
 * Flow:
 * 1. Check if idempotency key exists in cache
 * 2. If exists: Return cached result
 * 3. If not: Execute operation, cache result, return
 *
 * Usage:
 * ```php
 * $result = $service->execute(
 *     key: $idempotencyKey,
 *     operation: fn() => $this->processPayment($amount),
 *     ttl: Duration::fromMinutes(10)
 * );
 * ```
 */
final readonly class IdempotencyService
{
    public function __construct(
        private Cache $cache
    ) {
    }

    /**
     * Execute operation with idempotency protection
     *
     * @template T
     * @param IdempotencyKey $key Idempotency key for this operation
     * @param callable(): T $operation Operation to execute
     * @param Duration $ttl How long to cache the result
     * @return T Cached or fresh result
     */
    public function execute(
        IdempotencyKey $key,
        callable $operation,
        Duration $ttl
    ): mixed {
        $cacheKey = CacheKey::fromString($key->toStorageKey());

        // Check if we have a cached result
        $cached = $this->cache->get($cacheKey);

        if ($cached !== null) {
            // Return cached result (idempotent - same key, same result)
            return $cached->value;
        }

        // Execute operation
        $result = $operation();

        // Cache the result
        $cacheItem = CacheItem::forSetting(
            key: $cacheKey,
            value: $result,
            ttl: $ttl
        );

        $this->cache->set($cacheItem);

        return $result;
    }

    /**
     * Check if idempotency key has been processed
     *
     * @return bool True if key exists in cache (operation already executed)
     */
    public function hasBeenProcessed(IdempotencyKey $key): bool
    {
        $cacheKey = CacheKey::fromString($key->toStorageKey());

        return $this->cache->has($cacheKey);
    }

    /**
     * Get cached result for idempotency key
     *
     * @template T
     * @return T|null Cached result or null if not found
     */
    public function getCachedResult(IdempotencyKey $key): mixed
    {
        $cacheKey = CacheKey::fromString($key->toStorageKey());
        $cached = $this->cache->get($cacheKey);

        return $cached?->value;
    }

    /**
     * Store result for idempotency key
     *
     * Useful for pre-caching results or storing results from external operations.
     *
     * @param IdempotencyKey $key Idempotency key
     * @param mixed $result Result to cache
     * @param Duration $ttl Cache TTL
     */
    public function storeResult(
        IdempotencyKey $key,
        mixed $result,
        Duration $ttl
    ): void {
        $cacheKey = CacheKey::fromString($key->toStorageKey());

        $cacheItem = CacheItem::forSetting(
            key: $cacheKey,
            value: $result,
            ttl: $ttl
        );

        $this->cache->set($cacheItem);
    }

    /**
     * Invalidate idempotency key
     *
     * Useful for testing or admin operations.
     */
    public function invalidate(IdempotencyKey $key): void
    {
        $cacheKey = CacheKey::fromString($key->toStorageKey());
        $this->cache->forget($cacheKey);
    }

    /**
     * Execute with automatic retry handling
     *
     * If operation fails, returns cached result if available.
     * If no cached result and operation fails, throws exception.
     *
     * @template T
     * @param IdempotencyKey $key Idempotency key
     * @param callable(): T $operation Operation to execute
     * @param Duration $ttl Cache TTL
     * @return T Result
     * @throws \Throwable If operation fails and no cached result exists
     */
    public function executeWithRetry(
        IdempotencyKey $key,
        callable $operation,
        Duration $ttl
    ): mixed {
        // Check cache first
        $cached = $this->getCachedResult($key);

        if ($cached !== null) {
            return $cached;
        }

        try {
            // Execute operation
            $result = $operation();

            // Cache result
            $this->storeResult($key, $result, $ttl);

            return $result;
        } catch (\Throwable $e) {
            // Operation failed - check if we have stale cache
            $cached = $this->getCachedResult($key);

            if ($cached !== null) {
                // Return stale cache instead of failing
                return $cached;
            }

            // No cached result - re-throw exception
            throw $e;
        }
    }
}
