<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Middleware;

use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Middleware;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;

/**
 * Rate limiting middleware for sensitive API endpoints.
 *
 * Protects search, export, and report endpoints from abuse (data scraping).
 * Uses Nextcloud's distributed cache for rate tracking.
 *
 * Configurable limits per endpoint group:
 * - Search: 60 requests per minute
 * - Export: 10 requests per minute
 * - Report: 10 requests per minute
 * - Default: 120 requests per minute
 *
 * Part of Issue #64.
 */
class RateLimitMiddleware extends Middleware {

    /** Rate limits: [url_pattern => [max_requests, window_seconds]] */
    private const LIMITS = [
        '/search' => [60, 60],
        '/export/' => [10, 60],
        '/report/' => [10, 60],
        '/import/' => [5, 60],
    ];

    private const DEFAULT_LIMIT = [120, 60];

    private IRequest $request;
    private IUserSession $userSession;
    private ?ICache $cache;
    private LoggerInterface $logger;

    public function __construct(
        IRequest $request,
        IUserSession $userSession,
        ICacheFactory $cacheFactory,
        LoggerInterface $logger
    ) {
        $this->request = $request;
        $this->userSession = $userSession;
        $this->cache = $cacheFactory->isAvailable()
            ? $cacheFactory->createDistributed('mv_ratelimit')
            : null;
        $this->logger = $logger;
    }

    /**
     * Check rate limit before controller action executes.
     *
     * @throws \OCP\AppFramework\Http\TooManyRequestsException
     */
    public function beforeController($controller, $methodName): void {
        if ($this->cache === null) {
            // No cache backend available — cannot enforce rate limits
            return;
        }

        $user = $this->userSession->getUser();
        if ($user === null) {
            return;
        }

        $uri = $this->request->getRequestUri();
        [$maxRequests, $windowSeconds] = $this->getLimitForUri($uri);

        // Normalize URI by stripping query parameters to prevent bypass
        // via appending varying dummy params (e.g., ?_=1, ?_=2)
        $normalizedUri = strtok($uri, '?') ?: $uri;
        $cacheKey = 'rl_' . $user->getUID() . '_' . md5($normalizedUri);
        $current = $this->cache->get($cacheKey);

        if ($current === null) {
            $this->cache->set($cacheKey, 1, $windowSeconds);
            return;
        }

        if ($current >= $maxRequests) {
            $this->logger->warning('Rate limit exceeded', [
                'user' => $user->getUID(),
                'uri' => $uri,
                'limit' => $maxRequests,
                'window' => $windowSeconds,
                'app' => 'mitgliederverwaltung',
            ]);

            // We'll handle this in afterException
            throw new \RuntimeException('Rate limit exceeded');
        }

        $this->cache->set($cacheKey, $current + 1, $windowSeconds);
    }

    /**
     * Convert rate limit exceptions to proper HTTP 429 responses.
     */
    public function afterException($controller, $methodName, \Exception $exception): JSONResponse {
        if ($exception->getMessage() === 'Rate limit exceeded') {
            return new JSONResponse(
                ['error' => 'Zu viele Anfragen. Bitte versuchen Sie es spaeter erneut.'],
                Http::STATUS_TOO_MANY_REQUESTS
            );
        }

        throw $exception;
    }

    /**
     * Determine rate limit for a given URI.
     *
     * @return int[] [max_requests, window_seconds]
     */
    private function getLimitForUri(string $uri): array {
        foreach (self::LIMITS as $pattern => $limit) {
            if (str_contains($uri, $pattern)) {
                return $limit;
            }
        }
        return self::DEFAULT_LIMIT;
    }
}
