<?php

namespace EasyCriticalCSS;

use WP_Error;
use WP_REST_Request;

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

class REST_API {
	public static $route_namespace = 'easy-critical-css/v1';

	public static $max_raw_bytes     = 5242880; // 5 * 1024 * 1024 (5 MB).
	public static $max_decoded_bytes = 12582912; // 12 * 1024 * 1024 (12 MB).
	public static $max_css_bytes     = 2097152; // 2 * 1024 * 1024 (2 MB).

	public static function init() {
		add_action( 'rest_api_init', [ __CLASS__, 'register_routes' ] );
	}

	public static function register_routes() {
		register_rest_route(
			self::$route_namespace,
			'/receive',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'handle_critical_css' ],
				// Open permission_callback because request validation is securely handled at the start of `handle_critical_css` callback through a handshake validation.
				'permission_callback' => '__return_true',
				// Intentionally omitting 'args' here so the REST server does not pre-validate/parse the request body and gzipped content can be passed through.
				// Validation and parsing (including gzip handling) is performed inside the callback.
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/status/(?P<post_id>\d+)',
			[
				'methods'             => 'GET',
				'callback'            => [ __CLASS__, 'get_critical_css_status' ],
				// Open permission_callback so Critical CSS API can retrieve the Critical CSS status upon generation.
				'permission_callback' => '__return_true',
				'args'                => [
					'post_id' => [
						'required' => true,
						'type'     => 'integer',
					],
				],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/get-settings/(?P<id>\d+)',
			[
				'methods'             => 'GET',
				'callback'            => [ __CLASS__, 'get_settings_api' ],
				// Open permission_callback so Critical CSS API can retrieve the Critical CSS settings upon generation.
				'permission_callback' => '__return_true',
				'args'                => [
					'id' => [
						'required'          => true,
						'validate_callback' => function ( $param ) {
							return is_numeric( $param );
						},
					],
				],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/update-settings/(?P<id>\d+)',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'update_settings_api' ],
				'permission_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
				'args'                => [
					'id' => [
						'required'          => true,
						'validate_callback' => function ( $param ) {
							return is_numeric( $param );
						},
					],
				],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/generate',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'handle_generate_request' ],
				'permission_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
				'args'                => [
					'identifier' => [
						'required'          => true,
						'validate_callback' => function ( $param ) {
							return is_numeric( $param ) || filter_var( $param, FILTER_VALIDATE_URL );
						},
					],
				],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/delete',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'handle_delete_request' ],
				'permission_callback' => function () {
					return current_user_can( 'manage_options' );
				},
				'args'                => [
					'identifier' => [
						'required'          => true,
						'validate_callback' => function ( $param ) {
							return is_numeric( $param ) || filter_var( $param, FILTER_VALIDATE_URL );
						},
					],
				],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/content',
			[
				'methods'             => 'GET',
				'callback'            => [ __CLASS__, 'get_page_content' ],
				// Open permission_callback because request validation is securely handled at the start of `get_page_content` callback through a handshake validation.
				'permission_callback' => '__return_true',
				'args'                => [
					'url'                  => [
						'required'          => true,
						'type'              => 'string',
						'validate_callback' => 'esc_url_raw',
					],
					'ignoreCrossDomainCSS' => [
						'required' => false,
						'default'  => true,
						'type'     => 'boolean',
					],
					'excludeCSS'           => [
						'required' => false,
						'type'     => 'array',
						'items'    => [ 'type' => 'string' ],
					],
				],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/debug',
			[
				'methods'             => 'GET',
				'callback'            => [ 'EasyCriticalCSS\Debug', 'debug' ],
				'permission_callback' => [ 'EasyCriticalCSS\Debug', 'debug_permission' ],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/debug/(?P<hash>[A-Za-z0-9_-]+)',
			[
				'methods'             => 'GET',
				'callback'            => [ 'EasyCriticalCSS\Debug', 'debug_generated_css' ],
				'permission_callback' => [ 'EasyCriticalCSS\Debug', 'debug_permission' ],
				'args'                => [
					'hash' => [
						'required'          => true,
						'validate_callback' => function ( $param ) {
							return is_string( $param ) && preg_match( '#^[A-Za-z0-9_-]+$#', $param );
						},
					],
				],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/debug-log',
			[
				'methods'             => 'GET',
				'callback'            => [ 'EasyCriticalCSS\Debug', 'view_log' ],
				'permission_callback' => [ 'EasyCriticalCSS\Debug', 'debug_permission' ],
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/receive-test',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'handle_receive_test' ],
				// Open permission_callback because request validation is securely handled at the start of `handle_receive_test` callback through a handshake validation.
				'permission_callback' => '__return_true',
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/refresh-auto-mode-status',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'refresh_auto_mode_status' ],
				'permission_callback' => function () {
					return current_user_can( 'manage_options' );
				},
			]
		);

		register_rest_route(
			self::$route_namespace,
			'/dismiss-notification',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'dismiss_notification' ],
				'permission_callback' => function () {
					return current_user_can( 'manage_options' );
				},
			]
		);

		// Activate auto generation via REST (called from admin JS)
		register_rest_route(
			self::$route_namespace,
			'/activate-auto',
			[
				'methods'             => 'POST',
				'callback'            => [ __CLASS__, 'activate_auto_generation' ],
				'permission_callback' => function () {
					return current_user_can( 'manage_options' );
				},
			]
		);
	}

	private static function validate_criticalcss_net_url( $url ) {
		// Allow empty for inline-only cases.
		if ( $url === '' ) {
			return true;
		}

		// Basic URL syntax check.
		if ( filter_var( $url, FILTER_VALIDATE_URL ) === false ) {
			return false;
		}

		// Parse out the host and scheme.
		$parts = wp_parse_url( $url );
		if ( empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
			return false;
		}

		// Must be https.
		if ( 'https' !== $parts['scheme'] ) {
			return false;
		}

		// Host must end in ".criticalcss.net".
		if ( ! preg_match( '/(^|\.)criticalcss\.net$/i', $parts['host'] ) ) {
			return false;
		}

		return true;
	}

	private static function validate_receive_params( $params ) {
		// Basic shape
		if ( ! is_array( $params ) ) {
			return new WP_Error( 'invalid_params', __( 'Invalid request parameters.', 'easy-critical-css' ), [ 'status' => 400 ] );
		}

		// Required fields
		if ( empty( $params['hash'] ) ) {
			return new WP_Error( 'missing_hash', __( 'Hashed URL is missing.', 'easy-critical-css' ), [ 'status' => 400 ] );
		}

		if ( ! preg_match( '/^[A-Fa-f0-9]{32}$/', $params['hash'] ) ) {
			return new WP_Error( 'invalid_hash', __( 'Invalid hash format.', 'easy-critical-css' ), [ 'status' => 400 ] );
		}

		if ( empty( $params['handshake'] ) ) {
			return new WP_Error( 'missing_handshake', __( 'Handshake key is missing.', 'easy-critical-css' ), [ 'status' => 400 ] );
		}

		if ( ! is_string( $params['handshake'] ) ) {
			return new WP_Error( 'invalid_type', __( 'handshake must be a string.', 'easy-critical-css' ), [ 'status' => 400 ] );
		}

		// URL if present should be valid and a string.
		if ( isset( $params['url'] ) ) {
			if ( ! is_string( $params['url'] ) || filter_var( $params['url'], FILTER_VALIDATE_URL ) === false ) {
				return new WP_Error( 'invalid_url', __( 'Invalid URL provided.', 'easy-critical-css' ), [ 'status' => 400 ] );
			}
		}

		// Per-CSS type & size checks.
		foreach ( [ 'criticalCSS', 'secondaryCSS', 'remainingCSS' ] as $css_key ) {
			if ( isset( $params[ $css_key ] ) ) {
				if ( ! is_string( $params[ $css_key ] ) ) {
					// translators: %s is the CSS parameter name.
					return new WP_Error( 'invalid_type', sprintf( __( '%s must be a string.', 'easy-critical-css' ), $css_key ), [ 'status' => 400 ] );
				}
				if ( strlen( $params[ $css_key ] ) > self::$max_css_bytes ) {
					// translators: %s is the CSS parameter name.
					return new WP_Error( 'css_too_large', sprintf( __( '%s too large.', 'easy-critical-css' ), $css_key ), [ 'status' => 413 ] );
				}
			}
		}

		// Validate CSS URLs.
		foreach ( [ 'criticalCSSUrl', 'remainingCSSUrl', 'secondaryCSSUrl' ] as $url_key ) {
			if ( isset( $params[ $url_key ] ) && $params[ $url_key ] !== '' ) {
				if ( ! is_string( $params[ $url_key ] ) || ! self::validate_criticalcss_net_url( $params[ $url_key ] ) ) {
					// translators: %s is the CSS URL parameter name.
					return new WP_Error( 'invalid_css_url', sprintf( __( '%s is not a valid criticalcss.net URL.', 'easy-critical-css' ), $url_key ), [ 'status' => 400 ] );
				}
			}
		}

		return true;
	}

	private static function remove_old_css_files( $hash ) {
		$upload_dir = wp_upload_dir();
		$base_dir   = trailingslashit( $upload_dir['basedir'] ) . 'easy-critical-css/';
		$types      = [ 'critical', 'secondary', 'remaining' ];

		foreach ( $types as $type ) {
			$pattern = $base_dir . $type . '/' . $hash . '*.css';
			foreach ( glob( $pattern ) as $file ) {
				wp_delete_file( $file );
			}
		}
	}

	public static function save_css_to_file( $type, $hash, $css, $timestamp ) {
		if ( empty( $type ) || empty( $css ) || empty( $hash ) || empty( $timestamp ) ) {
			return false;
		}

		$upload_dir = wp_upload_dir();
		$base_dir   = trailingslashit( $upload_dir['basedir'] ) . "easy-critical-css/$type/";
		wp_mkdir_p( $base_dir );

		$file_name = ( $type === 'critical' ) ? "$hash.css" : "$hash-$timestamp.css";
		$file_path = $base_dir . $file_name;

		// We are writing to wp-content/uploads which is writable on all standard WordPress installs.
		// WP_Filesystem is not needed here and may cause permission prompts on some hosts.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		$result = file_put_contents( $file_path, $css );

		return $result !== false ? $file_path : false;
	}

	public static function handle_critical_css( WP_REST_Request $request ) {
		// Diagnostic logs: log content encoding and content length for debugging.
		$content_encoding = ! empty( $_SERVER['HTTP_CONTENT_ENCODING'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_CONTENT_ENCODING'] ) ) : '';
		$content_length   = ! empty( $_SERVER['CONTENT_LENGTH'] ) ? intval( wp_unslash( $_SERVER['CONTENT_LENGTH'] ) ) : 0;

		// Quick reject if Content-Length advertises a too-large body.
		if ( $content_length > 0 && $content_length > self::$max_raw_bytes ) {
			return new WP_Error(
				'request_too_large',
				__( 'Request body too large.', 'easy-critical-css' ),
				[ 'status' => 413 ]
			);
		}

		// Accept gzipped request bodies if sent that way.
		$raw = file_get_contents( 'php://input' );

		if ( stripos( $content_encoding, 'gzip' ) !== false ) {
			if ( function_exists( 'gzdecode' ) ) {
				$decoded = gzdecode( $raw );
				if ( $decoded === false ) {
					return new WP_Error(
						'invalid_gzip',
						__( 'Invalid gzip encoding.', 'easy-critical-css' ),
						[ 'status' => 400 ]
					);
				}
				$raw = $decoded;
			} else {
				return new WP_Error(
					'no_gzdecode',
					__( 'Server does not support gzip decoding.', 'easy-critical-css' ),
					[ 'status' => 501 ]
				);
			}
		}

		// If gzipped and decoded above, parse JSON from $raw; otherwise, use normal param parsing.
		if ( ! empty( $raw ) && stripos( $content_encoding, 'gzip' ) !== false ) {
			// Stop any too large payloads before JSON decoding.
			if ( strlen( $raw ) > self::$max_decoded_bytes ) {
				return new WP_Error(
					'request_too_large',
					__( 'Decoded request body too large.', 'easy-critical-css' ),
					[ 'status' => 413 ]
				);
			}
			$params = json_decode( $raw, true );
			if ( ! is_array( $params ) ) {
				return new WP_Error(
					'invalid_json',
					__( 'Invalid JSON in gzipped body.', 'easy-critical-css' ),
					[ 'status' => 400 ]
				);
			}
			$handshake = ! empty( $params['handshake'] ) ? sanitize_text_field( $params['handshake'] ) : '';
		} else {
			$params    = $request->get_params(); // Get all request parameters at once.
			$handshake = sanitize_text_field( $request->get_param( 'handshake' ) );
		}

		// Centralized validation of incoming params (hash, handshake, sizes, urls).
		$valid = self::validate_receive_params( $params );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}

		// Pull results from db to check for proper handshake.
		$existing = Database::get_row_by_url_hash( $params['hash'] );
		if ( empty( $existing ) ) {
			return new WP_Error(
				'hash_not_found',
				__( 'No matching record found for the provided hash.', 'easy-critical-css' ),
				[ 'status' => 404 ]
			);
		}

		if ( empty( $existing['handshake'] ) || ! hash_equals( sanitize_text_field( $existing['handshake'] ), $handshake ) ) {
			return new WP_Error(
				'invalid_handshake',
				__( 'Invalid handshake key.', 'easy-critical-css' ),
				[ 'status' => 403 ] // HTTP 403 Forbidden.
			);
		}

		// If no inline CSS, but we have URLs, fetch them now.
		$css_types = [
			'critical'  => 'criticalCSS',
			'remaining' => 'remainingCSS',
			'secondary' => 'secondaryCSS',
		];
		foreach ( $css_types as $type => $param_key ) {
			$url_key = $type . 'Url';
			if ( empty( $params[ $param_key ] ) && ! empty( $params[ $url_key ] ) ) {
				$resp = wp_remote_get( esc_url_raw( $params[ $url_key ] ), [ 'timeout' => 20 ] );
				if ( is_wp_error( $resp ) || 200 !== wp_remote_retrieve_response_code( $resp ) ) {
					if ( $type === 'critical' ) {
						return new WP_Error(
							'css_fetch_failed',
							__( 'Could not fetch critical CSS from URL.', 'easy-critical-css' ),
							[ 'status' => 502 ]
						);
					}
					continue;
				}
				$params[ $param_key ] = wp_remote_retrieve_body( $resp );
			}
		}

		// Now if we are empty, this is a legit failure.
		if ( empty( $params['criticalCSS'] ) ) {
			// Store CSS failure.
			$new_data = [
				'url_hash'          => $params['hash'],
				'processing_status' => 'failed',
			];
			Database::upsert_row( $new_data );

			// Record failure in pattern tracking.
			Failure_Patterns::record_failure( $params['hash'], Failure_Patterns::FAILURE_GENERIC );

			return new WP_Error(
				'no_critical_css',
				__( 'Missing Critical CSS.', 'easy-critical-css' ),
				[ 'status' => 400 ] // HTTP 403 Forbidden.
			);
		}

		// Determine secondary behavior and prep data to save.
		$behavior          = Settings::get_individual_secondary_behavior( $params['hash'] );
		$is_secondary_mode = $behavior === 'secondary';
		$secondary_css     = $is_secondary_mode ? $params['secondaryCSS'] : '';
		$remaining_css     = ! $is_secondary_mode ? $params['remainingCSS'] : '';

		// Save to files if needed.
		$timestamp = current_time( 'mysql' );
		if ( Settings::get_global_serve_css_from_files() ) {
			// Delete any old CSS files.
			self::remove_old_css_files( $params['hash'] );

			$file_timestamp  = gmdate( 'YmdHis', strtotime( $timestamp ) );
			$critical_saved  = self::save_css_to_file( 'critical', $params['hash'], $params['criticalCSS'], $file_timestamp );
			$secondary_saved = self::save_css_to_file( 'secondary', $params['hash'], $secondary_css, $file_timestamp );
			$remaining_saved = self::save_css_to_file( 'remaining', $params['hash'], $remaining_css, $file_timestamp );

			// Remove from DB save if we saved them locally.
			if ( $critical_saved ) {
				$params['criticalCSS'] = '';
			}
			if ( $secondary_saved ) {
				$secondary_css = '';
			}
			if ( $remaining_saved ) {
				$remaining_css = '';
			}
		}

		// Calculate savings of using secondary CSS.
		$remaining_size = strlen( $params['remainingCSS'] );
		$secondary_size = strlen( $params['secondaryCSS'] );

		$savings_bytes   = max( 0, $remaining_size - $secondary_size );
		$savings_percent = $remaining_size > 0
			? round( ( $savings_bytes / $remaining_size ) * 100, 2 )
			: 0;

		$savings = wp_json_encode(
			[
				'bytes'   => $savings_bytes,
				'percent' => $savings_percent,
			]
		);

		// Set requested_time now so later reads don't think it's missing.
		$requested_time_to_save = $timestamp;

		// Store generated CSS.
		$new_data = [
			'url_hash'          => $params['hash'],
			'critical_css'      => $params['criticalCSS'],
			'remaining_css'     => $remaining_css,
			'secondary_css'     => $secondary_css,
			'size_savings'      => $savings,
			'generated_time'    => $timestamp,
			'requested_time'    => $requested_time_to_save,
			'handshake'         => null, // Clear to prevent brute force attempts
			'processing_status' => 'completed',
		];

		// Capture previous row for debugging and upsert.
		$previous = Database::get_row_by_url_hash( $params['hash'] );

		Database::upsert_row( $new_data );

		// Record successful generation in failure tracking.
		Failure_Patterns::record_success( $params['hash'] );

		// Clear in-memory/static cache to prevent stale cache from showing an outdated status such as 'expired'.
		if ( ! empty( $params['hash'] ) ) {
			Critical_CSS::clear_cache( $params['hash'] );
		}

		// Dedupe any additional rows now that we have valid handshake and generated CSS.
		$resolved_post_id = null;
		if ( isset( $params['post_id'] ) && is_numeric( $params['post_id'] ) ) {
			$resolved_post_id = (int) $params['post_id'];
		} elseif ( ! empty( $previous ) && ! empty( $previous['post_id'] ) ) {
			$resolved_post_id = (int) $previous['post_id'];
		}

		if ( empty( $resolved_post_id ) ) {
			Debug::ecc_log(
				[
					'step'   => 'receive_dedupe_skipped',
					'reason' => 'no_post_id',
					'hash'   => $params['hash'],
				]
			);
		} else {
			global $wpdb;
			$table = esc_sql( Database::get_table_name() );
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Required SQL for deduplication, specific context makes caching unnecessary.
			$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE post_id = %d", $resolved_post_id ) );

			if ( (int) $count > 1 ) {
				$dedupe_result = Database::dedupe_post_rows( $resolved_post_id, $handshake, false );
				Debug::ecc_log(
					[
						'step'   => 'receive_dedupe_apply',
						'result' => $dedupe_result,
					]
				);
			} else {
				Debug::ecc_log(
					[
						'step'    => 'receive_dedupe_skipped',
						'reason'  => 'no_duplicates',
						'post_id' => $resolved_post_id,
					]
				);
			}
		}

		// Mark REST API reachability as successful so we don't trigger repeated
		// remote tests immediately after receiving CSS. Cache for 6 hours.
		Timed_Option::set( 'is_rest_api_reachable', '1', 6 * HOUR_IN_SECONDS );

		/**
		 * Fires immediately after Critical CSS has been generated and saved for a single post/page/archive etc...
		 *
		 * @since 1.0.0
		 *
		 * @param string $identifier Unique identifier of the generated Critical CSS.
		 */
		do_action( 'easy_cc_single_generated', $params['hash'] );

		return rest_ensure_response( [ 'message' => __( 'Critical CSS received successfully.', 'easy-critical-css' ) ] );
	}

	public static function get_critical_css_status( WP_REST_Request $request ) {
		$post_id = $request->get_param( 'post_id' );

		if ( ! $post_id || get_post_status( $post_id ) !== 'publish' ) {
			return new WP_Error(
				'invalid_post_id',
				__( 'Invalid post ID or post is not published.', 'easy-critical-css' ),
				[ 'status' => 400 ]
			);
		}

		$generated_css = Critical_CSS::get_generated_css( $post_id );
		$status        = Critical_CSS_Status::get_status( $post_id );

		return rest_ensure_response(
			[
				'status'         => $status,
				'generated_time' => $generated_css['generated_time'],
				'requested_time' => $generated_css['requested_time'],
				'size_savings'   => $generated_css['size_savings'],
				'critical_css'   => ! empty( $generated_css['critical_css'] ),
				'secondary_css'  => ! empty( $generated_css['secondary_css'] ),
				'remaining_css'  => ! empty( $generated_css['remaining_css'] ),
			]
		);
	}

	public static function get_settings_api( $request ) {
		$post_id = $request['id'];

		if ( ! $post_id ) {
			return new WP_Error( 'missing_post_id', __( 'Missing post ID.', 'easy-critical-css' ) );
		}

		$settings = Individual_Settings::get_settings( $post_id );

		return rest_ensure_response( $settings );
	}

	public static function update_settings_api( $request ) {
		$post_id  = $request['id'];
		$new_data = $request->get_json_params();

		if ( ! $post_id || ! is_array( $new_data ) ) {
			return new WP_Error( 'invalid_request', __( 'Invalid request.', 'easy-critical-css' ) );
		}

		Individual_Settings::save_settings( $post_id, $new_data );

		return rest_ensure_response( [ 'success' => true ] );
	}

	public static function handle_generate_request( WP_REST_Request $request ) {
		$identifier = $request->get_param( 'identifier' );
		$result     = API_Service::request_critical_css( $identifier );

		return rest_ensure_response( $result );
	}

	public static function handle_delete_request( WP_REST_Request $request ) {
		$identifier = $request->get_param( 'identifier' );
		$result     = Delete_Handler::delete_one( $identifier );

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return rest_ensure_response(
			[
				'success' => true,
				'message' => __( 'Critical CSS styles deleted for the page.', 'easy-critical-css' ),
			]
		);
	}

	/**
	 * Extracts and validates a nonce from request headers or params.
	 *
	 * @param  WP_REST_Request $request
	 *
	 * @return boolean|WP_Error  True if valid, WP_Error if invalid or missing.
	 */
	private static function validate_nonce( WP_REST_Request $request ) {
		$nonce = $request->get_header( 'X-WP-Nonce' );
		if ( empty( $nonce ) ) {
			$nonce = $request->get_param( '_wpnonce' );
		}

		if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
			return new WP_Error(
				'invalid_nonce',
				__( 'Invalid security token.', 'easy-critical-css' ),
				[ 'status' => 403 ]
			);
		}

		return true;
	}

	/**
	 * Checks if a handshake matches the stored value (or global override).
	 *
	 * @param  string $url
	 * @param  string $handshake
	 *
	 * @return boolean|WP_Error  True if valid, false if mismatch, WP_Error if URL not found.
	 */
	public static function has_valid_handshake( $url, $handshake ) {
		// Is there a global override in place?
		if ( defined( 'EASY_CC_GLOBAL_API_KEY' ) && hash_equals( EASY_CC_GLOBAL_API_KEY, sanitize_text_field( $handshake ) ) ) {
			return true;
		}

		$existing = Database::get_row_by_url( $url );
		if ( empty( $existing ) ) {
			return new WP_Error(
				'url_not_found',
				__( 'No matching record found for the provided URL.', 'easy-critical-css' ),
				[ 'status' => 404 ]
			);
		}

		if ( ! hash_equals( sanitize_text_field( $existing['handshake'] ), $handshake ) ) {
			return false;
		}

		return true;
	}

	public static function get_page_content( WP_REST_Request $request ) {
		$url       = esc_url_raw( $request->get_param( 'url' ) );
		$handshake = sanitize_text_field( $request->get_param( 'handshake' ) );

		// Check for required handshake.
		if ( empty( $handshake ) ) {
			return new WP_Error(
				'missing_handshake',
				__( 'Handshake key is missing.', 'easy-critical-css' ),
				[ 'status' => 400 ]
			);
		}

		$valid_handshake = self::has_valid_handshake( $url, $handshake );
		if ( is_wp_error( $valid_handshake ) ) {
			return $valid_handshake;
		}
		if ( ! $valid_handshake ) {
			return new WP_Error(
				'invalid_handshake',
				__( 'Invalid handshake key.', 'easy-critical-css' ),
				[ 'status' => 403 ] // HTTP 403 Forbidden.
			);
		}

		$ignore_cross_domain = (bool) $request->get_param( 'ignoreCrossDomainCSS' );
		$exclude_patterns    = ! empty( $request->get_param( 'excludeCSS' ) ) ? $request->get_param( 'excludeCSS' ) : [];

		$response = wp_remote_get( $url, [ 'User-Agent' => 'EasyCriticalCSS WP Fetch' ] );
		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'fetch_error', 'Unable to fetch content', [ 'status' => 500 ] );
		}

		$body = wp_remote_retrieve_body( $response );

		// Load HTML to get stylesheets
		preg_match_all( '#<link[^>]+rel=["\']stylesheet["\'][^>]*>#i', $body, $matches );
		$stylesheet_hrefs = [];
		foreach ( $matches[0] as $tag ) {
			if ( preg_match( '/href=["\']([^"\']+)["\']/', $tag, $href_match ) ) {
				$stylesheet_hrefs[] = $href_match[1];
			}
		}

		// Filter by cross-domain & excludeCSS
		$site_host = wp_parse_url( $url, PHP_URL_HOST );
		$allowed   = array_filter(
			$stylesheet_hrefs,
			function ( $href ) use ( $site_host, $ignore_cross_domain, $exclude_patterns ) {
				$parsed_host = wp_parse_url( $href, PHP_URL_HOST );
				$host        = ! empty( $parsed_host ) ? $parsed_host : $site_host;
				// Cross-domain?
				if ( $ignore_cross_domain && $host !== $site_host ) {
					return false;
				}
				// Excluded pattern?
				foreach ( $exclude_patterns as $pat ) {
					if ( strpos( $href, $pat ) !== false ) {
						return false;
					}
				}
				return true;
			}
		);

		// Fetch CSS and rewrite any non-absolute URLs
		$css_combined = '';
		foreach ( $allowed as $href ) {
			$uncached_href = add_query_arg( [ 'critical-css' => 'skip' . current_time( 'YmdHis' ) ], $href );
			$css_res       = wp_remote_get( $uncached_href );
			if ( is_wp_error( $css_res ) ) {
				continue;
			}

			$css_body = wp_remote_retrieve_body( $css_res );

			// Rewrite every url(...) found: https://regex101.com/r/Kk9s8b/1
			$rewritten = preg_replace_callback(
				'/url\(\s*([\'"]?)([^\'")]+)\1\s*\)/i',
				function ( $matches ) use ( $href ) {
					list(, $quote, $asset_path) = $matches;

					// Data-URIs or full URLs? Leave alone!
					if ( preg_match( '#^(?:https?:|data:)#i', $asset_path ) ) {
						return $matches[0];
					}

					// Protocol-relative? Leave alone!
					if ( strpos( $asset_path, '//' ) === 0 ) {
						return "url({$quote}{$asset_path}{$quote})";
					}

					// Root-relative? Make absolute!
					if ( strpos( $asset_path, '/' ) === 0 ) {
						$origin = untrailingslashit( home_url() );
						return "url({$quote}{$origin}{$asset_path}{$quote})";
					}

					// Root-relative? Make absolute!
					$base     = trailingslashit( dirname( $href ) );
					$absolute = $base . $asset_path;
					return "url({$quote}{$absolute}{$quote})";
				},
				$css_body
			);

			$css_combined .= $rewritten . ' ';
		}

		$response = [
			'html'        => $body,
			'stylesheets' => $stylesheet_hrefs,
			'allowed'     => $allowed,
			'css'         => $css_combined,
		];

		return rest_ensure_response( $response );
	}

	public static function handle_receive_test( WP_REST_Request $request ) {
		$nonce = sanitize_text_field( $request->get_param( 'nonce' ) );

		if ( empty( $nonce ) ) {
			return new WP_Error(
				'missing_nonce',
				__( 'Nonce is required.', 'easy-critical-css' ),
				[ 'status' => 400 ]
			);
		}

		$stored_nonce = Timed_Option::get( 'receive_test_nonce' );
		if ( ! $stored_nonce || $stored_nonce !== $nonce ) {
			return new WP_Error(
				'invalid_nonce',
				__( 'Invalid or expired nonce.', 'easy-critical-css' ),
				[ 'status' => 403 ]
			);
		}

		// Success, clean it up.
		Timed_Option::delete( 'receive_test_nonce' );

		return rest_ensure_response( [ 'success' => true ] );
	}

	public static function refresh_auto_mode_status( WP_REST_Request $request ) {
		$nonce = self::validate_nonce( $request );
		if ( is_wp_error( $nonce ) ) {
			return $nonce;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return new WP_Error(
				'insufficient_permissions',
				__( 'You do not have permission to perform this action.', 'easy-critical-css' ),
				[ 'status' => 403 ]
			);
		}

		// Clear the REST API reachability cache and get fresh status.
		delete_transient( 'easy_cc_is_rest_api_reachable' );
		$status = Helpers::get_auto_mode_status( true );

		return rest_ensure_response( $status );
	}

	public static function dismiss_notification( WP_REST_Request $request ) {
		$nonce = self::validate_nonce( $request );
		if ( is_wp_error( $nonce ) ) {
			return $nonce;
		}

		// Dismiss for a year to avoid nagging.
		Timed_Option::set( 'auto_mode_setup_notif', time(), YEAR_IN_SECONDS );

		return rest_ensure_response( [ 'success' => true ] );
	}

	public static function activate_auto_generation( WP_REST_Request $request ) {
		$nonce = self::validate_nonce( $request );
		if ( is_wp_error( $nonce ) ) {
			return $nonce;
		}

		update_option( 'easy_cc_critical_css_mode', 'auto' );

		// Dismiss notification for a day to avoid immediate nagging.
		Timed_Option::set( 'auto_mode_setup_notif', time(), DAY_IN_SECONDS );

		return rest_ensure_response( [ 'success' => true ] );
	}
}
