<?php

declare(strict_types=1);

namespace SiteFort\Modules\Firewall\RateLimiter;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

use SiteFort\Includes\IpDetector;
use SiteFort\Modules\Firewall\LockoutManager;
use SiteFort\Modules\Firewall\SearchTrust\SearchTrustBanGate;

class RateLimiter {
    // Probe-volume escalation looks back this far; a scanner that rides just
    // under the per-minute 404 limit still accumulates into a ban.
    private const PROBE_WINDOW_MINUTES = 10;

    private array $settings;
    private SlidingWindowStore $store;

    public function __construct( array $settings ) {
        $this->settings = $settings;
        $this->store    = new SlidingWindowStore();
    }

    // $throttle_guard runs once the limit is exceeded; true waives the 429.
    public function check_request( string $ip, bool $safe_service_exception = false, ?callable $throttle_guard = null ): void {
        if ( empty( $this->settings['rate_limit_enabled'] ) ) {
            return;
        }

        // XML-RPC exposure is controlled by hardening, not firewall throttling.
        if ( $this->is_xmlrpc_request() ) {
            return;
        }

        if ( $safe_service_exception ) {
            return;
        }

        $ip_hash = hash( 'sha256', $ip );

        $this->store->increment( $ip_hash, 'request' );

        $limit = (int) ( $this->settings['rate_limit_requests'] ?? 60 );

        if ( $this->is_rate_limited( $ip_hash, 'request', $limit ) ) {
            if ( $throttle_guard !== null && $throttle_guard() === true ) {
                return;
            }

            $this->send_429();
        }
    }

    /**
     * Count a scanner-shaped 404 and ban on sustained probe volume.
     */
    public function record_404_probe( string $ip ): void {
        $ip_hash = hash( 'sha256', $ip );
        $limit   = (int) ( $this->settings['rate_limit_404'] ?? 10 );

        $this->store->increment( $ip_hash, '404_probe' );
        $probe_count = $this->store->get_count( $ip_hash, '404_probe', self::PROBE_WINDOW_MINUTES );

        if ( $probe_count >= max( 30, 5 * $limit ) ) {
            $this->ban_probe_flood( $ip, $probe_count );
        }
    }

    /**
     * Count an unhandled 404 and throttle above the per-minute limit.
     */
    public function enforce_404_flood( string $ip, ?callable $throttle_guard = null ): void {
        $ip_hash = hash( 'sha256', $ip );
        $limit   = (int) ( $this->settings['rate_limit_404'] ?? 10 );

        $this->store->increment( $ip_hash, '404' );

        if ( $this->is_rate_limited( $ip_hash, '404', $limit ) ) {
            if ( $throttle_guard !== null && $throttle_guard() === true ) {
                return;
            }

            $this->send_429(
                '404 Flood',
                'Too many not-found requests were detected from this IP. Please wait before trying again.'
            );
        }
    }

    // LockoutManager rejects duplicate adds; only report blocks that landed.
    private function ban_probe_flood( string $ip, int $count ): void {
        if ( ! SearchTrustBanGate::allows_auto_ban( $ip ) ) {
            return;
        }

        $result = ( new LockoutManager() )->add(
            $ip,
            'ban',
            [
                'source'   => 'rate_limit',
                'reason'   => sprintf(
                    'Scanner-pattern 404 volume: %d probe requests within %d minutes.',
                    $count,
                    self::PROBE_WINDOW_MINUTES
                ),
                'duration' => (int) ( $this->settings['ban_duration'] ?? 0 ),
            ]
        );

        if ( empty( $result['success'] ) ) {
            return;
        }

        do_action( 'sitefort_firewall_block', [
            'site_url'    => home_url(),
            'ip'          => $ip,
            'http_status' => 403,
            'attack_type' => '404 Flood',
            'request_uri' => IpDetector::get_server_value( 'REQUEST_URI' ),
        ] );
    }

    /**
     * Cleanup old rate limit entries.
     *
     * Keeps enough history for both general rate limiting and sensitive-path
     * probe escalation, which shares the same counter table.
     */
    public function cleanup(): void {
        $sensitive_minutes = max( 1, (int) ceil( ( (int) ( $this->settings['sensitive_path_window'] ?? 600 ) ) / 60 ) );
        $retention_minutes = max( 5, $sensitive_minutes + 1, self::PROBE_WINDOW_MINUTES + 1 );

        $this->store->purge_old( $retention_minutes );
    }

    private function is_rate_limited( string $ip_hash, string $hit_type, int $limit ): bool {
        $count = $this->store->get_count( $ip_hash, $hit_type, 1 );
        return $count > $limit;
    }

    private function is_xmlrpc_request(): bool {
        if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
            return true;
        }

        $request_uri = (string) IpDetector::get_server_value( 'REQUEST_URI' );
        if ( $request_uri === '' ) {
            return false;
        }

        $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
        if ( $path === '' ) {
            return false;
        }

        $normalized = preg_replace( '#/+#', '/', str_replace( '\\', '/', $path ) );
        if ( ! is_string( $normalized ) ) {
            $normalized = $path;
        }

        return strtolower( basename( $normalized ) ) === 'xmlrpc.php';
    }

    private function send_429( string $attack_type = 'Rate Limit', string $message = 'Request rate limit exceeded. Please wait before trying again.' ): void {
        if ( headers_sent() ) {
            return;
        }

        do_action( 'sitefort_firewall_block', [
            'site_url'    => home_url(),
            'ip'          => IpDetector::get_ip(),
            'http_status' => 429,
            'attack_type' => $attack_type,
            'request_uri' => IpDetector::get_server_value( 'REQUEST_URI' ),
        ] );

        http_response_code( 429 );
        header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
        header( 'Retry-After: 60' );

        require_once dirname( __DIR__ ) . '/block-page.php';
        header( 'Content-Type: text/html; charset=UTF-8' );
        sitefort_render_block_page( 429, $message, IpDetector::get_ip() );
        exit;
    }

}
