<?php
declare(strict_types=1);
namespace Aen\RateLab;
use Predis\ClientInterface;
use Symfony\Component\RateLimiter\LimiterStateInterface;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
/** Shared trusted Redis only. I/O exceptions propagate: the API answers 503, not a fresh quota. */
final class RedisStateStorage implements StorageInterface {
 public function __construct(private ClientInterface $client,private string $prefix){}
 private function key(string $id):string{return $this->prefix.':state:'.hash('sha256',$id);}
 public function fetch(string $limiterStateId):?LimiterStateInterface {
  $raw=$this->client->get($this->key($limiterStateId));if($raw===null)return null;
  if(strlen($raw)>131072)throw new \RuntimeException('Oversized state');
  $state=@unserialize($raw,['allowed_classes'=>[
   \Symfony\Component\RateLimiter\Policy\Window::class,
   \Symfony\Component\RateLimiter\Policy\SlidingWindow::class,
   \Symfony\Component\RateLimiter\Policy\TokenBucket::class,
   \Symfony\Component\RateLimiter\Policy\Rate::class,\DateInterval::class
  ]]);
  if(!$state instanceof LimiterStateInterface)throw new \RuntimeException('Invalid state');
  return $state;
 }
 public function save(LimiterStateInterface $limiterState):void {
  $ttl=$limiterState->getExpirationTime();$raw=serialize($limiterState);
  $reply=$ttl===null?$this->client->set($this->key($limiterState->getId()),$raw):$this->client->setex($this->key($limiterState->getId()),max(1,$ttl),$raw);
  if((string)$reply!=='OK')throw new \RuntimeException('State not saved');
 }
 public function delete(string $limiterStateId):void {$this->client->del([$this->key($limiterStateId)]);}
}
