<?php if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * AI Security and Rate Limiting
 */
class EPKB_AI_Security {

	const RATE_LIMIT_TRANSIENT_PREFIX = 'epkb_ai_chat_rate_';
	const GLOBAL_RATE_LIMIT_TRANSIENT = 'epkb_ai_chat_global_rate';
	const SESSION_COOKIE_NAME = 'epkb_ai_session';
	const SESSION_COOKIE_EXPIRY = 0; // Browser session
	const CHAT_ID_PREFIX = 'chat_';
	
	private $nonce = null;
	private static $session_creation_lock = false;

	/**
	 * Create a new nonce for REST request
	 * @return string|null
	 */
	public function get_nonce() {

		if ( isset( $this->nonce ) ) {
			return $this->nonce;
		}

		$this->nonce = wp_create_nonce( 'wp_rest' );

		return $this->nonce;
	}

	/**
	 * REST API nonce verification
	 * rest_nonce = "is this request really from my site?"
	 * Security – proves the request originated from a page generated by your WordPress site (CSRF‑style protection)
	 * Life: 24h max (first 12h return code 1, next 12h return code 2)
	 * where: HTTP header X‑WP‑Nonce (or a restNonce field if the route looks for it)
	 * @param $request
	 * @return true|WP_Error true if valid, WP_Error if invalid
	 */
	public static function check_rest_nonce( $request ) {

		// Validates nonce from X-WP-Nonce header using WordPress nonce system.
		// Returns: false (invalid), 1 (0-12 hours old), or 2 (12-24 hours old)
		$nonce = $request->get_header( 'X-WP-Nonce' );
		if ( empty( $nonce ) ) {
			return new WP_Error( 'rest_missing_nonce', __( 'Missing nonce.', 'echo-knowledge-base' ), array( 'status' => 401 ) );
		}
		
		$rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
		if ( ! $rest_nonce ) {
			return new WP_Error( 'invalid_nonce', __( 'Invalid nonce.', 'echo-knowledge-base' ), array( 'status' => 401 ) );
		}

		// Apply filter and check result
		/* $filtered_result = apply_filters( 'epkb_rest_authorized', $rest_nonce, $request );
		if ( is_wp_error( $filtered_result ) ) {
			return $filtered_result;
		} elseif ( $filtered_result === false ) {
			return new WP_Error( 'rest_forbidden', __( 'Access denied.', 'echo-knowledge-base' ), array( 'status' => 403 ) );
		} */
		
		// Nonce is valid
		return true;
	}

	// admins only
	public static function can_access_settings() {
		return apply_filters( 'epkb_allow_configuration', current_user_can( 'manage_options' ) );
	}

	// admins and editors
	public static function can_access_features() {
		$editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
		return apply_filters( 'epkb_allow_features', $editor_or_admin );
	}

	public static function can_access_public_api( $route, $request ) {
		$logged_in = is_user_logged_in();
		return apply_filters( 'epkb_allow_public_api', $logged_in, $route, $request );
	}

	/**
	 * Get or create a session ID
	 * Creates a new session if one doesn't exist and headers haven't been sent
	 * 
	 * @return string|WP_Error Session ID or WP_Error on failure
	 */
	public static function get_or_create_session() {
		
		// Check for existing session
		$session_id = self::get_session_id();
		if ( is_wp_error( $session_id ) ) {
			// Invalid session in cookie - clear it and create new one
			self::set_session_cookie( '' ); // Clear cookie
		}

		// Prevent concurrent session creation
		if ( self::$session_creation_lock ) {
			// Wait a moment and try again
			usleep( 500000 ); // 500ms
			$session_id = self::get_session_id();
			if ( ! is_wp_error( $session_id ) ) {
				return $session_id;
			}

			return new WP_Error( 'session_locked', __( 'Session creation in progress', 'echo-knowledge-base' ) );
		}
		
		self::$session_creation_lock = true;
		
		try {
			// Double-check after acquiring lock
			$session_id = self::get_session_id();
			if ( ! is_wp_error( $session_id ) ) {
				return $session_id;
			}
			
			// Create new session if possible
			if ( ! headers_sent() && ! wp_doing_cron() && ! defined( 'WP_CLI' ) ) {
				$session_id = self::generate_session_id();
				if ( self::set_session_cookie( $session_id ) ) {  // set the cookie with new session ID
					return $session_id;
				}
			}
			
			// Return proper error for cron jobs
			if ( wp_doing_cron() ) {
				return 'wp-cron'; // Special case for cron jobs - already validated in is_session_valid
			}
			
			// Return proper error for WP-CLI
			/** @disregard P1011 */
			if ( defined( 'WP_CLI' ) && WP_CLI ) {
				return 'wp-cli'; // Special case for WP-CLI - should be validated in is_session_valid
			}
			
			// Return WP_Error instead of 'N/A'
			return new WP_Error( 'headers_sent', __( 'Cannot create session - headers already sent', 'echo-knowledge-base' ), array( 'status' => 503 ) );

		} finally {
			self::$session_creation_lock = false;
		}
	}

	/**
	 * Generate a secure session ID
	 *
	 * @return string 32 character hex string
	 */
	public static function generate_session_id() {
		// Try multiple methods for generating secure random data
		$bytes = false;
		
		// Method 1: random_bytes (most secure, PHP 7+)
		if ( function_exists( 'random_bytes' ) ) {
			try {
				$bytes = random_bytes( 16 );
			} catch ( Exception $e ) {
				$bytes = false;
			}
		}
		
		// Method 2: openssl_random_pseudo_bytes (widely compatible, PHP 5.3+)
		if ( $bytes === false && function_exists( 'openssl_random_pseudo_bytes' ) ) {
			$strong = false;
			$bytes = openssl_random_pseudo_bytes( 16, $strong );
			// Only use if cryptographically strong
			if ( ! $strong ) {
				$bytes = false;
			}
		}
		
		// Method 3: Fallback using multiple entropy sources
		if ( $bytes === false ) {
			// Combine multiple sources of entropy
			$entropy = uniqid( '', true );                    // Microsecond precision
			$entropy .= mt_rand();                            // Mersenne Twister
			$entropy .= microtime( true );                    // Current time with microseconds
			$entropy .= serialize( $_SERVER );                // Server variables
			if ( function_exists( 'wp_salt' ) ) {
				$entropy .= wp_salt( 'auth' );                // WordPress salt if available
			}
			
			// Hash the combined entropy
			return substr( hash( 'sha256', $entropy ), 0, 32 );
		}
		
		// Method 4: WordPress's built-in random generation (requires WordPress functions)
		if ( $bytes === false && function_exists( 'wp_generate_password' ) ) {
			// Generate 32 random hex characters using WordPress
			return substr( md5( wp_generate_password( 64, true, true ) . microtime( true ) . uniqid( '', true ) ), 0, 32 );
		}
		
		// Convert bytes to hex string
		return bin2hex( $bytes );
	}

	/**
	 * Get session ID from cookie (read-only)
	 * This method only reads existing session, does not create new ones
	 *
	 * @return bool|WP_Error Empty string if no session exists
	 */
	public static function get_session_id() {
		$session_id = isset( $_COOKIE[self::SESSION_COOKIE_NAME] ) ? sanitize_text_field( $_COOKIE[self::SESSION_COOKIE_NAME] ) : '';
		$result = EPKB_AI_Validation::validate_session( $session_id );
		return is_wp_error( $result ) ? $result : $session_id;
	}

	/**
	 * Clear session cookie
	 * 
	 * @return bool
	 */
	private static function set_session_cookie( $session_id ) {
		// Set expiry to past time to delete cookie
		$cookie_options = array(
			'expires'  => empty( $session_id) ? time() - 3600 : self::SESSION_COOKIE_EXPIRY, // an hour ago or 0 for browser session
			'path'     => COOKIEPATH ?: '/',
			'domain'   => COOKIE_DOMAIN ?: '',
			'secure'   => is_ssl(),
			'httponly' => true,
			'samesite' => 'Lax'
		);
		
		// PHP 7.3+ supports options array
		if ( version_compare( PHP_VERSION, '7.3.0', '>=' ) ) {
			return setcookie( self::SESSION_COOKIE_NAME, $session_id, $cookie_options );
		}
		
		// Fallback for older PHP versions
		return setcookie( 
			self::SESSION_COOKIE_NAME,
			$session_id, 
			$cookie_options['expires'],
			$cookie_options['path'],
			$cookie_options['domain'],
			$cookie_options['secure'],
			$cookie_options['httponly']
		);
	}
	
	/**
	 * Validate that a chat ID belongs to the current session
	 * Prevents session hijacking and unauthorized access
	 * 
	 * @param string $chat_id
	 * @param string $session_id Optional, uses current session if not provided
	 * @return bool
	 */
	public static function validate_chat_session( $chat_id, $session_id ) {
		
		if ( empty( $chat_id ) ) {
			return false;
		}
		
		if ( empty( $session_id ) ) {
			$session_id = self::get_session_id();
			if ( is_wp_error( $session_id ) ) {
				return false;
			}
		}

		// Check database for matching chat_id and session_id
		$messages_db = new EPKB_AI_Messages_DB();
		$conversation = $messages_db->get_conversation_by_chat_and_session( $chat_id, $session_id );
		
		// If not found, check if this is a recently created conversation (within last 5 seconds)
		// This handles race conditions where the second message arrives before DB write completes
		if ( $conversation === null ) {
			// Check if chat_id format is valid (should start with prefix)
			if ( strpos( $chat_id, self::CHAT_ID_PREFIX ) !== 0 ) {
				return false;
			}
			
			// Try once more with a small delay (100ms)
			usleep( 100000 );
			$conversation = $messages_db->get_conversation_by_chat_and_session( $chat_id, $session_id );
		}
		
		return $conversation !== null;
	}

	/**
	 * Ensure that user state is the same as the one who started the chat
	 *
	 * @param string $chat_id Chat ID (UUID)
	 * @return bool|WP_Error True if valid, WP_Error on failure
	 */
	public static function validate_user_matching( $chat_id ) {

		if ( empty( $chat_id ) ) {
			return new WP_Error( 'invalid_chat_id', __( 'Chat ID is required', 'echo-knowledge-base' ) );
		}

		$current_user_id = get_current_user_id();

		// Get the conversation by chat ID (UUID)
		$handler = new EPKB_AI_Messages_DB();
		$conversation = $handler->get_conversation_by_chat_id( $chat_id );
		if ( $conversation === null ) {
			return new WP_Error( 'invalid_chat_id_record', __( 'Chat record does not exist for this session', 'echo-knowledge-base' ) . ' ' . $chat_id );
		}

		// Check if the user ID matches (logged off user is 0) or if user is admin
		if ( $conversation->get_user_id() !== $current_user_id ) {
			// Check if it's a guest/logged-in mismatch for better error messaging
			if ( ( $conversation->get_user_id() === 0 && $current_user_id > 0 ) ||
				( $conversation->get_user_id() > 0 && $current_user_id === 0 ) ) {
				return new WP_Error( 'user_state_changed', __( 'Your login status has changed. Please start a new conversation.', 'echo-knowledge-base' ) );
			}

			return new WP_Error( 'user_mismatch', __( 'You are not authorized to continue this chat session', 'echo-knowledge-base' ) );
		}

		return true;
	}

	/**
	 * Check if user has exceeded rate limits
	 * 
	 * @param string $user_identifier - Can be user ID or IP hash
	 * @return bool|WP_Error - True if allowed, WP_Error if rate limited
	 */
	public static function check_rate_limit( $user_identifier = '' ) {

		// Get user identifier
		if ( empty( $user_identifier ) ) {
			$user_identifier = self::get_user_identifier();
		}
		
		// Check global rate limit first
		$global_limit = apply_filters( 'epkb_ai_chat_global_rate_limit', 1000 ); // 1000 requests per hour globally
		$global_count = get_transient( self::GLOBAL_RATE_LIMIT_TRANSIENT );
		
		if ( $global_count >= $global_limit ) {
			return new WP_Error( 'global_rate_limit', __( 'Chat service is temporarily unavailable due to high demand. Please try again later.', 'echo-knowledge-base' ), array( 'retry_after' => 60 ) );  // 1 minute
		}
		
		// Check user rate limit
		$user_limit = apply_filters( 'epkb_ai_chat_user_rate_limit', 50 ); // 50 requests per hour per user
		$user_transient = self::RATE_LIMIT_TRANSIENT_PREFIX . $user_identifier;
		$user_count = get_transient( $user_transient );
		
		if ( $user_count >= $user_limit ) {
			return new WP_Error( 'user_rate_limit', __( 'You have reached the chat limit. Please try again in a minute.', 'echo-knowledge-base' ), array( 'retry_after' => 60 ) );  // 1 minute
		}
		
		// Increment counters
		set_transient( self::GLOBAL_RATE_LIMIT_TRANSIENT, $global_count + 1, HOUR_IN_SECONDS );
		set_transient( $user_transient, $user_count + 1, HOUR_IN_SECONDS );
		
		return true;
	}
	
	/**
	 * Get unique user identifier for rate limiting
	 * 
	 * @return string
	 */
	private static function get_user_identifier() {
		
		// For logged-in users, use user ID
		if ( is_user_logged_in() ) {
			return 'user_' . get_current_user_id();
		}
		
		// For guests, use hashed IP (GDPR compliant)
		return 'ip_' . hash( 'sha256', self::get_client_ip() . wp_salt() );
	}
	
	/**
	 * Get client IP address
	 * 
	 * @return string
	 */
	private static function get_client_ip() {
		
		$ip_keys = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' );
		
		foreach ( $ip_keys as $key ) {
			if ( array_key_exists( $key, $_SERVER ) === true ) {
				foreach ( explode( ',', $_SERVER[$key] ) as $ip ) {
					$ip = trim( $ip );
					
					if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) {
						return $ip;
					}
				}
			}
		}
		
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
	}
	
	/**
	 * Sanitize output for display
	 * 
	 * @param string $text
	 * @return string
	 */
	public static function sanitize_output( $text ) {
		
		// Allow basic formatting tags
		$allowed_tags = apply_filters( 'epkb_ai_chat_allowed_tags', array(
			'p' => array(),
			'br' => array(),
			'strong' => array(),
			'em' => array(),
			'u' => array(),
			'ol' => array(),
			'ul' => array(),
			'li' => array(),
			'code' => array(),
			'pre' => array(),
			'blockquote' => array(),
			'a' => array(
				'href' => array(),
				'title' => array(),
				'target' => array(),
				'rel' => array()
			)
		) );
		
		return wp_kses( $text, $allowed_tags );
	}
	
	/**
	 * Log security events
	 * 
	 * @param string $event_type
	 * @param array $data
	 */
	public static function log_security_event( $event_type, $data = array() ) { // TODO
		
		if ( ! apply_filters( 'epkb_ai_chat_log_security_events', true ) ) {
			return;
		}
		
		$log_entry = array(
			'timestamp' => gmdate( 'Y-m-d H:i:s' ),
			'event' => $event_type,
			'ip_hash' => self::get_ip_hash(),
			'user_id' => get_current_user_id(),
			'data' => $data
		);
		
		// Store in transient with 7-day expiration
		$logs = get_transient( 'epkb_ai_chat_security_logs' );
		if ( ! is_array( $logs ) ) {
			$logs = array();
		}
		
		// Keep only last 100 entries
		if ( count( $logs ) >= 100 ) {
			array_shift( $logs );
		}
		
		$logs[] = $log_entry;
		set_transient( 'epkb_ai_chat_security_logs', $logs, WEEK_IN_SECONDS );
	}

	private static function get_ip_hash() {
		return hash( 'sha256', self::get_client_ip() . wp_salt() );
	}
}