<?php

declare(strict_types=1);

namespace SiteFort\Modules\Firewall;

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

use SiteFort\Includes\IpDetector;
use SiteFort\Includes\SitePathResolver;
use SiteFort\Modules\Firewall\RateLimiter\SlidingWindowStore;

/**
 * Application-level sensitive path probe detector.
 *
 * Blocks requests to sensitive files (.env, .git, wp-config backups, plugin/theme
 * readme.txt, etc.) at the PHP level. This is the belt-and-suspenders complement
 * to the server-level .htaccess/nginx rules written by SystemTweaks.
 *
 * Uses bounded event logging: every sensitive path request is denied with 403,
 * but only the first probe and the threshold escalation inside a configured
 * window are written to the firewall event table. These rows are written
 * directly so notifications, cloud state pushes, and external block sync hooks
 * are not triggered by high-volume probe traffic.
 *
 */
class SensitivePathDetector {

    /**
     * Path-independent probe signatures.
     *
     * @var string[]
     */
    private const STATIC_PATTERNS = [
        // Dotfiles and VCS directories.
        '#/\.env($|\.)#i',
        '#/\.git(/|$)#i',
        '#/\.svn(/|$)#i',
        '#/\.hg(/|$)#i',
        '#/\.htaccess$#i',
        '#/\.htpasswd$#i',
        '#/\.DS_Store$#i',
        '#/\.user\.ini$#i',

        // WordPress config backups / variants.
        '#/wp-config\.(php\.bak|old|txt|php~|save|orig|copy|php\.swp)$#i',
        '#/wp-config\.php\.[\d]+$#i',
    ];

    /**
     * Number of probes from one IP before the block action fires.
     *
     * @var int
     */
    private int $threshold;

    /**
     * Time window in seconds within which probes must occur.
     *
     * @var int
     */
    private int $window;

    /**
     * Ban duration in days.
     *
     * @var int
     */
    private int $ban_duration;

    private SlidingWindowStore $store;

    /**
     * @param int $threshold    Probe count before escalating to an IP ban.
     * @param int $window       Time window in seconds (default 300 = 5 minutes).
     * @param int $ban_duration Ban duration in days.
     */
    public function __construct( int $threshold = 3, int $window = 300, int $ban_duration = 0 ) {
        $this->threshold    = max( 1, $threshold );
        $this->window       = max( 60, $window );
        $this->ban_duration = max( 0, $ban_duration );
        $this->store        = new SlidingWindowStore();
    }

    public function check_request(): void {
        if ( $this->matches_request() ) {
            $this->enforce_match();
        }
    }

    public function matches_request(): bool {
        $uri = $this->get_request_path();
        if ( $uri === '' ) {
            return false;
        }

        foreach ( $this->get_patterns() as $pattern ) {
            if ( preg_match( $pattern, $uri ) === 1 ) {
                return true;
            }
        }

        return false;
    }

    public function enforce_match(): void {
        $uri = $this->get_request_path();
        if ( $uri === '' ) {
            return;
        }

        $this->handle_probe( $uri );
    }

    /**
     * Build request-path-aware patterns for the current site layout.
     *
     * @return string[]
     */
    private function get_patterns(): array {
        return self::build_bootstrap_patterns();
    }

    /** @return string[] */
    public static function build_bootstrap_patterns(): array {
        $patterns    = self::STATIC_PATTERNS;
        $site_root   = preg_quote( SitePathResolver::get_site_root_path(), '#' );
        $content_root = SitePathResolver::get_content_public_path( '' );
        $backup_exts = '(?:sql|sql\.gz|sql\.bz2|zip|tar|tar\.gz)';

        $patterns[] = '#^' . $site_root . '(debug\.log|error_log|error\.log)$#i';
        $patterns[] = '#^' . $site_root . 'phpinfo\.php$#i';
        $patterns[] = '#^' . $site_root . 'readme\.html?$#i';

        $install_path = SitePathResolver::get_admin_public_path( 'install.php' );
        if ( is_string( $install_path ) && $install_path !== '' ) {
            $patterns[] = self::build_exact_path_pattern( $install_path );
        }

        $patterns[] = '#^' . $site_root . '[^/]+\.' . $backup_exts . '$#i';
        $patterns[] = '#^' . $site_root . '(?:backup|backups|db-backup|db-backups|database-backups|dump|dumps)/[^?]+\.' . $backup_exts . '$#i';

        if ( is_string( $content_root ) && $content_root !== '' ) {
            $content_regex = preg_quote( $content_root, '#' );
            $patterns[]    = '#^' . $content_regex . '(plugins|themes)/[^/]+/readme\.txt$#i';
            $patterns[]    = '#^' . $content_regex . '(?:.*)?\.' . $backup_exts . '$#i';
        }

        return array_values( array_unique( array_filter( $patterns, 'is_string' ) ) );
    }

    private static function build_exact_path_pattern( string $path ): string {
        return '#^' . preg_quote( SitePathResolver::normalize_request_path( $path, false ), '#' ) . '$#i';
    }

    /**
     * Handle a detected sensitive path probe.
     *
     * Always returns 403 for the request. Increments the per-IP counter, writes
     * bounded DB-only traffic log rows, and escalates the source IP into the ban
     * list once the threshold is reached.
     */
    private function handle_probe( string $uri ): void {
        $ip    = IpDetector::get_ip();
        $count = $this->increment_probe_count( $ip );

        if ( $count === 1 && $this->threshold > 1 ) {
            $this->log_sensitive_path_event( $ip, $uri, 'Sensitive Path' );
        }

        if ( $count === $this->threshold ) {
            $this->ban_ip( $ip, $count, $uri );
            $this->log_sensitive_path_event( $ip, $uri, 'Sensitive Path Escalation' );
        }

        // Always deny access to sensitive files regardless of threshold.
        if ( ! headers_sent() ) {
            http_response_code( 403 );
            header( 'Content-Type: text/html; charset=UTF-8' );
            header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
        }
        require_once __DIR__ . '/block-page.php';
        sitefort_render_block_page( 403, 'This request was blocked due to suspicious activity.', IpDetector::get_ip() );
        exit;
    }

    /**
     * Write a DB-only firewall traffic row without firing notification/cloud hooks.
     */
    private function log_sensitive_path_event( string $ip, string $uri, string $attack_type ): void {
        FirewallLogger::log( [
            'site_url'    => home_url(),
            'ip'          => $ip,
            'attack_type' => $attack_type,
            'request_uri' => $uri,
        ] );
    }

    /**
     * Add the probing IP to the structured firewall lockout list.
     */
    private function ban_ip( string $ip, int $count, string $uri ): void {
        if ( $ip === '' ) {
            return;
        }

        $manager = new LockoutManager();
        $manager->add(
            $ip,
            'ban',
            [
                'source'   => 'sensitive_path',
                'reason'   => sprintf(
                    'Sensitive path probe threshold reached (%d requests within %d seconds). Last URI: %s',
                    $count,
                    $this->window,
                    substr( $uri, 0, 190 )
                ),
                'duration' => $this->ban_duration,
            ]
        );
    }

    /**
     * Increment and return the probe count for an IP within the configured window.
     */
    private function increment_probe_count( string $ip ): int {
        $ip_hash = hash( 'sha256', $ip );
        $minutes = max( 1, (int) ceil( $this->window / 60 ) );

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

        return $this->store->get_count( $ip_hash, 'sensitive_path', $minutes );
    }

    /**
     * Extract the path component from the request URI.
     */
    private function get_request_path(): string {
        $uri = filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_UNSAFE_RAW );
        $uri = is_string( $uri ) ? wp_unslash( $uri ) : '';
        if ( $uri === '' ) {
            return '';
        }

        // Strip query string.
        $path = wp_parse_url( $uri, PHP_URL_PATH );
        if ( ! is_string( $path ) || $path === '' ) {
            return '';
        }

        return SitePathResolver::normalize_request_path( $path, false );
    }
}
