<?php
/**
 * Shared public request guards for widget AJAX handlers.
 *
 * @since      0.14.322
 * @package    SiteStaffr
 * @subpackage SiteStaffr/public
 */

/**
 * SiteStaffr Public Request Guards class.
 *
 * Keeps repeated nonce/honeypot/rate-limit/ban checks consistent across
 * public AJAX handlers without changing endpoint contracts.
 *
 * Usage pattern — callers MUST call verify_nonce() at the top of the handler.
 * This dies automatically on failure. Field accessors then read verified POST data.
 *
 * @since      0.14.322
 * @package    SiteStaffr
 * @subpackage SiteStaffr/public
 * @author     SiteStaffr
 */
class SiteStaffr_Public_Request_Guards {

	/**
	 * Nonce action for the current request.
	 *
	 * @since 1.4.16
	 * @var string
	 */
	private static $nonce_action = '';

	/**
	 * Nonce field name for the current request.
	 *
	 * @since 1.4.16
	 * @var string
	 */
	private static $nonce_field = '';

	/**
	 * Verify a WordPress nonce from POST payload.
	 *
	 * Dies automatically on failure (check_ajax_referer default behavior).
	 * Must be called at the top of every AJAX handler before any field access.
	 *
	 * @since 1.4.16
	 *
	 * @param string $action Nonce action.
	 * @param string $field  Nonce field name in $_POST/$_REQUEST.
	 * @return void
	 */
	public static function verify_nonce( $action, $field = 'nonce' ) {
		self::$nonce_action = $action;
		self::$nonce_field  = $field;
		check_ajax_referer( $action, $field );
	}

	/**
	 * Run all public endpoint security guards in a single call.
	 *
	 * Dies automatically on any failure. Each check is optional —
	 * omit a key to skip it.
	 *
	 * Execution order: nonce → honeypot → ban → global rate limit →
	 * per-IP rate limit → burst detection → message length.
	 *
	 * @since 1.6.0
	 *
	 * @param array $config {
	 *     @type string $nonce_action       Required. Nonce action name.
	 *     @type string $ip                 Client IP. Required if using rate/ban/burst checks.
	 *     @type bool   $honeypot           Check honeypot field. Default false.
	 *     @type bool   $ban_check          Check IP ban list. Default false.
	 *     @type int    $global_limit       Global requests/min (0 = skip). Default 0.
	 *     @type int    $rate_limit         Per-IP requests/min (0 = skip). Default 0.
	 *     @type bool   $burst_detect       Enable burst detection (8+ in 10s = 2min block). Default false.
	 *     @type int    $max_message_length Max POST message length (0 = skip). Default 0.
	 *     @type string $message_field      POST field to check length against. Default 'message'.
	 * }
	 * @return void
	 */
	public static function enforce_public_guards( $config ) {
		$action = $config['nonce_action'];
		$ip     = isset( $config['ip'] ) ? $config['ip'] : '';
		$prefix = sanitize_key( str_replace( 'sitestaffr_', '', $action ) );

		// 1. Nonce verification (auto-dies on failure).
		self::verify_nonce( $action );

		// 2. Honeypot.
		if ( ! empty( $config['honeypot'] ) && self::is_honeypot_triggered() ) {
			SiteStaffr_Logger::legacy( sprintf( '[Guard:%s] Honeypot triggered for IP %s', $prefix, $ip ) );
			wp_send_json_error( array( 'message' => 'Security verification failed.' ), 403 );
		}

		// 3. Ban check.
		if ( ! empty( $config['ban_check'] ) && self::is_banned_ip( $ip ) ) {
			SiteStaffr_Logger::legacy( sprintf( '[Guard:%s] Blocked banned IP %s', $prefix, $ip ) );
			wp_send_json_error( array(
				'message'    => 'Access denied.',
				'error_code' => 'IDENTIFIER_BANNED',
				'reason'     => 'ip_banned',
			), 403 );
		}

		// 4. Global rate limit.
		$global_limit = isset( $config['global_limit'] ) ? intval( $config['global_limit'] ) : 0;
		if ( $global_limit > 0 ) {
			$global_key = 'sitestaffr_' . $prefix . '_global';
			if ( ! self::allow_rate_limited_request( $global_key, $global_limit, 60 ) ) {
				SiteStaffr_Logger::legacy( sprintf( '[Guard:%s] Global rate limit exceeded (%d/min)', $prefix, $global_limit ) );
				wp_send_json_error( array(
					'message'     => 'Service temporarily unavailable. Try again later.',
					'retry_after' => 60,
					'code'        => 'rate_limited',
				), 503 );
			}
		}

		// 5. Per-IP rate limit.
		$rate_limit = isset( $config['rate_limit'] ) ? intval( $config['rate_limit'] ) : 0;
		if ( $rate_limit > 0 && '' !== $ip ) {
			$ip_hash = hash( 'sha256', $ip );
			$ip_key  = 'sitestaffr_' . $prefix . '_ip_' . $ip_hash;
			if ( ! self::allow_rate_limited_request( $ip_key, $rate_limit, 60 ) ) {
				SiteStaffr_Logger::legacy( sprintf( '[Guard:%s] Per-IP rate limit exceeded for IP %s', $prefix, $ip ) );
				wp_send_json_error( array(
					'message'     => 'Too many requests. Please wait a moment.',
					'retry_after' => 60,
					'code'        => 'rate_limited',
				), 429 );
			}
		}

		// 6. Burst detection: 8+ requests in 10 seconds → block for 2 minutes.
		if ( ! empty( $config['burst_detect'] ) && '' !== $ip ) {
			$ip_hash     = hash( 'sha256', $ip );
			$block_key   = 'sitestaffr_' . $prefix . '_blocked_' . $ip_hash;
			$pattern_key = 'sitestaffr_' . $prefix . '_pattern_' . $ip_hash;

			// Check if already blocked.
			if ( get_transient( $block_key ) ) {
				wp_send_json_error( array(
					'message'     => 'Too many requests. Please wait a moment.',
					'retry_after' => 120,
					'code'        => 'rate_limited',
				), 429 );
			}

			$timestamps   = get_transient( $pattern_key );
			$current_time = time();
			if ( false === $timestamps ) {
				$timestamps = array();
			}
			$timestamps   = array_filter( $timestamps, function ( $ts ) use ( $current_time ) {
				return ( $current_time - $ts ) < 10;
			} );
			$timestamps[] = $current_time;

			if ( count( $timestamps ) >= 8 ) {
				set_transient( $block_key, true, 120 );
				SiteStaffr_Logger::legacy( sprintf( '[Guard:%s] Burst detected — blocking IP %s for 2min', $prefix, $ip ) );
				wp_send_json_error( array(
					'message'     => 'Too many requests. Please wait a moment.',
					'retry_after' => 120,
					'code'        => 'rate_limited',
				), 429 );
			}

			set_transient( $pattern_key, $timestamps, 10 );
		}

		// 7. Message length validation.
		$max_len = isset( $config['max_message_length'] ) ? intval( $config['max_message_length'] ) : 0;
		if ( $max_len > 0 ) {
			$msg_field = isset( $config['message_field'] ) ? $config['message_field'] : 'message';
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified above
			$raw_len = isset( $_POST[ $msg_field ] ) ? strlen( sanitize_text_field( wp_unslash( $_POST[ $msg_field ] ) ) ) : 0;
			if ( $raw_len > $max_len ) {
				wp_send_json_error( array( 'message' => 'Message too long.' ), 400 );
			}
		}
	}

	/**
	 * Read a text field from $_POST with unslash + sanitize.
	 *
	 * Caller must have already verified the nonce via verify_nonce().
	 *
	 * @since 0.14.322
	 *
	 * @param string $field Field key.
	 * @return string
	 */
	public static function get_post_text_field( $field ) {
		check_ajax_referer( self::$nonce_action, self::$nonce_field );
		if ( ! isset( $_POST[ $field ] ) ) {
			return '';
		}
		return sanitize_text_field( wp_unslash( $_POST[ $field ] ) );
	}

	/**
	 * Read an absolute integer field from $_POST with unslash.
	 *
	 * Caller must have already verified the nonce via verify_nonce().
	 *
	 * @since 0.14.360
	 *
	 * @param string $field Field key.
	 * @return int
	 */
	public static function get_post_absint( $field ) {
		check_ajax_referer( self::$nonce_action, self::$nonce_field );
		if ( ! isset( $_POST[ $field ] ) ) {
			return 0;
		}
		return absint( wp_unslash( $_POST[ $field ] ) );
	}

	/**
	 * Read a key field from $_POST with unslash + sanitize_key.
	 *
	 * Caller must have already verified the nonce via verify_nonce().
	 *
	 * @since 0.14.360
	 *
	 * @param string $field Field key.
	 * @return string
	 */
	public static function get_post_key_field( $field ) {
		check_ajax_referer( self::$nonce_action, self::$nonce_field );
		if ( ! isset( $_POST[ $field ] ) ) {
			return '';
		}
		return sanitize_key( wp_unslash( $_POST[ $field ] ) );
	}

	/**
	 * Read a structured data payload from $_POST with unslash + sanitize.
	 *
	 * Used for SDP offers (WebRTC Session Description Protocol) and JSON
	 * payloads (transcripts, usage data, contact fields). Sanitized with
	 * wp_kses() using an empty tag allowlist — strips all HTML while
	 * preserving the text/JSON content.
	 *
	 * Additional security:
	 * - Nonce verification (check_ajax_referer)
	 * - HMAC signature verification on the receiving middleware
	 * - json_decode() / SDP parser validation by the calling handler
	 *
	 * Caller must have already verified the nonce via verify_nonce().
	 *
	 * @since 0.14.360
	 *
	 * @param string $field Field key.
	 * @return string
	 */
	public static function get_post_raw_field( $field ) {
		check_ajax_referer( self::$nonce_action, self::$nonce_field );
		if ( ! isset( $_POST[ $field ] ) ) {
			return '';
		}
		$value = wp_kses( wp_unslash( $_POST[ $field ] ), array() );
		return is_string( $value ) ? $value : '';
	}

	/**
	 * Check whether honeypot field is filled.
	 *
	 * @since 0.14.322
	 *
	 * @param string $field Honeypot field name.
	 * @return bool
	 */
	public static function is_honeypot_triggered( $field = 'email' ) {
		$honeypot = self::get_post_text_field( $field );
		return ! empty( $honeypot );
	}

	/**
	 * Apply transient-based rate limiting with increment-on-pass semantics.
	 *
	 * @since 0.14.322
	 *
	 * @param string $transient_key Cache key.
	 * @param int    $max_requests  Maximum requests allowed per window.
	 * @param int    $window_seconds Window length in seconds.
	 * @return bool
	 */
	public static function allow_rate_limited_request( $transient_key, $max_requests, $window_seconds ) {
		$request_count = get_transient( $transient_key );

		if ( false === $request_count ) {
			set_transient( $transient_key, 1, $window_seconds );
			return true;
		}

		if ( $request_count >= $max_requests ) {
			return false;
		}

		set_transient( $transient_key, $request_count + 1, $window_seconds );
		return true;
	}

	/**
	 * Check whether an IP is currently banned.
	 *
	 * @since 0.14.322
	 *
	 * @param string $ip_address Visitor IP address.
	 * @return bool
	 */
	public static function is_banned_ip( $ip_address ) {
		return class_exists( 'SiteStaffr_Ban_Manager' ) && SiteStaffr_Ban_Manager::is_banned( 'ip', $ip_address );
	}

	/**
	 * Send a standardized JSON error response.
	 *
	 * @since 0.14.322
	 *
	 * @param array    $response    Error payload.
	 * @param int|null $status_code Optional HTTP status code.
	 * @return void
	 */
	public static function send_json_error( $response, $status_code = null ) {
		if ( null === $status_code ) {
			wp_send_json_error( $response );
			return;
		}

		wp_send_json_error( $response, $status_code );
	}
}
