<?php
/**
 * Firewall Engine
 *
 * Runs on every request (priority 1 on 'init') and inspects:
 *  - IP blacklist / whitelist
 *  - SQL injection patterns
 *  - XSS payloads
 *  - Malicious query strings
 *  - Known bad bots (via BotDetector)
 *  - Rate limiting (via RateLimiter)
 *
 * Uses a scoring system: each detected pattern adds points.
 * Requests exceeding the threshold are blocked outright.
 *
 * @package GuardianShield
 */

namespace GuardianShield;

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

class Firewall {

	/** Score threshold above which a request is hard-blocked. */
	private const BLOCK_SCORE = 10;

	// SQLi pattern fragments — scored by severity.
	private const SQLI_PATTERNS = [
		// High-severity — almost certain attack.
		[ 'pattern' => '/(\bunion\b.{0,30}\bselect\b)/i',                         'score' => 8 ],
		[ 'pattern' => '/(\bselect\b.{0,30}\bfrom\b.{0,30}\bwhere\b)/i',          'score' => 8 ],
		[ 'pattern' => '/(\bdrop\s+table\b)/i',                                    'score' => 10 ],
		[ 'pattern' => '/(\binsert\s+into\b.{0,30}\bvalues\b)/i',                  'score' => 7 ],
		[ 'pattern' => '/(\bupdate\b.{0,30}\bset\b.{0,30}\bwhere\b)/i',            'score' => 7 ],
		[ 'pattern' => '/\bexec\s*\(/i',                                           'score' => 9 ],
		[ 'pattern' => '/\bxp_cmdshell\b/i',                                       'score' => 10 ],
		// Medium — classic bypass tricks.
		[ 'pattern' => '/(\'\s*or\s*\'?\d+\'?\s*=\s*\'?\d+\'?)/i',               'score' => 6 ],
		[ 'pattern' => '/(--\s*$|;--)/m',                                          'score' => 4 ],
		[ 'pattern' => '/\bsleep\s*\(\s*\d+\s*\)/i',                              'score' => 6 ],
		[ 'pattern' => '/\bbenchmark\s*\(/i',                                      'score' => 6 ],
		[ 'pattern' => '/\bload_file\s*\(/i',                                      'score' => 7 ],
		[ 'pattern' => '/\binto\s+(outfile|dumpfile)\b/i',                         'score' => 9 ],
		// Information gathering.
		[ 'pattern' => '/\binformation_schema\b/i',                                'score' => 5 ],
		[ 'pattern' => '/\bsys\.tables\b/i',                                       'score' => 5 ],
	];

	// XSS pattern fragments.
	private const XSS_PATTERNS = [
		[ 'pattern' => '/<script[\s>]/i',                                          'score' => 8 ],
		[ 'pattern' => '/javascript\s*:/i',                                        'score' => 7 ],
		[ 'pattern' => '/on(load|error|click|mouse\w+|key\w+)\s*=/i',             'score' => 7 ],
		[ 'pattern' => '/<iframe\b/i',                                             'score' => 6 ],
		[ 'pattern' => '/<object\b/i',                                             'score' => 5 ],
		[ 'pattern' => '/expression\s*\(/i',                                       'score' => 6 ],
		[ 'pattern' => '/vbscript\s*:/i',                                          'score' => 7 ],
		[ 'pattern' => '/data\s*:\s*text\/html/i',                                 'score' => 5 ],
		[ 'pattern' => '/&#x[0-9a-f]{2,4};/i',                                    'score' => 3 ],
		[ 'pattern' => '/%3cscript/i',                                             'score' => 8 ],
	];

	// Malicious query-string / path patterns.
	private const PATH_PATTERNS = [
		[ 'pattern' => '/\.\.\//',                                                 'score' => 8,  'name' => 'path_traversal' ],
		[ 'pattern' => '/\.\.\\\\/',                                               'score' => 8,  'name' => 'path_traversal_win' ],
		[ 'pattern' => '/%00/',                                                    'score' => 7,  'name' => 'null_byte' ],
		[ 'pattern' => '/\beval\s*\(/i',                                           'score' => 7,  'name' => 'eval_call' ],
		[ 'pattern' => '/\bbase64_decode\s*\(/i',                                  'score' => 6,  'name' => 'base64_decode' ],
		[ 'pattern' => '/\bpassthru\s*\(/i',                                       'score' => 9,  'name' => 'shell_passthru' ],
		[ 'pattern' => '/\bsystem\s*\(/i',                                         'score' => 9,  'name' => 'shell_system' ],
		[ 'pattern' => '/\bshell_exec\s*\(/i',                                     'score' => 9,  'name' => 'shell_exec' ],
		[ 'pattern' => '/\bproc_open\s*\(/i',                                      'score' => 9,  'name' => 'proc_open' ],
		[ 'pattern' => '/\bpopen\s*\(/i',                                          'score' => 8,  'name' => 'popen' ],
		[ 'pattern' => '/etc\/passwd/i',                                           'score' => 8,  'name' => 'etc_passwd' ],
		[ 'pattern' => '/\/proc\/self\//i',                                        'score' => 7,  'name' => 'proc_self' ],
	];

	private Logger      $logger;
	private RateLimiter $rate_limiter;
	private BotDetector $bot_detector;
	private bool        $enabled;

	public function __construct( Logger $logger, RateLimiter $rate_limiter, BotDetector $bot_detector ) {
		$this->logger       = $logger;
		$this->rate_limiter = $rate_limiter;
		$this->bot_detector = $bot_detector;
		$this->enabled      = (bool) gs_get_option( 'firewall_enabled', true );
	}

	// ── Main Entry Point ─────────────────────────
	public function run(): void {
		if ( ! $this->enabled ) {
			return;
		}

		// 1. Get and normalise the real client IP.
		$ip = gs_get_client_ip();

		// 2. Whitelisted IPs bypass all checks.
		if ( $this->is_whitelisted( $ip ) ) {
			return;
		}

		// 3. Hard-blocked IPs — instant rejection.
		if ( $this->is_blacklisted( $ip ) ) {
			$this->block_request( $ip, 'ip_blacklist', 'IP is on the blacklist.', 10 );
		}

		// 4. Rate limiting check.
		if ( ! $this->rate_limiter->check_and_record( $ip ) ) {
			$this->block_request( $ip, 'rate_limit', 'Too many requests.', 8 );
		}

		// 5. Bad bot check.
		if ( $this->bot_detector->is_bad_bot() ) {
			$this->block_request( $ip, 'bad_bot', 'Known malicious bot user agent.', 7 );
		}

		// 6. Inspect all request data.
		$score    = 0;
		$matched  = [];
		$payload  = $this->collect_request_data();

		foreach ( $payload as $source => $value ) {
			$result = $this->inspect( $value );
			if ( $result['score'] > 0 ) {
				$score   += $result['score'];
				$matched  = array_merge( $matched, $result['rules'] );
			}
		}

		if ( $score >= self::BLOCK_SCORE ) {
			$rules_str = implode( ', ', array_unique( $matched ) );
			$this->block_request(
				$ip,
				'firewall',
				sprintf( 'Threat score %d — rules matched: %s', $score, $rules_str ),
				min( 10, (int) ceil( $score / 2 ) )
			);
		} elseif ( $score > 0 ) {
			// Low-score — log but allow (suspicious but not conclusive).
			$this->logger->log_attack( [
				'ip_address'   => $ip,
				'attack_type'  => 'suspicious_request',
				'severity'     => 1,
				'description'  => sprintf( 'Low threat score %d — rules: %s', $score, implode( ', ', $matched ) ),
				'request_uri'  => $_SERVER['REQUEST_URI'] ?? '',
				'user_agent'   => $_SERVER['HTTP_USER_AGENT'] ?? '',
				'blocked'      => 0,
				'threat_score' => $score,
			] );
		}
	}

	// ── Pattern Inspection ───────────────────────
	/**
	 * Run all pattern groups against a given string.
	 * Returns [ 'score' => int, 'rules' => string[] ].
	 */
	private function inspect( string $value ): array {
		if ( empty( $value ) ) {
			return [ 'score' => 0, 'rules' => [] ];
		}

		// URL-decode twice to catch double-encoded payloads.
		$decoded = urldecode( urldecode( $value ) );

		$score = 0;
		$rules = [];

		$check_patterns = function ( array $patterns, string $group ) use ( $decoded, &$score, &$rules ): void {
			foreach ( $patterns as $entry ) {
				if ( preg_match( $entry['pattern'], $decoded ) ) {
					$score += $entry['score'];
					$rules[] = $entry['name'] ?? $group;
				}
			}
		};

		if ( gs_get_option( 'block_sqli', true ) ) {
			$check_patterns( self::SQLI_PATTERNS, 'sqli' );
		}
		if ( gs_get_option( 'block_xss', true ) ) {
			$check_patterns( self::XSS_PATTERNS, 'xss' );
		}

		// Path patterns always checked.
		$check_patterns( self::PATH_PATTERNS, 'path' );

		return [ 'score' => $score, 'rules' => $rules ];
	}

	// ── Request Data Collection ──────────────────
	/**
	 * Gathers all user-supplied data from the request.
	 * $_COOKIE is intentionally excluded from deep inspection
	 * to avoid false positives on WP session cookies.
	 */
	private function collect_request_data(): array {
		$data = [];

		// Query string and POST body.
		foreach ( array_merge( $_GET, $_POST ) as $key => $value ) {
			$data[ 'param_' . $key ] = is_array( $value )
				? implode( ' ', $value )
				: (string) $value;
		}

		// Request URI path.
		$data['request_uri'] = urldecode( $_SERVER['REQUEST_URI'] ?? '' );

		// User-Agent and Referer can carry XSS payloads.
		$data['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
		$data['referer']    = $_SERVER['HTTP_REFERER'] ?? '';

		return $data;
	}

	// ── IP Rule Checks ───────────────────────────
	private function is_whitelisted( string $ip ): bool {
		return $this->check_ip_rule( $ip, 'whitelist' );
	}

	private function is_blacklisted( string $ip ): bool {
		return $this->check_ip_rule( $ip, 'blacklist' )
			|| $this->check_ip_rule( $ip, 'temp_block' );
	}

	private function check_ip_rule( string $ip, string $type ): bool {
		global $wpdb;

		$cache_key = "gs_ip_{$type}_{$ip}";
		$cached    = get_transient( $cache_key );

		if ( false !== $cached ) {
			return (bool) $cached;
		}

		$table = $wpdb->prefix . 'gs_ip_rules';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id, expires_at
				   FROM `{$table}`
				  WHERE ip_address = %s
				    AND rule_type  = %s
				  LIMIT 1",
				$ip,
				$type
			)
		);

		if ( ! $row ) {
			set_transient( $cache_key, 0, 60 );
			return false;
		}

		// Check if a temp block has expired.
		if ( $row->expires_at && strtotime( $row->expires_at ) < time() ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->delete( $table, [ 'id' => $row->id ], [ '%d' ] );
			set_transient( $cache_key, 0, 60 );
			return false;
		}

		set_transient( $cache_key, 1, 60 );
		return true;
	}

	// ── Block & Terminate ────────────────────────
	/**
	 * Logs the attack, then sends a 403 and exits.
	 * This is the only point where we terminate execution.
	 */
	private function block_request(
		string $ip,
		string $type,
		string $reason,
		int    $severity = 3
	): never {
		$uri        = $_SERVER['REQUEST_URI'] ?? '';
		$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';

		$this->logger->log_attack( [
			'ip_address'   => $ip,
			'attack_type'  => $type,
			'severity'     => $severity,
			'description'  => $reason,
			'request_uri'  => $uri,
			'user_agent'   => $user_agent,
			'blocked'      => 1,
			'threat_score' => $severity * 10,
		] );

		// Log to firewall events table too.
		$this->logger->log_firewall_event( [
			'ip_address'   => $ip,
			'rule_matched' => $type,
			'action'       => 'block',
			'request_uri'  => $uri,
			'payload'      => $reason,
		] );

		// Trigger alert for critical attacks.
		if ( $severity >= 3 ) {
			do_action( 'gs_critical_attack_detected', $ip, $type, $reason );
		}

		// Standard WordPress 403 response.
		wp_die(
			esc_html__( 'Access Denied — Guardian Shield has blocked this request.', 'guardian-shield' ),
			esc_html__( '403 Forbidden', 'guardian-shield' ),
			[ 'response' => 403, 'back_link' => false ]
		);
	}

	// ── Public API ───────────────────────────────

	/** Temporarily block an IP (e.g., after brute force). */
	public function temp_block_ip( string $ip, int $minutes = 30, string $reason = '' ): void {
		global $wpdb;

		$table = $wpdb->prefix . 'gs_ip_rules';
		$wpdb->replace(
			$table,
			[
				'ip_address' => $ip,
				'rule_type'  => 'temp_block',
				'reason'     => $reason ?: 'Automatic block',
				'expires_at' => gmdate( 'Y-m-d H:i:s', time() + ( $minutes * 60 ) ),
				'created_by' => null,
			],
			[ '%s', '%s', '%s', '%s', '%d' ]
		);

		// Bust the cache.
		delete_transient( "gs_ip_temp_block_{$ip}" );
	}

	/** Permanently add to blacklist. */
	public function blacklist_ip( string $ip, string $reason = '', int $user_id = 0 ): bool {
		global $wpdb;

		$table = $wpdb->prefix . 'gs_ip_rules';
		$result = $wpdb->replace(
			$table,
			[
				'ip_address' => $ip,
				'rule_type'  => 'blacklist',
				'reason'     => $reason,
				'expires_at' => null,
				'created_by' => $user_id ?: null,
			],
			[ '%s', '%s', '%s', '%s', '%d' ]
		);

		delete_transient( "gs_ip_blacklist_{$ip}" );
		return false !== $result;
	}

	/** Add an IP to the whitelist. */
	public function whitelist_ip( string $ip, string $reason = '', int $user_id = 0 ): bool {
		global $wpdb;

		$table  = $wpdb->prefix . 'gs_ip_rules';
		$result = $wpdb->replace(
			$table,
			[
				'ip_address' => $ip,
				'rule_type'  => 'whitelist',
				'reason'     => $reason,
				'expires_at' => null,
				'created_by' => $user_id ?: null,
			],
			[ '%s', '%s', '%s', '%s', '%d' ]
		);

		delete_transient( "gs_ip_whitelist_{$ip}" );
		return false !== $result;
	}
}
