<?php

namespace Velis\RateLimiter;

use Velis\Cache\CacheInterface;

/**
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 */
final class LeakyBucketRateLimiter implements LeakyBucketRateLimiterInterface
{
    private string $identifier = '';

    public function __construct(
        private readonly CacheInterface $cache,
        private readonly string $prefix = 'rate-limiter-',
    ) {
    }

    /**
     * @inheritDoc
     */
    public function setIdentifier(string $identifier): self
    {
        $this->identifier = $identifier;

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function attempt(string $key, int $rateLimit, float $leakRate = 1, int $decaySeconds = 60): void
    {
        if ($this->isRateLimitExceeded($key, $rateLimit, $leakRate)) {
            throw new TooManyAttemptsException();
        }

        $this->hit($key, $leakRate, $decaySeconds);
    }

    /**
     * @inheritDoc
     */
    public function attemptByIdentifier(
        int $rateLimit,
        float $leakRate = 1,
        string $key = '',
        int $decaySeconds = 60,
    ): void {
        $this->attempt("$this->identifier-$key", $rateLimit, $leakRate, $decaySeconds);
    }

    /**
     * @inheritDoc
     */
    public function isRateLimitExceeded(string $key, int $rateLimit, float $leakRate = 1): bool
    {
        $this->leak($key, $leakRate);
        return $this->attempts($key) >= $rateLimit;
    }

    /**
     * @inheritDoc
     */
    public function hit(string $key, float $leakRate = 1, int $decaySeconds = 60): void
    {
        $this->leak($key, $leakRate);
        $this->increment($key, $decaySeconds);
    }

    public function increment(string $key, int $decaySeconds = 60, int $amount = 1): void
    {
        $cleanKey = $this->getCleanKey($key);
        $currentAttempts = $this->attempts($key);
        $this->cache->set(
            key: $cleanKey,
            value: $currentAttempts + $amount,
            ttl: $decaySeconds
        );
    }

    /**
     * @inheritDoc
     */
    public function attempts(string $key): int
    {
        $cleanKey = $this->getCleanKey($key);
        return $this->cache->get($cleanKey, 0) ?? 0;
    }

    private function leak(string $key, float $leakRate): void
    {
        if ($leakRate <= 0) {
            return;
        }

        $cleanKey = $this->getCleanKey($key);
        $lastLeakTimeKey = $cleanKey . '-leak-time';
        $lastLeakTime = $this->cache->get($lastLeakTimeKey);
        $currentTime = microtime(true);

        if (!$lastLeakTime) {
            $this->cache->set($lastLeakTimeKey, $currentTime, 3600);
            $lastLeakTime = $currentTime;
        }

        $elapsedTime = $currentTime - $lastLeakTime;
        $leaked = (int) floor($elapsedTime * $leakRate);
        $currentAttempts = $this->attempts($key);

        if ($leaked > 0) {
            $newAttempts = max(0, $currentAttempts - $leaked);
            $this->cache->set($cleanKey, $newAttempts, 3600);
            $this->cache->set($lastLeakTimeKey, $currentTime, 3600);
        }
    }

    public function getCleanKey(string $key): string
    {
        $key = str_replace(':', '-', $key);

        $clean = preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities($key));

        return $this->prefix . md5($clean);
    }
}
