<?php

namespace KrokedilKlarnaPaymentsDeps\Firebase\JWT;

use ArrayAccess;
use InvalidArgumentException;
use LogicException;
use OutOfBoundsException;
use KrokedilKlarnaPaymentsDeps\Psr\Cache\CacheItemInterface;
use KrokedilKlarnaPaymentsDeps\Psr\Cache\CacheItemPoolInterface;
use KrokedilKlarnaPaymentsDeps\Psr\Http\Client\ClientInterface;
use KrokedilKlarnaPaymentsDeps\Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
use UnexpectedValueException;
/**
 * @implements ArrayAccess<string, Key>
 */
class CachedKeySet implements ArrayAccess
{
    /**
     * @var string
     */
    private $jwksUri;
    /**
     * @var ClientInterface
     */
    private $httpClient;
    /**
     * @var RequestFactoryInterface
     */
    private $httpFactory;
    /**
     * @var CacheItemPoolInterface
     */
    private $cache;
    /**
     * @var ?int
     */
    private $expiresAfter;
    /**
     * @var ?CacheItemInterface
     */
    private $cacheItem;
    /**
     * @var array<string, array<mixed>>
     */
    private $keySet;
    /**
     * @var string
     */
    private $cacheKey;
    /**
     * @var string
     */
    private $cacheKeyPrefix = 'jwks';
    /**
     * @var int
     */
    private $maxKeyLength = 64;
    /**
     * @var bool
     */
    private $rateLimit;
    /**
     * @var string
     */
    private $rateLimitCacheKey;
    /**
     * @var int
     */
    private $maxCallsPerMinute = 10;
    /**
     * @var string|null
     */
    private $defaultAlg;
    public function __construct(string $jwksUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, ?int $expiresAfter = null, bool $rateLimit = \false, ?string $defaultAlg = null)
    {
        $this->jwksUri = $jwksUri;
        $this->httpClient = $httpClient;
        $this->httpFactory = $httpFactory;
        $this->cache = $cache;
        $this->expiresAfter = $expiresAfter;
        $this->rateLimit = $rateLimit;
        $this->defaultAlg = $defaultAlg;
        $this->setCacheKeys();
    }
    /**
     * @param string $keyId
     * @return Key
     */
    public function offsetGet($keyId) : Key
    {
        if (!$this->keyIdExists($keyId)) {
            throw new OutOfBoundsException('Key ID not found');
        }
        return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
    }
    /**
     * @param string $keyId
     * @return bool
     */
    public function offsetExists($keyId) : bool
    {
        return $this->keyIdExists($keyId);
    }
    /**
     * @param string $offset
     * @param Key $value
     */
    public function offsetSet($offset, $value) : void
    {
        throw new LogicException('Method not implemented');
    }
    /**
     * @param string $offset
     */
    public function offsetUnset($offset) : void
    {
        throw new LogicException('Method not implemented');
    }
    /**
     * @return array<mixed>
     */
    private function formatJwksForCache(string $jwks) : array
    {
        $jwks = \json_decode($jwks, \true);
        if (!isset($jwks['keys'])) {
            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
        }
        if (empty($jwks['keys'])) {
            throw new InvalidArgumentException('JWK Set did not contain any keys');
        }
        $keys = [];
        foreach ($jwks['keys'] as $k => $v) {
            $kid = isset($v['kid']) ? $v['kid'] : $k;
            $keys[(string) $kid] = $v;
        }
        return $keys;
    }
    private function keyIdExists(string $keyId) : bool
    {
        if (null === $this->keySet) {
            $item = $this->getCacheItem();
            // Try to load keys from cache
            if ($item->isHit()) {
                // item found! retrieve it
                $this->keySet = $item->get();
                // If the cached item is a string, the JWKS response was cached (previous behavior).
                // Parse this into expected format array<kid, jwk> instead.
                if (\is_string($this->keySet)) {
                    $this->keySet = $this->formatJwksForCache($this->keySet);
                }
            }
        }
        if (!isset($this->keySet[$keyId])) {
            if ($this->rateLimitExceeded()) {
                return \false;
            }
            $request = $this->httpFactory->createRequest('GET', $this->jwksUri);
            $jwksResponse = $this->httpClient->sendRequest($request);
            if ($jwksResponse->getStatusCode() !== 200) {
                throw new UnexpectedValueException(\sprintf('HTTP Error: %d %s for URI "%s"', $jwksResponse->getStatusCode(), $jwksResponse->getReasonPhrase(), $this->jwksUri), $jwksResponse->getStatusCode());
            }
            $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
            if (!isset($this->keySet[$keyId])) {
                return \false;
            }
            $item = $this->getCacheItem();
            $item->set($this->keySet);
            if ($this->expiresAfter) {
                $item->expiresAfter($this->expiresAfter);
            }
            $this->cache->save($item);
        }
        return \true;
    }
    private function rateLimitExceeded() : bool
    {
        if (!$this->rateLimit) {
            return \false;
        }
        $cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
        $cacheItemData = [];
        if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
            $cacheItemData = $data;
        }
        $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
        $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
        if (++$callsPerMinute > $this->maxCallsPerMinute) {
            return \true;
        }
        $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
        $cacheItem->expiresAt($expiry);
        $this->cache->save($cacheItem);
        return \false;
    }
    private function getCacheItem() : CacheItemInterface
    {
        if (\is_null($this->cacheItem)) {
            $this->cacheItem = $this->cache->getItem($this->cacheKey);
        }
        return $this->cacheItem;
    }
    private function setCacheKeys() : void
    {
        if (empty($this->jwksUri)) {
            throw new RuntimeException('JWKS URI is empty');
        }
        // ensure we do not have illegal characters
        $key = \preg_replace('|[^a-zA-Z0-9_\\.!]|', '', $this->jwksUri);
        // add prefix
        $key = $this->cacheKeyPrefix . $key;
        // Hash keys if they exceed $maxKeyLength of 64
        if (\strlen($key) > $this->maxKeyLength) {
            $key = \substr(\hash('sha256', $key), 0, $this->maxKeyLength);
        }
        $this->cacheKey = $key;
        if ($this->rateLimit) {
            // add prefix
            $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
            // Hash keys if they exceed $maxKeyLength of 64
            if (\strlen($rateLimitKey) > $this->maxKeyLength) {
                $rateLimitKey = \substr(\hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
            }
            $this->rateLimitCacheKey = $rateLimitKey;
        }
    }
}
