<?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', array( __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_Utils::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', 'security-ninja' ) );
		}

		// 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 (from 2nd 404 onward: 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 when approaching threshold (-2 from limit)
			if ( $count === ( $threshold - 2 ) ) {
				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event(
					'security_ninja',
					'404_guard_warning',
					__( 'IP approaching 404 threshold', 'security-ninja' ),
					array(
						'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', 'security-ninja' ),
					array(
						'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', 'security-ninja' ),
					array(
						'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', 'security-ninja' ) );
		}
	}

	/**
	 * 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;
	}

	/**
	 * Whether an IP is currently blocked by 404 Guard.
	 *
	 * @param string $ip IP address.
	 * @return bool
	 */
	public static function is_ip_blocked( $ip ) {
		if ( ! $ip || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return false;
		}

		$timeout_option = '_transient_timeout_' . self::BLOCK_TRANSIENT_PREFIX . $ip;
		$expiry         = get_option( $timeout_option );

		return $expiry && (int) $expiry > time();
	}

	/**
	 * Lift 404 Guard block and count transients for a single IP.
	 *
	 * @param string $ip Validated IP address.
	 * @return bool True when transients were cleared or IP was not blocked.
	 */
	public static function lift_ban_for_ip( $ip ) {
		$ip = sanitize_text_field( trim( (string) $ip ) );
		if ( '' === $ip || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return false;
		}

		delete_transient( self::BLOCK_TRANSIENT_PREFIX . $ip );
		delete_transient( self::TRANSIENT_PREFIX . $ip );
		self::$banned_ips    = null;
		self::$monitored_ips = null;

		return true;
	}

	/**
	 * Clear all 404 Guard block and count transients.
	 *
	 * @return int Number of transient groups cleared.
	 */
	public static function clear_all_blocks() {
		global $wpdb;

		$cleared = 0;

		$prefixes = array(
			self::BLOCK_TRANSIENT_PREFIX,
			self::TRANSIENT_PREFIX,
		);

		foreach ( $prefixes as $prefix ) {
			$transient_prefix = '_transient_' . $prefix;
			$query            = $wpdb->prepare(
				"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
				$transient_prefix . '%'
			);
			$results          = $wpdb->get_results( $query );

			if ( ! is_array( $results ) ) {
				continue;
			}

			foreach ( $results as $row ) {
				$option_name = $row->option_name;
				$ip          = str_replace( $transient_prefix, '', $option_name );

				if ( strpos( $option_name, '_transient_timeout_' ) === 0 ) {
					continue;
				}

				delete_transient( $prefix . $ip );
				++$cleared;
			}
		}

		self::$banned_ips    = null;
		self::$monitored_ips = null;

		return $cleared;
	}

	/**
	 * 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();

			// Get expiry time for this count transient
			$count_timeout_option = '_transient_timeout_' . self::TRANSIENT_PREFIX . $ip;
			$expiry_time          = get_option( $count_timeout_option );

			// Only include if below threshold, not banned, and monitoring window still valid
			if ( $count > 0 && $count < $threshold && ! $is_banned && $expiry_time && $expiry_time > time() ) {
				$monitored_ips[ $ip ] = array(
					'count'  => $count,
					'expiry' => $expiry_time,
				);
			} elseif ( $expiry_time && $expiry_time <= time() ) {
				// Clean up expired count transient to avoid DB bloat
				delete_option( '_transient_' . self::TRANSIENT_PREFIX . $ip );
				delete_option( $count_timeout_option );
			}
		}

		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 Wf_sn_cf::is_active() && ! 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;
	}
}
