<?php

namespace App\Filters;

use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;

/**
 * Rate-limits the app's first-ever anonymous/public routes (the booking
 * widget, event registration, event check-in) — every route before these
 * assumed an already-authenticated, already-trusted staff session, so
 * nothing like this existed. CodeIgniter 4 doesn't ship a ready-made
 * Throttle filter class to alias; it ships the Throttler service
 * (token-bucket, cache-backed), which this wraps into a filter.
 *
 * Usage: `'filter' => 'throttle:10,60'` = 10 requests per 60 seconds,
 * keyed by IP + route path so one budget doesn't bleed into another
 * route. Deliberately NOT added to $globals in Filters.php — applied
 * per-route-group only, so it never touches normal staff traffic.
 *
 * A route nested in a throttled group AND given its own tighter
 * 'filter' => 'throttle:X,Y' gets BOTH — CodeIgniter's RouteCollection
 * merges group and per-route filter lists rather than letting one
 * override the other (see RouteCollection::create()), so this filter
 * runs twice for that one request with two different budgets. The key
 * includes the capacity/window themselves so those two checks land on
 * separate counters instead of one shared counter fed conflicting
 * capacities — the practical effect is just "must satisfy the tighter
 * of the two", which is exactly the intent of layering a stricter
 * budget onto one route inside a looser group.
 */
class ThrottleFilter implements FilterInterface
{
    public function before(RequestInterface $request, $arguments = null)
    {
        [$capacity, $seconds] = $this->parseArguments($arguments);

        // CI4's cache key validation forbids {}()/\@: — both the request
        // path (slashes, dots) and an IPv6 address (colons) can contain
        // those, so the raw "ip_path_budget" string can't be used as-is.
        // Hashed rather than character-stripped so nothing needs to be
        // re-escaped if the path/IP shape ever changes.
        $key = 'throttle_' . md5($request->getIPAddress() . '|' . $request->getUri()->getPath() . "|{$capacity}-{$seconds}");

        if (! service('throttler')->check($key, $capacity, $seconds)) {
            return service('response')
                ->setStatusCode(429)
                ->setBody('Too many requests — please wait a moment and try again.');
        }

        return null;
    }

    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
    {
        // no-op
    }

    /** @return array{0: int, 1: int} [capacity, seconds] — defaults to a conservative 10/60 if the route omits args. */
    private function parseArguments(?array $arguments): array
    {
        $capacity = (int) ($arguments[0] ?? 10);
        $seconds  = (int) ($arguments[1] ?? 60);

        return [$capacity > 0 ? $capacity : 10, $seconds > 0 ? $seconds : 60];
    }
}
