<?php

namespace WPSecurityNinja\Plugin;

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

/**
 * 404 Guard - Bot Protection System
 * 
 * This class provides protection against bots that generate excessive 404 errors.
 * It monitors 404 requests in real-time and blocks IPs that exceed configurable thresholds.
 * 
 * Features:
 * - Real-time 404 monitoring
 * - Automatic whitelisting of search engines and crawlers
 * - Respects existing firewall whitelist
 * - Configurable thresholds and block durations
 * - Memory-optimized loading (only when enabled)
 * - Comprehensive logging for admin review
 * 
 * @author  Lars Koudal
 * @since   v0.0.1
 * @version v1.0.0  Friday, January 1st, 2021.
 */
class SN_404_Guard {
	/**
	 * Thresholds and settings
	 */
	const TRANSIENT_PREFIX = 'wf_sn_404_count_';
	const BLOCK_TRANSIENT_PREFIX = 'wf_sn_404_block_';

	public static $banned_ips = null;
	public static $monitored_ips = null;
	/**
	 * Init hook
	 */
	public static function init()
	{
		add_action('template_redirect', [__CLASS__, 'maybe_check_404'], 20);
	}

	/**
	 * Main entry: check for 404 and handle
	 */
	public static function maybe_check_404()
	{
 
		if (!self::is_enabled()) {
			return;
		}
		if (!is_404()) {
			return;
		}
    
		$ip = Wf_sn_cf::get_user_ip();
		if (!$ip) {
			return;
		}

		// Whitelist checks
		if (Wf_sn_cf::is_whitelisted_service($ip)) {
			return;
		}
		if (Wf_sn_cf::validate_crawler_ip($ip)) {
			return;
		}
		if (isset(Wf_sn_cf::$options['whitelist']) && Wf_sn_cf::is_whitelisted($ip, Wf_sn_cf::$options['whitelist'])) {
			return;
		}

		// Already blocked?
		if (get_transient(self::BLOCK_TRANSIENT_PREFIX . $ip)) {
			Wf_sn_cf::kill_request($ip, 'Blocked due to excessive 404s');
		}

		// Get firewall options
		$firewall_options = Wf_sn_cf::get_options();
		
		// Count 404s
		$count = (int) get_transient(self::TRANSIENT_PREFIX . $ip);
		$count++;
		set_transient(self::TRANSIENT_PREFIX . $ip, $count, $firewall_options['404guard_window']);

		// Log strategic events only (first 404, approaching threshold, and when blocked)
		if (secnin_fs()->can_use_premium_code__premium_only()) {
			$threshold = $firewall_options['404guard_threshold'];
			
			// Get the current URL safely
			$current_url = isset($_SERVER['REQUEST_URI']) ? esc_url_raw($_SERVER['REQUEST_URI']) : '';
			
			// Log first 404 from this IP
			if ($count === 1) {
				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event(
					'security_ninja',
					'404_guard_event',
					'First 404 detected from IP',
					['ip' => $ip, 'count' => $count, 'threshold' => $threshold, 'url' => $current_url]
				);
			}
			// Log when approaching threshold (-2 from limit)
			elseif ($count === ($threshold - 2)) {
				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event(
					'security_ninja',
					'404_guard_warning',
					'IP approaching 404 threshold',
					['ip' => $ip, 'count' => $count, 'threshold' => $threshold, 'remaining' => 2, 'url' => $current_url]
				);
			}
			// Log when at threshold -1 (final warning)
			elseif ($count === ($threshold - 1)) {
				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event(
					'security_ninja',
					'404_guard_warning',
					'IP at final warning before block',
					['ip' => $ip, 'count' => $count, 'threshold' => $threshold, 'remaining' => 1, 'url' => $current_url]
				);
			}
		}

		if ($count >= $firewall_options['404guard_threshold']) {
			set_transient(self::BLOCK_TRANSIENT_PREFIX . $ip, 1, $firewall_options['404guard_block_time']);
			if (secnin_fs()->can_use_premium_code__premium_only()) {
				// Get the current URL safely
				$current_url = isset($_SERVER['REQUEST_URI']) ? esc_url_raw($_SERVER['REQUEST_URI']) : '';
				
				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event(
					'security_ninja',
					'404_guard_block',
					'IP blocked for excessive 404s',
					['ip' => $ip, 'count' => $count, 'threshold' => $firewall_options['404guard_threshold'], 'block_duration' => $firewall_options['404guard_block_time'], 'url' => $current_url]
				);
			}
			Wf_sn_cf::kill_request($ip, 'Blocked due to excessive 404s');
		}
	}

	/**
	 * Get currently banned IPs due to 404 Guard
	 * 
	 * @return array Array of banned IPs with their block expiry times
	 */
	public static function get_currently_banned_ips()
	{
		if ( ! is_null( self::$banned_ips ) ) {
			return self::$banned_ips;
		}
		global $wpdb;
		$banned_ips = array();
		
		// Get all transients that match our block prefix
		$transient_prefix = '_transient_' . self::BLOCK_TRANSIENT_PREFIX;
		$query = $wpdb->prepare(
			"SELECT option_name, option_value FROM {$wpdb->options} 
			WHERE option_name LIKE %s AND option_value != ''",
			$transient_prefix . '%'
		);
		
		$results = $wpdb->get_results($query);
		
		foreach ($results as $row) {
			$ip = str_replace($transient_prefix, '', $row->option_name);
			
			// Get the expiry time from the _transient_timeout_ option
			$timeout_option_name = '_transient_timeout_' . self::BLOCK_TRANSIENT_PREFIX . $ip;
			$expiry_time = get_option($timeout_option_name);
			
			if ($expiry_time && $expiry_time > time()) {
				$banned_ips[$ip] = $expiry_time;
			}
		}
		self::$banned_ips = $banned_ips;
		return $banned_ips;
	}

	/**
	 * Get IPs currently being monitored (have 404 counts but not yet banned)
	 * 
	 * @return array Array of monitored IPs with their current 404 counts and expiry times
	 */
	public static function get_monitored_ips()
	{
		if ( ! is_null( self::$monitored_ips ) ) {
			return self::$monitored_ips;
		}
		global $wpdb;
		$monitored_ips = array();
		
		// Get all transients that match our count prefix
		$transient_prefix = '_transient_' . self::TRANSIENT_PREFIX;
		$query = $wpdb->prepare(
			"SELECT option_name, option_value FROM {$wpdb->options} 
			WHERE option_name LIKE %s AND option_value != ''",
			$transient_prefix . '%'
		);
		
		$results = $wpdb->get_results($query);
		$firewall_options = Wf_sn_cf::get_options();
		$threshold = $firewall_options['404guard_threshold'];
		
		foreach ($results as $row) {
			$ip = str_replace($transient_prefix, '', $row->option_name);
			$count = (int) $row->option_value;
			
			// Check if this IP is currently banned
			$block_timeout_option = '_transient_timeout_' . self::BLOCK_TRANSIENT_PREFIX . $ip;
			$ban_expiry = get_option($block_timeout_option);
			$is_banned = $ban_expiry && $ban_expiry > time();
			
			// Only include if below threshold and not banned
			if ($count > 0 && $count < $threshold && !$is_banned) {
				// Get expiry time for this count transient
				$count_timeout_option = '_transient_timeout_' . self::TRANSIENT_PREFIX . $ip;
				$expiry_time = get_option($count_timeout_option);
				$monitored_ips[$ip] = array(
					'count' => $count,
					'expiry' => $expiry_time
				);
			}
		}
		
		return $monitored_ips;
	}

	/**
	 * Get 404 Guard statistics
	 * 
	 * @return array Array with banned_count, monitored_count, expiring_soon_ips, and oldest_monitored_ips
	 */
	public static function get_statistics()
	{
		$banned_ips = self::get_currently_banned_ips();
		$monitored_ips = self::get_monitored_ips();
		
		// Get IPs expiring soon (within next 5 minutes)
		$expiring_soon = array();
		$current_time = time();
		$five_minutes = 5 * 60;
		
		foreach ($banned_ips as $ip => $expiry_time) {
			$time_until_expiry = $expiry_time - $current_time;
			if ($time_until_expiry <= $five_minutes && $time_until_expiry > 0) {
				$expiring_soon[$ip] = $time_until_expiry;
			}
		}
		
		// Sort expiring soon by time remaining (closest to expiring first)
		asort($expiring_soon);
		// Limit to top 5
		$expiring_soon = array_slice($expiring_soon, 0, 5, true);

		// Get the 5 oldest monitored IPs (closest to expiry)
		$oldest_monitored = $monitored_ips;
		// Remove any with no expiry (shouldn't happen, but just in case)
		$oldest_monitored = array_filter($oldest_monitored, function($item) { return !empty($item['expiry']); });
		// Sort by expiry ascending (oldest first)
		uasort($oldest_monitored, function($a, $b) { return $a['expiry'] <=> $b['expiry']; });
		$oldest_monitored = array_slice($oldest_monitored, 0, 5, true);

		// Get recent 404 Guard events count (last 24 hours)
		$recent_events_count = self::get_recent_events_count();
		
		// Get all-time statistics
		$all_time_stats = self::get_all_time_statistics();
		
		return array(
			'banned_count' => count($banned_ips),
			'monitored_count' => count($monitored_ips),
			'expiring_soon_ips' => $expiring_soon,
			'oldest_monitored_ips' => $oldest_monitored,
			'recent_events_count' => $recent_events_count,
			'all_time_stats' => $all_time_stats
		);
	}

	/**
	 * Get count of recent 404 Guard events (last 24 hours)
	 * 
	 * @return int Number of events in the last 24 hours
	 */
	public static function get_recent_events_count()
	{
		global $wpdb;
		
		$table_name = $wpdb->prefix . 'wf_sn_el';
		$query = $wpdb->prepare(
			"SELECT COUNT(*) FROM {$table_name} 
			WHERE module = %s AND action IN (%s, %s, %s) 
			AND timestamp >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
			'security_ninja',
			'404_guard_event',
			'404_guard_warning', 
			'404_guard_block'
		);
		
		return (int) $wpdb->get_var($query);
	}

	/**
	 * Get all-time statistics for 404 Guard
	 * 
	 * @return array Array with all-time statistics
	 */
	public static function get_all_time_statistics()
	{
		global $wpdb;
		
		$table_name = $wpdb->prefix . 'wf_sn_el';
		$event_types = array(
			'404_guard_event',
			'404_guard_warning',
			'404_guard_block'
		);
		
		$placeholders = implode(',', array_fill(0, count($event_types), '%s'));
		$query = $wpdb->prepare(
			"SELECT action, COUNT(*) as count FROM {$table_name} 
			WHERE module = %s AND action IN ({$placeholders})
			GROUP BY action",
			array_merge(array('security_ninja'), $event_types)
		);
		
		$results = $wpdb->get_results($query);
		$counts = array();
		
		foreach ($event_types as $event_type) {
			$counts[$event_type] = 0;
		}
		
		foreach ($results as $row) {
			$counts[$row->action] = (int) $row->count;
		}
		
		// Calculate totals
		$totals = array(
			'total_events' => array_sum($counts),
			'total_blocks' => $counts['404_guard_block'],
			'total_warnings' => $counts['404_guard_warning'],
			'total_detections' => $counts['404_guard_event']
		);
		
		return array_merge($counts, $totals);
	}

	/**
	 * Format time remaining in human readable format
	 * 
	 * @param int $seconds Seconds remaining
	 * @return string Formatted time string
	 */
	public static function format_time_remaining($seconds)
	{
		if ($seconds < 60) {
			return sprintf(_n('%d second', '%d seconds', $seconds, 'security-ninja'), $seconds);
		} elseif ($seconds < 3600) {
			$minutes = floor($seconds / 60);
			return sprintf(_n('%d minute', '%d minutes', $minutes, 'security-ninja'), $minutes);
		} else {
			$hours = floor($seconds / 3600);
			return sprintf(_n('%d hour', '%d hours', $hours, 'security-ninja'), $hours);
		}
	}

	/**
	 * Is the guard enabled?
	 */
	protected static function is_enabled()
	{
		$firewall_options = Wf_sn_cf::get_options();
		return !empty($firewall_options['404guard_enabled']);
	}

	/**
	 * Check if 404 Guard is loaded and enabled
	 * This can be called from other parts of the system
	 */
	public static function is_loaded_and_enabled()
	{
		return class_exists(__CLASS__) && self::is_enabled();
	}

	/**
	 * Load 404 Guard on demand
	 * This can be called from other parts of the system if needed
	 */
	public static function load_on_demand()
	{
		if (!class_exists(__CLASS__)) {
			require_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-404-guard.php';
		}
		if (!self::is_enabled()) {
			return false;
		}
		return true;
	}
} 