<?php

namespace CommerceBird\Admin\Traits;

use CommerceBird\Admin\Template;

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

trait AjaxRequest {

	/**
	 * When true, the verify() method will immediately return without performing
	 * any of the usual security checks. This is only intended for use from
	 * automated unit tests.
	 *
	 * @var bool
	 */
	private static bool $skip_verify_for_tests = false;

	/**
	 * Array to store registered AJAX requests.
	 *
	 * @var array
	 */
	private array $request = array();

	/**
	 * Array to store registered AJAX response.
	 *
	 * @var array
	 */
	private array $response = array( 'message' => 'Saved' );

	/**
	 * Array to store registered AJAX posted data.
	 *
	 * @var array
	 */
	private array $data = array();

	/**
	 * Array to store registered AJAX errors.
	 *
	 * @var array
	 */
	private array $errors = array();

	/**
	 * Load all registered AJAX actions
	 *
	 * Loops through the self::ACTIONS array and registers each action
	 * with WordPress using the add_action hook.
	 */
	private function load_actions() {
		foreach ( self::ACTIONS as $action => $handler ) {
			add_action(
				$this->action( $action ),
				array( $this, $handler ),
			);
		}
	}

	/**
	 * Serve data to AJAX request.
	 */
	/**
	 * @visibleForTesting
	 */
	public function _test_get_response(): array {
		return $this->response;
	}

	/**
	 * @visibleForTesting
	 */
	public function _test_get_errors(): array {
		return $this->errors;
	}

	/**
	 * Toggle the verify bypass flag used in tests.
	 *
	 * @param bool $skip
	 */
	public static function _test_set_skip_verify( bool $skip ): void {
		self::$skip_verify_for_tests = $skip;
	}

	/**
	 * Serve data to AJAX request.
	 */
	private function serve(): void {
		if ( count( $this->errors ) > 0 ) {
			wp_send_json_error( $this->errors );
		}

		wp_send_json_success( $this->response );
	}

	/**
	 * Verify AJAX request with enhanced security.
	 *
	 * @param array $keys The keys to verify (optional).
	 */
	private function verify( array $keys = array() ): void {
		// testing bypass.
		if ( self::$skip_verify_for_tests ) {
			return;
		}
		// Early exit: these Ajax handlers should never be called by logged-out users.
		// WordPress wp_ajax_ actions are only for logged-in users, so this shouldn't happen.
		if ( ! is_user_logged_in() ) {
			// If we reach here, something is wrong with the action registration.
			// Just exit silently to prevent frontend errors.
			wp_die( '', '', array( 'response' => 200 ) );
		}

		// Check if the request is coming from the admin area (for logged-in users).
		$referer = wp_get_referer();
		if ( ! $this->is_valid_admin_referer( $referer ) ) {
			$this->errors = array( 'message' => 'Invalid request origin' );
			$this->serve();
			return;
		}

		// Enhanced CSRF protection with proper nonce verification.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified below.
		$security_token = isset( $_REQUEST['security_token'] ) ? wp_unslash( $_REQUEST['security_token'] ) : ( isset( $_GET['security_token'] ) ? wp_unslash( $_GET['security_token'] ) : '' );
		$nonce_action   = Template::NAME;

		if ( ! wp_verify_nonce( $security_token, $nonce_action ) ) {
			$this->errors = array( 'message' => 'Security check failed' );
			$this->serve();
			return;
		}

		// Rate limiting check - only for actions that make Zoho API calls.
		if ( $this->requires_zoho_api_call() && ! $this->check_zoho_rate_limit() ) {
			$this->errors = array( 'message' => 'Zoho API rate limit exceeded. Please wait and try again.' );
			$this->serve();
			return;
		}

		// Capability check - ensure user has proper permissions (same as menu page).
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			$this->errors = array( 'message' => 'Insufficient permissions' );
			$this->serve();
			return;
		}

		// Initialize response and errors.
		$this->response = array( 'success' => true );
		$this->errors   = array();
		$this->request  = array_map( 'sanitize_text_field', wp_unslash( $_REQUEST ) );

		// Attempt to retrieve JSON if POST is empty.
		if ( empty( $_POST ) ) {
			$contents = trim( file_get_contents( 'php://input' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended.

			if ( $this->is_json( $contents ) ) {
				$json_data = json_decode( $contents, true );

				if ( ! empty( $json_data ) ) {
					// Deep sanitization of JSON data.
					$this->data = empty( $keys ) ? $this->sanitize_deep( $json_data ) : $this->extract_data( $this->sanitize_deep( $json_data ), $keys );
				}
			}
		} else {
			// Sanitize POST data.
			$this->data = empty( $keys ) ? $this->sanitize_deep( $_POST ) : $this->extract_data( $this->sanitize_deep( $_POST ), $keys );
		}

		// Log security events for audit.
		$this->log_security_event();
	}

	/**
	 * Validate that request referer belongs to this site's admin area.
	 *
	 * This supports local development setups where the admin referer may include
	 * a different port than admin_url(), while still requiring the same host and
	 * an admin path prefix.
	 *
	 * @param string|false $referer Request referer URL.
	 * @return bool
	 */
	private function is_valid_admin_referer( $referer ): bool {
		if ( empty( $referer ) ) {
			return false;
		}

		$admin_url = admin_url();

		// Fast path when URLs match exactly.
		if ( strpos( $referer, $admin_url ) === 0 ) {
			return true;
		}

		$referer_parts = wp_parse_url( $referer );
		$admin_parts   = wp_parse_url( $admin_url );

		if ( ! is_array( $referer_parts ) || ! is_array( $admin_parts ) ) {
			return false;
		}

		$referer_host = $referer_parts['host'] ?? '';
		$admin_host   = $admin_parts['host'] ?? '';

		if ( '' === $referer_host || '' === $admin_host || $referer_host !== $admin_host ) {
			return false;
		}

		$admin_path   = untrailingslashit( $admin_parts['path'] ?? '/wp-admin' );
		$referer_path = $referer_parts['path'] ?? '';

		return '' !== $referer_path && strpos( $referer_path, $admin_path ) === 0;
	}

	/**
	 * Utility to check if a string is JSON.
	 */
	private function is_json( string $string ): bool {
		json_decode( $string );
		return json_last_error() === JSON_ERROR_NONE;
	}

	/**
	 * Extracts data from an array using the given keys.
	 *
	 * @param array $sanitized The array from which to extract data.
	 * @param array $keys The keys to use for extraction.
	 *
	 * @return array The extracted data.
	 */
	public function extract_data( array $sanitized, array $keys ): array {
		return array_intersect_key( $sanitized, array_flip( $keys ) );
	}

	/**
	 * Register AJAX actions.
	 *
	 * @param string $action The action to register.
	 * @return string The action name.
	 */
	private function action( $action ): string {
		return sprintf( 'wp_ajax_%s-%s', Template::NAME, $action );
	}

	/**
	 * Check if the current action requires Zoho API calls.
	 * Override this method in classes that need specific rate limiting rules.
	 *
	 * @return bool
	 */
	protected function requires_zoho_api_call(): bool {
		// Get the current action being processed.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Action name used for rate limiting check.
		$current_action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : '';

		// Actions that are internal AJAX calls (no Zoho API interaction).
		$internal_ajax_actions = array(
			'get_zoho_categories',   // Fetches cached categories data.
			'get_zoho_taxes',        // Fetches cached tax data.
			'get_zoho_locations',    // Fetches cached location data.
			'get_zoho_prices',       // Fetches cached price data.
			'get_wc_taxes',          // WordPress internal data.
			'save_zoho_',            // All save operations are internal settings.
			'get_zoho_connect',      // Connection settings retrieval.
			'reset_zoho_',           // Reset operations are internal.
			'is_connected',          // Connection check uses cached data.
		);

		// Check if current action is internal (should NOT be rate limited).
		foreach ( $internal_ajax_actions as $internal_action ) {
			if ( strpos( $current_action, $internal_action ) !== false ) {
				return false;
			}
		}

		// Actions that make actual Zoho API calls (external requests - SHOULD be rate limited).
		$zoho_api_actions = array(
			'zoho_ajax_call_item',                     // Product sync to Zoho.
			'zoho_ajax_call_variable_item_from_zoho',  // Fetch variable products from Zoho.
			'zoho_ajax_call_item_from_zoho',           // Fetch simple products from Zoho.
			'zoho_ajax_call_composite_item_from_zoho', // Fetch composite products from Zoho.
			'zoho_ajax_call_composite_item',           // Sync composite products to Zoho.
			'zoho_ajax_call_parent_categories',        // Sync parent categories (makes Zoho API calls).
			'zoho_ajax_call_subcategories',            // Sync subcategories (makes Zoho API calls).
			'zoho_ajax_call_subcategories_start',      // Start async subcategory sync.
			'zoho_ajax_call_subcategories_batch',      // Process subcategory sync batch.
			'zoho_ajax_call_subcategories_status',     // Get subcategory sync status.
			'zoho_ajax_call_remove_duplicates',        // Remove duplicates (makes Zoho API calls).
			'import_zoho_contacts',                    // Contact sync from Zoho.
			'convert_guest',                           // Convert guest customers (Zoho API calls).
		);

		// Check if current action makes Zoho API calls.
		foreach ( $zoho_api_actions as $api_action ) {
			if ( strpos( $current_action, $api_action ) !== false ) {
				return true;
			}
		}

		// Default to no rate limiting for unknown actions.
		return false;
	}

	/**
	 * Check Zoho API rate limit (based on Zoho's own rate limiting).
	 *
	 * @return bool
	 */
	private function check_zoho_rate_limit(): bool {
		// Check if Zoho has indicated rate limit exceeded.
		$zoho_rate_limit_exceeded = get_option( 'cmbird_zoho_rate_limit_exceeded', false );

		if ( $zoho_rate_limit_exceeded ) {
			// Check if enough time has passed since the rate limit was hit.
			$rate_limit_time = get_option( 'cmbird_zoho_rate_limit_time', 0 );
			$current_time    = time();

			// Zoho typically resets limits every minute, so wait 65 seconds to be safe.
			if ( ( $current_time - $rate_limit_time ) < 65 ) {
				return false;
			} else {
				// Enough time has passed, reset the rate limit flag.
				update_option( 'cmbird_zoho_rate_limit_exceeded', false );
				delete_option( 'cmbird_zoho_rate_limit_time' );
			}
		}

		return true;
	}

	/**
	 * Deep sanitization of data arrays.
	 *
	 * @param mixed $data Data to sanitize.
	 * @return mixed Sanitized data.
	 */
	private function sanitize_deep( $data ) {
		if ( is_array( $data ) ) {
			return array_map( array( $this, 'sanitize_deep' ), $data );
		}

		if ( is_string( $data ) ) {
			return sanitize_text_field( $data );
		}

		return $data;
	}

	/**
	 * Log security events for audit trail.
	 */
	private function log_security_event(): void {
		$log_data = array(
			'timestamp'  => current_time( 'mysql' ),
			'user_id'    => get_current_user_id(),
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Security logging.
			'ip_address' => isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : 'unknown',
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Security logging.
			'action'     => isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : 'unknown',
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Security logging.
			'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown',
		);

		// Store in transient for security monitoring.
		$security_log   = get_transient( 'commercebird_security_log' ) ?: array();
		$security_log[] = $log_data;

		// Keep only last 100 entries.
		if ( count( $security_log ) > 100 ) {
			$security_log = array_slice( $security_log, -100 );
		}

		set_transient( 'commercebird_security_log', $security_log, DAY_IN_SECONDS );
	}
}
